Skip to content

perf(lsp): session parse cache + file watchers for cross-file features - #14

Merged
klaidliadon merged 7 commits into
masterfrom
perf/lsp-parse-cache
Jun 21, 2026
Merged

klaidliadon merged 7 commits into
masterfrom
perf/lsp-parse-cache

Conversation

@klaidliadon

Copy link
Copy Markdown
Collaborator

PR1 made codeLens parse the workspace once per request. But every other cross-file feature — hover, go-to-definition, find-references, rename, document-highlight, workspace-symbols, and import diagnostics — still re-parsed every closed .ridl file (and its whole import graph, twice) on each request. This adds a session-level parse cache so those closed-file parses are reused across requests, invalidated precisely when the workspace actually changes.


How it works

  • Workspace generation counter. A monotonic gen (atomic.Uint64) on the server is bumped — atomically with the document-store mutation, under a single workspaceMu.Lock — on every content-affecting event: didOpen/didChange/didClose and .ridl didChangeWatchedFiles. Not on didSave (no content change) or setResult.
  • Session parse cache. A mutex-guarded map[path]*ParseResult keyed by the current generation. Any gen change drops it wholesale (a closed file's parse depends on its entire import graph's overlay+disk state, so per-file dependency tracking isn't worth the complexity). Readers snapshot (docs, gen, overlays, cache-hit) under workspaceMu.RLock and release before parsing — the lock is never held across a parse or a WalkDir.
  • File watchers. Initialized registers **/*.ridl watchers via client/registerCapability (this also revives the previously dead didChangeWatchedFiles handler). The cache is enabled only if registration succeeds; otherwise it's bypassed entirely and behavior is exactly as before (every parse re-reads disk). This is the correctness boundary: without watcher events we can't see closed-file disk edits, so we don't cache.
  • Candidate-path cache. referenceCandidatePaths()'s full WalkDir is cached by the same generation (same snapshot discipline).
  • Import diagnostics now route through the cached parsePath instead of a direct re-parse.

Correctness guards: parsePath never caches a cancelled or incomplete parse (ctx.Err() checked on entry and after parse — the upstream parser can return err==nil with a partial result after cancellation) and stores only if the generation didn't move during the parse (TOCTOU). Cached *ParseResult (Root and Schema) is treated read-only. CodeLens (PR1) is untouched.

This is PR2 of two; PR1 (#13) is merged.

Test plan

$ make test
ok  (156 tests across internal/lsp + internal/ridl + cmd)

$ go test -race ./internal/lsp/ ./internal/ridl/
ok   (no races)   # incl. TestCacheConcurrentAccess: 16 readers × 2 gen-churners

$ make lint
0 issues.

New tests: parse-cache gen semantics (hit/miss/roll-forward/stale-put/nil-skip); generation bumps on open/change/close/.ridl-watched-files and not on save; watcher registration + cache-enable gating (advertised+success / not-advertised / register-fails); closed-file cache hit + reparse-after-gen-bump + no-cache-on-cancel + disabled-bypass; candidate-path caching + invalidation; and a concurrency race test that (verified by coverage) actually exercises the session cache under gen churn.

Review notes

Design converged with Codex up front (the watcher-gating fallback, the atomic mutation boundary, and the no-cache-on-cancel rule all came out of that round), then the diff went through a Claude per-task + opus core-task review and a Codex adversarial diff review. Codex's adversarial pass caught that the concurrency test was vacuous for the session cache (cursor on a non-symbol → the cache was never reached; parseCache.get/put at 0% coverage) — fixed by targeting a real cross-file reference, now 100%/80% covered.

Non-blocking follow-ups: candidatePathCache.get could use RLock (matches parseCache's plain Mutex for now); per-file dependency-graph invalidation was intentionally skipped in favor of whole-generation drop.

@klaidliadon
klaidliadon merged commit e5dd675 into master Jun 21, 2026
1 check passed
@klaidliadon
klaidliadon deleted the perf/lsp-parse-cache branch June 21, 2026 07:40
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