Cut LSP semantic-enrichment cost on cold index#262
Merged
Conversation
The reference-confirm pass positions textDocument/references on each ambiguous edge's TARGET declaration, not its call site, so every edge pointing at the same overloaded or hot declaration re-issued the identical query. Measured fan-in is 4.7x-9.4x for call/reference edges, so a common target drew that many redundant round-trips. Cache the reference list per target node within the confirm group and let the per-edge site match filter the shared list: byte-identical confirm and refute decisions, far fewer server round-trips. A clean A/B on a small Go repo cut the gopls enrichment pass 2.1x (230s to 108s, references 6475 to 2199) with identical confirmed/added/enriched counts.
The reference-confirm target set was every confidence<1.0 edge, so it also issued a textDocument/references round-trip for structural-containment edges (member_of, defines, contains, param_of, imports, captures). Those anchor a declaration's fixed relationship to its enclosing structure — they carry no use site a reference list can adjudicate, so the match is meaningless and, on a miss, the definition-rebind fallback can mutate a correct edge's target. On a four-repo cold index these kinds are a third of the target set (member_of alone is the single largest confidence<1.0 kind). Confine the confirm targets to edges that anchor a use, type-position, or dataflow site the server can resolve; skip the structural-containment family. Use-site (calls, references, reads, writes, instantiates), type-position (typed_as, returns) and dataflow (arg_of, value_flow, returns_to) edges stay confirmable, so their LSP disambiguation is preserved — verified by a store diff that is byte-identical for every kept kind, including the arg_of rebind onto the call's actual EnqueueJob overload. The one edge that changes is a member_of on a local variable that the fallback had mis-rebound onto an unrelated same-named method; skipping it restores the resolver's correct bind. req_references drops ~35% on a small Go repo on top of the per-target cache.
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.
Cut LSP semantic-enrichment cost on cold index
Profiling a real multi-repo cold index showed the wall-time is dominated by LSP semantic enrichment, not resolve. Two gopls/pyright passes alone accounted for ~28 min:
textDocument/referencestextDocument/referencesreferencesrequests dominate every enrichment pass. They come from the reference-confirm pass, which promotes/refutes each ambiguous (confidence < 1.0) edge by asking the server for the target declaration's references and checking the edge's site is among them. Two inefficiencies made that pass issue far more round-trips than it needs.1. Cache the reference list per target declaration
findReferencesis positioned on the edge's target declaration, not its call site, so every ambiguous edge pointing at the same overloaded/hot declaration re-issued the identical query. Measured fan-in is 4.7×–9.4× for call/reference edges. The list is now fetched once per target within a confirm group and the per-edge site match filters the shared list — byte-identical confirm/refute decisions, far fewer round-trips.Clean A/B, cold gopls pass on a small Go repo: duration 230s → 108s (2.1×),
req_references6,475 → 2,199, with identicalconfirmed/added/nodes_enriched.2. Skip structural-containment edges
The confirm target set was every
confidence < 1.0edge — with no kind filter. So it also issued a reference round-trip for structural-containment edges (member_of,defines,contains,param_of,imports,captures) — a third of the target set,member_ofalone being the single largest kind. Those anchor a declaration's fixed relationship to its enclosing structure; they carry no use site a reference list can adjudicate, and on a miss the definition-rebind fallback can mutate a correct edge's target.The confirm targets are now confined to edges that anchor a use / type-position / dataflow site the server can actually resolve. Use-site (
calls,references,reads,writes,instantiates), type-position (typed_as,returns) and dataflow (arg_of,value_flow,returns_to) edges stay confirmable, so their LSP disambiguation is preserved.Verified by a full store diff (dedup vs +filter, same repo):
arg_ofrebind onto the call's actualPool.EnqueueJoboverload (confirmed against source) and thetyped_as/returnsbinds to an interface rather than a concrete impl.member_ofon a local variable that the fallback had mis-rebound onto an unrelated same-named method; skipping it restores the resolver's correct bind.Net: 0 regressions, 1 misbind fixed, and
req_referencesdrops a further ~35% on top of the per-target cache.Verification
GOWORK=off go build ./...GOWORK=off go test -race ./internal/semantic/lsp/ ./internal/resolver/ ./internal/indexer/ ./internal/mcp/— greenHOME(the live daemon untouched); byte-level store diff for the correctness proof above.At-scale A/B (23k-node Go repo, warm gopls cache — isolates the confirm pass)
req_referencesnodes_enrichedaddedconfirmeddrops 37,323 → 28,722 — that is the structural-containment kinds no longer being promoted to the lsp tier; their targets are unchanged (proven by the byte-level store diff on the smaller repo, and the mechanism is repo-independent). Cold, the same before-pass ran >17 min without completing.