Skip to content

Conversation

CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Aug 20, 2025

The core issue hee is that we were caching the CompilationWithAnalyzers for a compilation with all analyzers. Then, when being asked to compute again with, say, a single analyzer, we'd reuse the CompialtionWithAnalyzers.

This would then take ages to run all analyzers, even when running a fix-all for a single analyzer.

ProjectState,
ConcurrentDictionary<
(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers),
AsyncLazy<CompilationWithAnalyzersPair?>>> s_projectToCompilationWithAnalyzers = new();

private static async Task<CompilationWithAnalyzersPair?> GetOrCreateCompilationWithAnalyzersAsync(
Copy link
Member Author

Choose a reason for hiding this comment

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

note: i dont' like this design at all. it means on the client side we end up makign the compilatino, then create the compilationWithAnalyzersPair only to eventually call over to oop with the set of analyzers we want, where it does the same thing over ther eas well.

we should make the call to oop much higher in stack.

ProjectState,
ConcurrentDictionary<
(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers),
AsyncLazy<CompilationWithAnalyzersPair?>>> s_projectToCompilationWithAnalyzers = new();
Copy link
Member Author

Choose a reason for hiding this comment

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

NOte: i designed it this way so that we dont' toss out a very good 'full compilation with analyzers' and replace it with a 'compilation with analyzers with a single analyzer in it'.

@CyrusNajmabadi
Copy link
Member Author

Note: from talking to @mavasani it sounds like he thinks the issue is that FixAll is going through these paths at all, and reusing a CompilationWithAnalyzers that was created for the full set of analyzers.

I don't disagree that we could fix things by having FixAll not go through that path. However, i find the reasoning a bit suspec here.

SPecifically, the code i'm modifying specifically and clearly was stating that if someone called in with a subset of analyzers, that the existing cached CompilationWithAnalyzers could be reused. However, what scenarios exist where that happens? Normal diagnostics will use teh same set of analyzers when calling into the compilation. So the only subset case would be something like fix-all, which ends up just using a single analyzer. Given that, it seems like the intention here strongly was to reuse the CompWithAnalyzers. But that ends up just being incredibly broken given that it runs everything.

--

To me, the fix in this pr is fairly reasonable. It simply says that the same Comp+Analyzers gives you back the same CompWithAnalyzers instance. And if you have a different set of Analyzers, you get a different instance. That seems entirely fine (esp. since the alternative proposed fix would create a new CompWithAnalyzers instance as well).

It is the case that in this PR we do cache that in the CWT. HOwever, as the CWT is keyed by ProjectState, it will drop away very shortly once the FixAll completes.

--

Note: the diag subsystem still needs many waves of cleanup. It continues to be massively complex and difficult ot reason about. APIs are sprawling and complex. And it's very unclear what any particular layer should call. It's then trivial to not call the right thing, and end up not passing along all the information that is needed to do things in the right way.

@mavasani
Copy link
Contributor

Functionally, your PR seems fine to me, my only concern would be a perf regression where earlier if we computed full document diagnostics for all analyzers using a CompWithAnalyzers instance, and later a call comes in to get diagnostics for a single analyzer for that document, prior to this PR we would have reused the same CompWithAnalyzers instance and fetched cached diagnostics for that analyzer from prior computation, but now we will create a new CompWithAnalyzers and add binding + analyzer execution cost for the second request. Theoretically, we need a new CompWithAnalyzers instance only if there is at least one analyzer in the new request that is not part of the analyzers passed into the constructor of the original CompWithAnayzers instance.

IMO, FixAll was earlier using a separate code path to compute whole compilation diagnostics for a single analyzer. It is fine to use this code path now but instead of requesting all analyzer diagnostics on entire compilation for this CompilationWithAnalyzers instance, we should request diagnostics for only that analyzer for the compilation.

i.e. currently we are invoking the below API on CompilationWithAnalyzers for FixAll path:

public async Task<AnalysisResult> GetAnalysisResultAsync(CancellationToken cancellationToken)

Instead we should be invoking the below API where the single analyzer corresponding to FixAll diagnostic ID is passed as the argument:

public async Task<AnalysisResult> GetAnalysisResultAsync(ImmutableArray<DiagnosticAnalyzer> analyzers, CancellationToken cancellationToken)

@CyrusNajmabadi
Copy link
Member Author

So I can try that. But it will def be a bunch of work to thread that information from where we are, all the way down to this call.

Let me start with this as it is still much better to just have a fresh CompilationWithAnalyzers, just with the same compilation and a different set of analyzers versus using the one with all analyzers (140+ here).

As you mentioned in our offline convo, you also thought the original design was that FixAll would have a fresh CompWithAnal anyways, so this at least aligns us with that.

I will continue trying to simplify and clean up things here.

In particular, I think the OOP boundary is too low level. We should likely push it all the way up to the top of the IDiagService, remote immediately, and then do all the work on the remote side.

Right now, we're creating lots of things (like CompWithAnal) on the host side, only to not use at all since the computation happens on oop.

@CyrusNajmabadi CyrusNajmabadi merged commit 4e77260 into dotnet:main Aug 20, 2025
25 checks passed
@CyrusNajmabadi CyrusNajmabadi deleted the cacheCompilationForSameAnalyzers branch August 20, 2025 15:21
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Aug 20, 2025
JoeRobich added a commit to dotnet/roslyn-tools that referenced this pull request Aug 29, 2025
46 PRs before and 36 after
```diff
+    * [Cache diagnostic analyzer computation](dotnet/roslyn#80045)
+    * [Remove parameter always passed the same value](dotnet/roslyn#80042)
     * [Update doc for IMethodSymbol.IsExtensionMethod](dotnet/roslyn#80016)
     * [Don't cache known-broken compositions](dotnet/roslyn#80021)
     * [Cleanup methods in DiagAnalyzerService](dotnet/roslyn#80013)
     * [Simplify processing of errors reported by the build](dotnet/roslyn#79964)
     * [Additional cleanup of the DiagnosticAnalyzerServier](dotnet/roslyn#80005)
     * [Fix Code Lens around source generated files](dotnet/roslyn#79992)
     * [Remove superflous DiagService api that can be achieved with existing apis](dotnet/roslyn#80007)
     * [Generate `init` accessor for required properties inside `readonly struct`s](dotnet/roslyn#80004)
     * [Remove existing low level diag oop code now that it's all handled at higher levels.](dotnet/roslyn#79994)
     * [Allow large InlineHint ArrayBuilder pooling](dotnet/roslyn#79857)
     * [Reduce allocations obtaining classified spans in ClassifierHelper](dotnet/roslyn#79856)
     * [Compute span diagnostics in oop](dotnet/roslyn#79991)
     * [Allow Razor cohosting to work with non-Razor SDK projects](dotnet/roslyn#79953)
     * [Move computation of deprioritized analyzers to oop](dotnet/roslyn#79989)
     * [EnC: Fix symbol mapping of delegates with indexed name](dotnet/roslyn#79837)
     * [Emit telemetry 'durations' with known radix point '.'](dotnet/roslyn#79988)
     * [Move logic up into DiagService](dotnet/roslyn#79985)
     * [Move the StateManager type up to the DiagnosticService from the DiagnosticIncrementalANalyzer](dotnet/roslyn#79984)
     * [Immediately remote diagnostics call to OOP](dotnet/roslyn#79983)
     * [Build Microsoft.CodeAnalysis.SemanticSearch.Extension ref assembly for use in semantic search queries](dotnet/roslyn#79972)
     * [Only cache compilation if we have the same set of analyzers](dotnet/roslyn#79978)
     * [Delete unused property](dotnet/roslyn#79963)
     * [Update 'use expr body' to be a purely syntactic analyzer](dotnet/roslyn#79979)
     * [Mark 'Use expr body' as a syntax-only fixer](dotnet/roslyn#79971)
     * [♻️ MSBuildWorkspaceDirectory - Fallback to AppContext.BaseDirectory when Assembly Location is empty](dotnet/roslyn#79934)
     * [Merge runtime async support into main](dotnet/roslyn#79833)
     * [Implement "Simplify property accessor" feature](dotnet/roslyn#79754)
-    * Merge main to runtime async branch (PR: [#79961](dotnet/roslyn#79961))
     * [Redo how and when we report source generator telemetry](dotnet/roslyn#79951)
     * [Allow MEF components to supply assembly path resolvers](dotnet/roslyn#79218)
     * [Allow Razor to hook up the source generator in misc files](dotnet/roslyn#79891)
     * [Block ENC for extension blocks](dotnet/roslyn#79883)
     * [Upgrade servicehub.client to fix test source discovery](dotnet/roslyn#79899)
     * [Update package restore error message.](dotnet/roslyn#79876)
-    * Merge main (PR: [#79834](dotnet/roslyn#79834))
-    * Merge main (PR: [#79830](dotnet/roslyn#79830))
     * [Baseline struct lifting tests](dotnet/roslyn#79505)
-    * Merge main to runtime async branch (PR: [#79582](dotnet/roslyn#79582))
-    * Merge main (PR: [#79424](dotnet/roslyn#79424))
-    * Merge main (PR: [#78994](dotnet/roslyn#78994))
-    * Merge main to runtime async branch (PR: [#78740](dotnet/roslyn#78740))
-    * Merge main to runtime async branch (PR: [#78517](dotnet/roslyn#78517))
-    * Merge main to runtime async branch (PR: [#78114](dotnet/roslyn#78114))
-    * Merge main to runtime async branch (PR: [#77700](dotnet/roslyn#77700))
-    * Merge main to runtime async branch (PR: [#77533](dotnet/roslyn#77533))
-    * Merge main to runtime async branch (PR: [#77265](dotnet/roslyn#77265))
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants