Skip to content

resolver: stop framework synthesizers from full-scanning the graph#267

Merged
zzet merged 1 commit into
mainfrom
perf/framework-synth-dispatch-cost
Jul 8, 2026
Merged

resolver: stop framework synthesizers from full-scanning the graph#267
zzet merged 1 commit into
mainfrom
perf/framework-synth-dispatch-cost

Conversation

@zzet

@zzet zzet commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • On a real 26-repo daemon, the framework dispatch synthesis global pass took 9,667.76s (161 min) — 97.6% of the 165-minute post-resolve settle phase, with most of the ~46 registered synthesizers landing zero edges on this corpus.
  • Root cause: two synthesizers (ResolveFnValueCallbacks, ResolveFactoryChains) scanned every edge in the graph via AllEdges() and filtered by kind in Go, instead of using the existing kind-scoped EdgesByKind/edgesByKinds helpers every other synthesizer already uses (which push the filter into SQL).
  • ResolveFnValueCallbacks additionally re-issued a fresh GetFileNodes SQL round-trip per candidate edge, not per file — on this real graph, 522,679 candidate placeholder edges triggered that many round-trips, with a single generated file (tree-sitter-swift/src/parser.c) alone responsible for 199,117 of them against a file holding only 24 nodes.
  • Added per-synthesizer and per-tail-pass elapsed-time logging to RunFrameworkSynthesizers so a regression like this shows up directly in the existing log line instead of hiding behind one aggregate duration.

Measured

Isolated before/after against a .backup'd snapshot of the real 26-repo store (live daemon untouched throughout):

synthesizer before after edges (before → after)
fn-value-callback >51 min (killed, still running) 5m19.6s — → 15,950 (daemon log showed 15,947 pre-fix)
factory-chain 17.0s 8.5s 0 → 0

Same resolution results (edge counts match within noise), dramatically faster.

Test plan

  • GOWORK=off go build ./...
  • GOWORK=off go vet ./internal/resolver/... ./internal/indexer/...
  • GOWORK=off go test ./internal/resolver/... ./internal/indexer/...
  • GOWORK=off go test -race ./internal/resolver/... ./internal/indexer/...

Two of the framework-dispatch synthesizers (fn-value-callback,
factory-chain) scanned every edge in the graph via AllEdges() and
filtered by kind in Go, instead of using the existing kind-scoped
EdgesByKind/edgesByKinds helpers that push the filter into SQL. On a
large multi-repo graph this meant decoding several times more edges
(including their Meta blobs) than the pass could ever act on.

fn-value-callback additionally re-fetched a file's node list via a
fresh GetFileNodes SQL round-trip for every candidate in that file,
rather than once per file — turning a file with a large candidate
count (a generated parser, an ORM-generated source file) into
hundreds of thousands of redundant queries against a handful of
nodes. Caching the fetch per file removes the redundancy.

Also add per-synthesizer and per-tail-pass elapsed-time logging to
RunFrameworkSynthesizers so a future regression like this shows up
in the existing per-pass log line instead of hiding behind one
aggregate duration.
@zzet zzet merged commit ce5588b into main Jul 8, 2026
9 checks passed
@zzet zzet deleted the perf/framework-synth-dispatch-cost branch July 8, 2026 08:49
vitaliyslion pushed a commit to vitaliyslion/gortex that referenced this pull request Jul 8, 2026
Same treatment as the attribution sweep in the prior commit: the six
lang-dispatch passes (relative/Lua/Razor imports, non-Go module
attribution, Java/PHP override dispatch) shared one opaque duration.
Broken out so a slow one has a breadcrumb instead of hiding behind the
aggregate.

Investigated a red herring while measuring this: several of these
passes independently EdgesByKind-scan the same import edges, which
looked like the redundant cross-pass pattern the framework-synthesizer
fix (zzet#267) addressed. Measured cost was negligible (~1.5s combined on
a 26-repo store) — not worth touching. The real, previously-invisible
cost turned out to be the untimed gap around the pending-edge
collection itself; see the follow-up investigation for that.
zzet added a commit that referenced this pull request Jul 10, 2026
resolver: stop framework synthesizers from full-scanning the graph
zzet added a commit that referenced this pull request Jul 10, 2026
Same treatment as the attribution sweep in the prior commit: the six
lang-dispatch passes (relative/Lua/Razor imports, non-Go module
attribution, Java/PHP override dispatch) shared one opaque duration.
Broken out so a slow one has a breadcrumb instead of hiding behind the
aggregate.

Investigated a red herring while measuring this: several of these
passes independently EdgesByKind-scan the same import edges, which
looked like the redundant cross-pass pattern the framework-synthesizer
fix (#267) addressed. Measured cost was negligible (~1.5s combined on
a 26-repo store) — not worth touching. The real, previously-invisible
cost turned out to be the untimed gap around the pending-edge
collection itself; see the follow-up investigation for that.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant