33// See the LICENSE file in the project root for more information.
44
55using System ;
6+ using System . Collections . Generic ;
67using System . Collections . Immutable ;
78using System . Linq ;
89using System . Runtime . CompilerServices ;
@@ -16,14 +17,54 @@ namespace Microsoft.CodeAnalysis.Diagnostics;
1617
1718internal 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 ,
0 commit comments