Skip to content

[perf][feature] Precomputed reference index — make CodeLens counts / FAR O(1) lookups instead of per-request searches #189

Description

@msarson

Goal

Make reference counts (and eventually Find-All-References / "who calls X") O(refs) lookups instead of per-request searches. After #188 the per-method CodeLens cost is dominated by the reference search scanning the file set on every count (~1–2s/method). A precomputed symbol → [reference sites] index turns each count into a map lookup, independent of how many methods a file has.

This is Mark's proposal (2026-06-07): "build a list of the methods in some graph, then just analyze references to those methods — would work well with procedure implementations as well."

Why it's feasible now

Data structure

class ReferenceIndex {
  // symbolKey → reference sites across the whole solution
  private bySymbol: Map<string, ReferenceSite[]>;
  // file uri → the symbolKeys it contributes sites to (for incremental invalidation)
  private byFile: Map<string, Set<string>>;
  count(symbolKey): number;        // O(1)
  references(symbolKey): ReferenceSite[];
  reindexFile(uri, sites): void;   // remove old contributions of uri, add new
  removeFile(uri): void;
  isReady(): boolean;
}
ReferenceSite = { uri, line, character };

symbolKey (normalized identity — must mirror how references resolve):

Phasing (each independently shippable, behaviour-preserving)

  • Phase 1 — substrate. ReferenceIndex class + a pure extractReferenceSites(tokens, uri, resolve) that walks call/dot-access/type-usage sites and yields {symbolKey, site}. Unit-tested in isolation. Not wired to anything → zero behaviour risk.
  • Phase 2 — build + wire counts. Build the index at solutionReady (one pass over solution files, cheap tokens, yield per file per [perf][umbrella] Yield + honor cancellation across hot LSP search loops (WorkspaceSymbol, Implementation, overload/include walks) #187). Point onCodeLensResolve at index.count(key) with fallback to the live FAR when the index isn't ready or misses (so it's never wrong, only faster). Measure.
  • Phase 3 — incremental. On didChangeContent, reindexFile(changedUri) (using byFile to drop stale contributions) instead of the current global codeLensGeneration bump; refresh CodeLens.
  • Phase 4 — precision + reuse. Overload-signature discrimination; let FAR / "who calls X" read from the index too.

Correctness guardrails

  • Phase 2 keeps the live FAR as the fallback/source of truth; the index is a cache. Compare index counts vs FAR counts in tests on disk fixtures before trusting it.
  • Symbol-identity must match FAR's resolution or counts drift — pin with tests that assert index.count(sym) === provideReferences(sym).length on representative fixtures.
  • mtime/byFile invalidation so external edits don't serve stale counts (same discipline as getTokensForClosedFile).

Cross-ref

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions