Repository navigation
perf(resolution): run the resolver workers the main thread can keep busy - #3183
Merged
Merged
Conversation
colbymchenry
force-pushed
the
claude/spd2-resolver-pool
branch
3 times, most recently
from
November 1, 2026 09:37
498d133 to
4e5526f
Compare
The pool started 6 workers on any large project, though the main thread saves every batch itself and keeps about 4 busy. A further worker mostly waited, having cost a second or two of CPU to load and to compile the hot code in its own isolate, a share of the others' cache hits, and a few hundred MB. That's a large part of why rocksdb's CPU rose 31% for a wall rise of 8%: V8's background threads went from 12 to 25 s, mostly compiling, for more code in every isolate. The pool now takes at most 4, and fewer when a run projects little work. Workers 6 to 4, 2 rounds: rocksdb 125.9 to 107.3 s CPU, 2780 to 2425 MB, best wall 37.0 to 35.8 s; react 113.4 to 103.2; django 70.0 to 60.0; vscode-be 53.2 to 47.3, wall 14.4 to 12.2. Dumps of the whole stack are byte-identical on 24 repos. The rest of the waiting was the batch barrier: 10 chunks over a few workers, and the next batch held until the slowest was done. Past the prerequisites no batch makes an edge resolving reads, so the next one now queues behind it. A file's chunks stay on one worker, which has its cached reads. The WAL valve parks only at a pool-idle boundary, since a full backfill needs the readers between statements, so it now waits for a batch with nothing queued ahead, which the connection recycle guarantees every 8 batches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
force-pushed
the
claude/spd2-resolver-pool
branch
from
November 1, 2026 09:39
4e5526f to
c2e11c0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The resolver pool started 6 workers on any large project, though the main thread, which saves every batch itself, keeps about 4 busy. The pool now takes at most 4, and fewer when a run projects little work. Batches queue behind each other past the prerequisites, and a file's chunks stay on one worker.
Stacked on the shared scans PR, which adds a line to
createPoolthis one edits.Root cause
rocksdb's CPU rose 31% from 1.6.2 with its wall up 8%. Per thread (
process.threadCpuUsagein every thread, against process CPU): main +3 s, resolver workers +19 s, V8's background threads 12 to 25 s, mostly compiling. V8 compiles per isolate, so every worker pays for every hot function again, and there is more code than there was. Tier flags don't help:--no-maglev,--no-turbofanand--no-sparkpluggain nothing. Fewer isolates do.The workers were also idle about 40% of the time, at the batch barrier: 10 chunks over the pool, and the next batch held until the slowest finished.
The fix
MAX_RESOLVE_WORKERS = 4, andresolveWorkersWorthStarting: a mid-size run that engages the pool from its own measured rate takes 2 to 4 by what it projects.CODEGRAPH_RESOLVE_WORKERSstill overrides.extendsorimplementsedge, the only ones resolving reads, so the next batch queues behind it. Should one make such an edge, the batch queued ahead is dropped and starts again once the edges are in: results only take effect at settle, in order.Two boundaries needed the pool idle, and keep it: the connection recycle (no batch queues ahead of one), and the WAL valve's parked backfill, which needs the readers between statements (#1539). The valve now waits for a batch with nothing queued ahead, which the recycle guarantees every 8 batches, so the WAL can overshoot the hard cap by at most that much.
Validation
Windows 11.
resolver-pool-sizing.test.tspins the sizes.resolver-pool-pipeline.test.ts: a fake pool proves the loop queues ahead (2 batches running at once) and that both the valve and the recycle find it idle (red without the valve's check), and a real pool on a generated project makes the graph one thread makes.Graphs. Full dumps of the whole stack against its base, byte-identical on 24 repos: rocksdb, redis, leveldb, fmt (C, C++), react, excalidraw, taxonomy, express (JS, TS), django, flask (Python), rails-src, sinatra (Ruby), framework, ci3 (PHP), vscode-be (TS), jellyfin, serilog (C#), jsoup, nowinandroid (Java, Kotlin), gin, caddy (Go), tokio, ripgrep (Rust), Alamofire (Swift).
Workers, 2 rounds, wall (best) / CPU / peak RSS. Machine noise is large, so read the best round and the CPU:
4 costs no wall anywhere. 3 saves more CPU and starts to cost wall.
The whole stack, 1.6.2 / 4499d00 / this, medians of 3, interleaved. This side also carries the regression fixes that landed since 4499d00, which add work of their own: on rocksdb, against the main it's based on, 33.6 to 29.2 s, 131.1 to 107.8 s CPU, 2692 to 2485 MB in a quieter hour.
A sync after an edit:
What's left on rocksdb: thread CPU is level with 1.6.2 and profile self time below it (93.5, 109.8, 88.1 s). V8's background threads are the difference, 13 to 23.5 s.
🤖 Generated with Claude Code