Skip to content

idea: LSP enrichment as a continuous background lane, with a much faster bulk path #605

Description

@pbednarcik

Not a bug report, more of a direction I'd like to explore and discuss, with numbers from
my production C# monorepo (~117k nodes) now that the lane actually works (#603 + #604).

What the first working pass showed

The first complete enrichment pass produced 33,841 LSP-origin edges at confidence 1.0:
26,178 calls, 3,426 implements, 2,944 overrides, 1,187 extends. It confirmed 5,474
heuristic edges and added 26,782 new ones. The server itself was flawless the whole time,
0 errors, 0 reconnects. Compiler-grade edges at this volume change what the graph can
answer.

But the pass took 84.5 minutes to reach 22% node coverage and then hit its deadline
(partial: true, 24,620 of 110,908 candidates). That's on a 16-core machine with plenty
of RAM, against a repo that is honestly not that big, and the sweep started from a
workspace that was fully loaded in about 30 seconds. Under real working conditions
(branch switches, files moving around) a pass this slow never finishes. The world changes
faster than the sweep covers it.

Why I think it's slow, and what I'd like to try

24,620 nodes in 5,069s is about 5 nodes/second. My working assumption (not profiled yet)
is that this rate comes from the protocol, not from compute: the sweep pays several
JSON-RPC round trips per node (hover, definition, references, call hierarchy) over one
stdio connection, so both sides plausibly spend most of the wall clock waiting on each
other. Worth verifying with a profile before optimizing. The ideas below don't depend on
which half dominates, roughly in order of leverage:

  1. Bulk-export instead of per-node interrogation. For a batch sweep, use the compiler
    as a compiler: one pass over the loaded solution's semantic models can emit every
    call/implements/overrides/extends binding in roughly the time of a build. Minutes for
    the whole workspace instead of hours at 5 nodes/s. LSP then stays for what it is good
    at: incremental, per-change updates on a warm server. On the Roslyn side this could be
    a small companion exporter, and other compilers have equivalents.
  2. Sweep only where heuristics are uncertain, by default. Most of the 110k candidates
    are edges the resolver already binds at high confidence. Prioritizing
    ambiguous/unresolved targets would deliver most of the correction value at a fraction
    of the requests (the pass already prioritizes unresolved-demand, narrowing the
    candidate set outright would go further). The exhaustive sweep that also re-checks
    confident edges is still worth keeping, but as an opt-in mode: it is the only pass
    that audits high-confidence heuristic edges instead of trusting them, and it pairs
    naturally with the disagreement ledger below. Same opt-in posture as eager LSP and the
    C# resolver helper.
  3. Persist sweep progress and invalidate incrementally. Today the pass is one-shot
    and the next full pass starts over. Remembering per-node coverage, resuming across
    passes, and re-sweeping only changed files after a branch switch turns "never
    finishes" into "converges and stays fresh".
  4. Ordering. The graph went queryable at ~7 minutes; the sweep then ran another 84
    minutes inside the warmup window, holding the daemon in "warming" long after every
    query worked. I'd spawn LSP after the cheap passes land and run the sweep strictly in
    the background at low priority. Nothing should gate on it.
  5. Wire up refuted plus a disagreement log. EnrichResult.EdgesRefuted is declared
    and logged but, as far as I can tell, never set anywhere. So an LSP answer that
    disagrees with a heuristic edge changes nothing today. Wiring it, ideally with a
    per-edge record of disagreements (edge, heuristic tier/confidence, LSP verdict), makes
    the lane self-auditing and shows exactly which heuristic tiers mis-bind most.

The division of labor this is aiming at

Heuristics build the complete, fast, no-build graph. The compiler-backed lane owns the
long tail that would be very complex to reimplement by hand: overload resolution with
implicit conversions, flow-dependent typing, and especially source-generator output (code
that never exists as text in the repo, invisible to any text-based extractor, while the
compiler sees it natively). Heuristics leave the hard cases honestly unresolved and the
lane upgrades them continuously in the background. Instant availability plus
compiler-grade accuracy that compounds, without an 84-minute tax.

Happy to prototype any of these on this monorepo and report numbers.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions