Skip to content

perf(diagnostics): share analyzer results across document requests - #404

Open
alsi-lawr wants to merge 2 commits into
razzmatazz:mainfrom
alsi-lawr:perf/shared-analyzer-diagnostics-a4-002
Open

perf(diagnostics): share analyzer results across document requests#404
alsi-lawr wants to merge 2 commits into
razzmatazz:mainfrom
alsi-lawr:perf/shared-analyzer-diagnostics-a4-002

Conversation

@alsi-lawr

Copy link
Copy Markdown
Contributor

Fixes #403.

Diagnosis

Document diagnostics currently create a new CompilationWithAnalyzers and run GetAllDiagnosticsAsync for every requested document. The result covers the whole project, but each request keeps only diagnostics for its own syntax tree. Pulling several documents from one unchanged solution snapshot therefore repeats the same project-wide analyzer work.

Change

Cache one lazy analyzer task per project and immutable Roslyn Solution snapshot. Document and compilation diagnostics share that result. A new solution snapshot naturally gets a new cache, and the weak solution key allows old snapshots to be collected.

Request cancellation only cancels that request's wait. It does not cancel analyzer work already shared with another document request.

The production change is 43 added lines and 18 removed lines in Roslyn/Analyzers.fs.

User impact

Diagnostic payloads and update behavior do not change. The first analyzer request for a solution snapshot still performs the full analysis. Later document requests for the same snapshot reuse it. Editing any document creates a new snapshot, so the next request analyzes the updated project before reuse begins again.

A real-LSP four-file evaluation covered same-file edits that clear and add IDE diagnostics, saves, unrelated document pulls, cross-file edits that add and clear CS0103, workspace pulls, and push diagnostics. Normalized diagnostic payloads matched the baseline in all three fresh sessions.

Benchmark

tests/benchmarks/AnalyzerDiagnostics.fsx starts the real LSP, enables analyzers, warms one document, then pulls diagnostics for 12 other documents in the same project snapshot. It prints the actual request times and diagnostic counts.

nix develop -c dotnet build tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj
nix develop -c dotnet fsi tests/benchmarks/AnalyzerDiagnostics.fsx

Upstream main (e2efc47), representative run:

request times (ms): [|47L; 42L; 43L; 72L; 44L; 41L; 42L; 42L; 40L; 63L; 42L; 38L|]
diagnostics per request: [|6; 6; 6; 6; 6; 6; 6; 6; 6; 6; 6; 6|]
wall time: 563 ms
server CPU time: 2150 ms

This branch (c2002e0), representative run:

request times (ms): [|4L; 2L; 2L; 2L; 2L; 2L; 2L; 2L; 2L; 2L; 2L; 2L|]
diagnostics per request: [|6; 6; 6; 6; 6; 6; 6; 6; 6; 6; 6; 6|]
wall time: 27 ms
server CPU time: 40 ms

Across three fresh server processes, the median wall time fell from 572 ms to 27 ms. Median server CPU time fell from 2,150 ms to 30 ms. Every sample returned the same 72 diagnostics.

Verification

Passed: 293, Failed: 0, Skipped: 0

The analyzer cache regression test also covers concurrent document requests, request cancellation isolation, completed-result reuse, and invalidation after a document edit.


type private SolutionAnalysisCache = ConcurrentDictionary<ProjectId, Lazy<Task<ImmutableArray<Diagnostic>>>>

let private solutionAnalysisCaches =

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a fan of this global cache. can we add this to wf structure? unsure how state transfers/caching/locking would work though

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a fan of this global cache. can we add this to wf structure? unsure how state transfers/caching/locking would work though

What exactly do you mean by "add this to wf"? Do you mean:

  • LspWorkspaceFolder holds the cache, which is then plugged into the WorkspaceFolder life-cycle by being created at solution load, and dropped in teardown? This would be a big improvement, I agree.
  • The cache itself is objectionable and we should just thread the record data through LspWorkspaceFolderUpdateFn? This would gut the PR, because it'll no longer cache the in-flight Task properly. The UpdateFns apply only after the handlers are evaluated, so the burst of diagnostics will always just see an empty set of diagnostics and re-request them all again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went ahead with option 1 there since Option 2 should just close the PR/Issue as rejected

Move the shared analyzer cache from a module-level global into
LspWorkspaceFolder so it is reachable from ServerState and released by
workspaceFolderTeardown rather than when the GC clears the weak keys.

Locking, invalidation, and retention are unchanged: the
ConditionalWeakTable moves intact into AnalyzerDiagnosticsCache, still
keyed by immutable Solution snapshot.
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.

Document diagnostics rerun whole-project analyzers for every document

2 participants