Skip to content

Commit 4e77260

Browse files
Only cache compilation if we have the same set of analyzers (#79978)
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.
2 parents 528decf + 135e1b0 commit 4e77260

File tree

2 files changed

+74
-31
lines changed

2 files changed

+74
-31
lines changed

src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Collections.Generic;
67
using System.Collections.Immutable;
78
using System.Linq;
89
using System.Runtime.CompilerServices;
@@ -16,14 +17,54 @@ namespace Microsoft.CodeAnalysis.Diagnostics;
1617

1718
internal sealed partial class DiagnosticAnalyzerService
1819
{
20+
private sealed class ChecksumAndAnalyzersEqualityComparer
21+
: IEqualityComparer<(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers)>
22+
{
23+
public static readonly ChecksumAndAnalyzersEqualityComparer Instance = new();
24+
25+
public bool Equals((Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers) x, (Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers) y)
26+
{
27+
if (x.checksum != y.checksum)
28+
return false;
29+
30+
// Fast path for when the analyzers are the same reference.
31+
return x.analyzers == y.analyzers || x.analyzers.SetEquals(y.analyzers);
32+
}
33+
34+
public int GetHashCode((Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers) obj)
35+
{
36+
var hashCode = obj.checksum.GetHashCode();
37+
38+
// Use addition so that we're resilient to any order for the analyzers.
39+
foreach (var analyzer in obj.analyzers)
40+
hashCode += analyzer.GetHashCode();
41+
42+
return hashCode;
43+
}
44+
}
45+
46+
/// <summary>
47+
/// Cached data from a <see cref="ProjectState"/> to the <see cref="CompilationWithAnalyzersPair"/>s
48+
/// we've created for it. Note: the CompilationWithAnalyzersPair instance is dependent on the set of <see
49+
/// cref="DiagnosticAnalyzer"/>s passed along with the project.
50+
/// <para/>
51+
/// The value of the table is a SmallDictionary that maps from the
52+
/// <see cref="Project"/> checksum the set of <see cref="DiagnosticAnalyzer"/>s being requested.
53+
/// Note: this dictionary must be locked with <see cref="s_gate"/> before accessing it. A
54+
/// small dictionary is chosen as this will normally only have one item in it (the current project
55+
/// and all its analyzers). Occasionally it will have more, if (for example) a request to run
56+
/// a single analyzer is performed.
57+
/// </summary>
58+
private static readonly ConditionalWeakTable<
59+
ProjectState,
60+
SmallDictionary<
61+
(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers),
62+
AsyncLazy<CompilationWithAnalyzersPair?>>> s_projectToCompilationWithAnalyzers = new();
63+
1964
/// <summary>
20-
/// Cached data from a <see cref="ProjectState"/> to the last <see cref="CompilationWithAnalyzersPair"/> instance
21-
/// created for it. Note: the CompilationWithAnalyzersPair instance is dependent on the set of <see
22-
/// cref="DiagnosticAnalyzer"/>s passed along with the project. As such, we might not be able to use a prior cached
23-
/// value if the set of analyzers changes. In that case, a new instance will be created and will be cached for the
24-
/// next caller.
65+
/// Protection around the SmallDictionary in <see cref="s_projectToCompilationWithAnalyzers"/>.
2566
/// </summary>
26-
private static readonly ConditionalWeakTable<ProjectState, StrongBox<(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers, CompilationWithAnalyzersPair? compilationWithAnalyzersPair)>> s_projectToCompilationWithAnalyzers = new();
67+
private static readonly SemaphoreSlim s_gate = new(initialCount: 1);
2768

2869
private static async Task<CompilationWithAnalyzersPair?> GetOrCreateCompilationWithAnalyzersAsync(
2970
Project project,
@@ -35,41 +76,42 @@ internal sealed partial class DiagnosticAnalyzerService
3576
if (!project.SupportsCompilation)
3677
return null;
3778

38-
var projectState = project.State;
3979
var checksum = await project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false);
4080

41-
// Make sure the cached pair was computed with at least the same state sets we're asking about. if not,
81+
// Make sure the cached pair was computed with the same state sets we're asking about. if not,
4282
// recompute and cache with the new state sets.
43-
if (!s_projectToCompilationWithAnalyzers.TryGetValue(projectState, out var tupleBox) ||
44-
tupleBox.Value.checksum != checksum ||
45-
!analyzers.IsSubsetOf(tupleBox.Value.analyzers))
83+
var map = s_projectToCompilationWithAnalyzers.GetValue(
84+
project.State, static _ => new(ChecksumAndAnalyzersEqualityComparer.Instance));
85+
86+
AsyncLazy<CompilationWithAnalyzersPair?>? lazy;
87+
using (await s_gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
4688
{
47-
var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
48-
var compilationWithAnalyzersPair = CreateCompilationWithAnalyzers(projectState, compilation);
49-
tupleBox = new((checksum, analyzers, compilationWithAnalyzersPair));
50-
51-
#if NET
52-
s_projectToCompilationWithAnalyzers.AddOrUpdate(projectState, tupleBox);
53-
#else
54-
// Make a best effort attempt to store the latest computed value against these state sets. If this
55-
// fails (because another thread interleaves with this), that's ok. We still return the pair we
56-
// computed, so our caller will still see the right data
57-
s_projectToCompilationWithAnalyzers.Remove(projectState);
58-
59-
// Intentionally ignore the result of this. We still want to use the value we computed above, even if
60-
// another thread interleaves and sets a different value.
61-
s_projectToCompilationWithAnalyzers.GetValue(projectState, _ => tupleBox);
62-
#endif
89+
var checksumAndAnalyzers = (checksum, analyzers);
90+
if (!map.TryGetValue(checksumAndAnalyzers, out lazy))
91+
{
92+
lazy = AsyncLazy.Create(
93+
asynchronousComputeFunction: CreateCompilationWithAnalyzersAsync,
94+
arg: (project, analyzers, hostAnalyzerInfo, crashOnAnalyzerException));
95+
map.Add(checksumAndAnalyzers, lazy);
96+
}
6397
}
6498

65-
return tupleBox.Value.compilationWithAnalyzersPair;
99+
return await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
66100

67101
// <summary>
68102
// Should only be called on a <see cref="Project"/> that <see cref="Project.SupportsCompilation"/>.
69103
// </summary>
70-
CompilationWithAnalyzersPair? CreateCompilationWithAnalyzers(
71-
ProjectState project, Compilation compilation)
104+
static async Task<CompilationWithAnalyzersPair?> CreateCompilationWithAnalyzersAsync(
105+
(Project project,
106+
ImmutableArray<DiagnosticAnalyzer> analyzers,
107+
HostAnalyzerInfo hostAnalyzerInfo,
108+
bool crashOnAnalyzerException) tuple,
109+
CancellationToken cancellationToken)
72110
{
111+
var (project, analyzers, hostAnalyzerInfo, crashOnAnalyzerException) = tuple;
112+
113+
var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
114+
73115
var projectAnalyzers = analyzers.WhereAsArray(static (s, info) => !info.IsHostAnalyzer(s), hostAnalyzerInfo);
74116
var hostAnalyzers = analyzers.WhereAsArray(static (s, info) => info.IsHostAnalyzer(s), hostAnalyzerInfo);
75117

@@ -105,7 +147,7 @@ internal sealed partial class DiagnosticAnalyzerService
105147
var projectCompilation = !filteredProjectAnalyzers.Any()
106148
? null
107149
: compilation.WithAnalyzers(filteredProjectAnalyzers, new CompilationWithAnalyzersOptions(
108-
options: project.ProjectAnalyzerOptions,
150+
options: project.State.ProjectAnalyzerOptions,
109151
onAnalyzerException: null,
110152
analyzerExceptionFilter: exceptionFilter,
111153
concurrentAnalysis: false,

src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<Compile Include="$(MSBuildThisFileDirectory)..\..\..\..\Compilers\Core\Portable\InternalUtilities\SharedStopwatch.cs" Link="InternalUtilities\SharedStopwatch.cs" />
2424
<Compile Include="$(MSBuildThisFileDirectory)..\..\..\..\Compilers\Core\Portable\Syntax\SyntaxTreeExtensions.cs" Link="Syntax\SyntaxTreeExtensions.cs" />
2525
<Compile Include="$(MSBuildThisFileDirectory)..\..\..\..\Compilers\Core\Portable\Collections\BitVector.cs" Link="Collections\BitVector.cs" />
26+
<Compile Include="$(MSBuildThisFileDirectory)..\..\..\..\Compilers\Core\Portable\Collections\SmallDictionary.cs" Link="Collections\SmallDictionary.cs" />
2627
<Compile Include="$(MSBuildThisFileDirectory)..\..\..\..\Compilers\Core\Portable\Collections\IOrderedReadOnlySet.cs" Link="Collections\IOrderedReadOnlySet.cs" />
2728
<Compile Include="$(MSBuildThisFileDirectory)..\..\..\..\Compilers\Core\Portable\SourceGeneration\GeneratedCodeUtilities.cs" Link="SourceGeneration\GeneratedCodeUtilities.cs" />
2829
<Compile Include="$(MSBuildThisFileDirectory)..\..\..\..\Compilers\Core\Portable\SourceCodeKindExtensions.cs" Link="Utilities\Compiler\SourceCodeKindExtensions.cs" />

0 commit comments

Comments
 (0)