diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor_Helpers.cs b/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor_Helpers.cs index 4748effdadb07..7de75d8b668ab 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor_Helpers.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor_Helpers.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.SolutionCrawler; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -127,84 +128,6 @@ static string GetLanguageSpecificId(string? language, string noLanguageId, strin language: language); } - public static async Task CreateCompilationWithAnalyzersAsync( - Project project, - ImmutableArray projectAnalyzers, - ImmutableArray hostAnalyzers, - bool crashOnAnalyzerException, - CancellationToken cancellationToken) - { - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - if (compilation == null) - { - // project doesn't support compilation - return null; - } - - // Create driver that holds onto compilation and associated analyzers - var filteredProjectAnalyzers = projectAnalyzers.WhereAsArray(static a => !a.IsWorkspaceDiagnosticAnalyzer()); - var filteredHostAnalyzers = hostAnalyzers.WhereAsArray(static a => !a.IsWorkspaceDiagnosticAnalyzer()); - var filteredProjectSuppressors = filteredProjectAnalyzers.WhereAsArray(static a => a is DiagnosticSuppressor); - filteredHostAnalyzers = filteredHostAnalyzers.AddRange(filteredProjectSuppressors); - - // PERF: there is no analyzers for this compilation. - // compilationWithAnalyzer will throw if it is created with no analyzers which is perf optimization. - if (filteredProjectAnalyzers.IsEmpty && filteredHostAnalyzers.IsEmpty) - { - return null; - } - - Contract.ThrowIfFalse(project.SupportsCompilation); - AssertCompilation(project, compilation); - - // in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to - // async being used with synchronous blocking concurrency. - var projectAnalyzerOptions = new CompilationWithAnalyzersOptions( - options: project.AnalyzerOptions, - onAnalyzerException: null, - analyzerExceptionFilter: GetAnalyzerExceptionFilter(), - concurrentAnalysis: false, - logAnalyzerExecutionTime: true, - reportSuppressedDiagnostics: true); - var hostAnalyzerOptions = new CompilationWithAnalyzersOptions( - options: project.HostAnalyzerOptions, - onAnalyzerException: null, - analyzerExceptionFilter: GetAnalyzerExceptionFilter(), - concurrentAnalysis: false, - logAnalyzerExecutionTime: true, - reportSuppressedDiagnostics: true); - - // Create driver that holds onto compilation and associated analyzers - return new CompilationWithAnalyzersPair( - filteredProjectAnalyzers.Any() ? compilation.WithAnalyzers(filteredProjectAnalyzers, projectAnalyzerOptions) : null, - filteredHostAnalyzers.Any() ? compilation.WithAnalyzers(filteredHostAnalyzers, hostAnalyzerOptions) : null); - - Func GetAnalyzerExceptionFilter() - { - return ex => - { - if (ex is not OperationCanceledException && crashOnAnalyzerException) - { - // report telemetry - FatalError.ReportAndPropagate(ex); - - // force fail fast (the host might not crash when reporting telemetry): - FailFast.OnFatalException(ex); - } - - return true; - }; - } - } - - [Conditional("DEBUG")] - private static void AssertCompilation(Project project, Compilation compilation1) - { - // given compilation must be from given project. - Contract.ThrowIfFalse(project.TryGetCompilation(out var compilation2)); - Contract.ThrowIfFalse(compilation1 == compilation2); - } - /// /// Return true if the given is not suppressed for the given project. /// NOTE: This API is intended to be used only for performance optimization. diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs index b13fd1018622e..0795b3454a313 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs @@ -2,22 +2,130 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics; internal partial class DiagnosticAnalyzerService { - private partial class DiagnosticIncrementalAnalyzer + /// + /// Cached data from a to the last instance created + /// for it. Note: the CompilationWithAnalyzersPair instance is dependent on the set of s + /// passed along with the project. As such, we might not be able to use a prior cached value if the set of state + /// sets changes. In that case, a new instance will be created and will be cached for the next caller. + /// + private static readonly ConditionalWeakTable stateSets, CompilationWithAnalyzersPair? compilationWithAnalyzersPair)>> s_projectToCompilationWithAnalyzers = new(); + + private static async Task GetOrCreateCompilationWithAnalyzersAsync( + Project project, + ImmutableArray stateSets, + bool crashOnAnalyzerException, + CancellationToken cancellationToken) + { + if (!project.SupportsCompilation) + return null; + + // Make sure the cached pair was computed with at least the same state sets we're asking about. if not, + // recompute and cache with the new state sets. + if (!s_projectToCompilationWithAnalyzers.TryGetValue(project, out var tupleBox) || + !stateSets.IsSubsetOf(tupleBox.Value.stateSets)) + { + var compilationWithAnalyzersPair = await CreateCompilationWithAnalyzersAsync().ConfigureAwait(false); + tupleBox = new((stateSets, compilationWithAnalyzersPair)); + + // Make a best effort attempt to store the latest computed value against these state sets. If this + // fails (because another thread interleaves with this), that's ok. We still return the pair we + // computed, so our caller will still see the right data + s_projectToCompilationWithAnalyzers.Remove(project); + + // Intentionally ignore the result of this. We still want to use the value we computed above, even if + // another thread interleaves and sets a different value. + s_projectToCompilationWithAnalyzers.GetValue(project, _ => tupleBox); + } + + return tupleBox.Value.compilationWithAnalyzersPair; + + // + // Should only be called on a that . + // + async Task CreateCompilationWithAnalyzersAsync() + { + var projectAnalyzers = stateSets.SelectAsArray(s => !s.IsHostAnalyzer, s => s.Analyzer); + var hostAnalyzers = stateSets.SelectAsArray(s => s.IsHostAnalyzer, s => s.Analyzer); + + var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + + // Create driver that holds onto compilation and associated analyzers + var filteredProjectAnalyzers = projectAnalyzers.WhereAsArray(static a => !a.IsWorkspaceDiagnosticAnalyzer()); + var filteredHostAnalyzers = hostAnalyzers.WhereAsArray(static a => !a.IsWorkspaceDiagnosticAnalyzer()); + var filteredProjectSuppressors = filteredProjectAnalyzers.WhereAsArray(static a => a is DiagnosticSuppressor); + filteredHostAnalyzers = filteredHostAnalyzers.AddRange(filteredProjectSuppressors); + + // PERF: there is no analyzers for this compilation. + // compilationWithAnalyzer will throw if it is created with no analyzers which is perf optimization. + if (filteredProjectAnalyzers.IsEmpty && filteredHostAnalyzers.IsEmpty) + { + return null; + } + + Contract.ThrowIfFalse(project.SupportsCompilation); + AssertCompilation(project, compilation); + + // in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to + // async being used with synchronous blocking concurrency. + var projectAnalyzerOptions = new CompilationWithAnalyzersOptions( + options: project.AnalyzerOptions, + onAnalyzerException: null, + analyzerExceptionFilter: GetAnalyzerExceptionFilter(), + concurrentAnalysis: false, + logAnalyzerExecutionTime: true, + reportSuppressedDiagnostics: true); + var hostAnalyzerOptions = new CompilationWithAnalyzersOptions( + options: project.HostAnalyzerOptions, + onAnalyzerException: null, + analyzerExceptionFilter: GetAnalyzerExceptionFilter(), + concurrentAnalysis: false, + logAnalyzerExecutionTime: true, + reportSuppressedDiagnostics: true); + + // Create driver that holds onto compilation and associated analyzers + return new CompilationWithAnalyzersPair( + filteredProjectAnalyzers.Any() ? compilation.WithAnalyzers(filteredProjectAnalyzers, projectAnalyzerOptions) : null, + filteredHostAnalyzers.Any() ? compilation.WithAnalyzers(filteredHostAnalyzers, hostAnalyzerOptions) : null); + + Func GetAnalyzerExceptionFilter() + { + return ex => + { + if (ex is not OperationCanceledException && crashOnAnalyzerException) + { + // report telemetry + FatalError.ReportAndPropagate(ex); + + // force fail fast (the host might not crash when reporting telemetry): + FailFast.OnFatalException(ex); + } + + return true; + }; + } + } + } + + [Conditional("DEBUG")] + private static void AssertCompilation(Project project, Compilation compilation1) { - private static Task CreateCompilationWithAnalyzersAsync(Project project, ImmutableArray stateSets, bool crashOnAnalyzerException, CancellationToken cancellationToken) - => DocumentAnalysisExecutor.CreateCompilationWithAnalyzersAsync( - project, - stateSets.SelectAsArray(s => !s.IsHostAnalyzer, s => s.Analyzer), - stateSets.SelectAsArray(s => s.IsHostAnalyzer, s => s.Analyzer), - crashOnAnalyzerException, - cancellationToken); + // given compilation must be from given project. + Contract.ThrowIfFalse(project.TryGetCompilation(out var compilation2)); + Contract.ThrowIfFalse(compilation1 == compilation2); } } diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs index 1323696fd9d7c..af13df1bcf9c3 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs @@ -6,21 +6,18 @@ namespace Microsoft.CodeAnalysis.Diagnostics; internal partial class DiagnosticAnalyzerService { - private partial class DiagnosticIncrementalAnalyzer + /// + /// this contains all states regarding a + /// + private sealed class StateSet { - /// - /// this contains all states regarding a - /// - private sealed class StateSet - { - public readonly DiagnosticAnalyzer Analyzer; - public readonly bool IsHostAnalyzer; + public readonly DiagnosticAnalyzer Analyzer; + public readonly bool IsHostAnalyzer; - public StateSet(DiagnosticAnalyzer analyzer, bool isHostAnalyzer) - { - Analyzer = analyzer; - IsHostAnalyzer = isHostAnalyzer; - } + public StateSet(DiagnosticAnalyzer analyzer, bool isHostAnalyzer) + { + Analyzer = analyzer; + IsHostAnalyzer = isHostAnalyzer; } } } diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs index dc107a58ad2c0..3e2dacfc9a04e 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs @@ -149,7 +149,7 @@ async Task> Ge } // Otherwise, just compute for the state sets we care about. - var compilation = await CreateCompilationWithAnalyzersAsync(project, stateSets, Owner.AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); + var compilation = await GetOrCreateCompilationWithAnalyzersAsync(project, stateSets, Owner.AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); var result = await Owner.ComputeDiagnosticAnalysisResultsAsync(compilation, project, stateSets, cancellationToken).ConfigureAwait(false); return result; diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.ProjectAndCompilationWithAnalyzers.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.ProjectAndCompilationWithAnalyzers.cs deleted file mode 100644 index be9d14efc42f9..0000000000000 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.ProjectAndCompilationWithAnalyzers.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Microsoft.CodeAnalysis.Diagnostics; - -internal partial class DiagnosticAnalyzerService -{ - private partial class DiagnosticIncrementalAnalyzer - { - private sealed class ProjectAndCompilationWithAnalyzers - { - public Project Project { get; } - public CompilationWithAnalyzersPair? CompilationWithAnalyzers { get; } - - public ProjectAndCompilationWithAnalyzers(Project project, CompilationWithAnalyzersPair? compilationWithAnalyzers) - { - Project = project; - CompilationWithAnalyzers = compilationWithAnalyzers; - } - } - } -} diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs index ad5549f909833..1a92f8294f952 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs @@ -7,6 +7,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; @@ -47,11 +48,6 @@ public async Task> GetDiagnosticsForSpanAsync( /// private sealed class LatestDiagnosticsForSpanGetter { - // PERF: Cache the last Project and corresponding CompilationWithAnalyzers used to compute analyzer diagnostics for span. - // This is now required as async lightbulb will query and execute different priority buckets of analyzers with multiple - // calls, and we want to reuse CompilationWithAnalyzers instance if possible. - private static readonly WeakReference s_lastProjectAndCompilationWithAnalyzers = new(null); - private readonly DiagnosticIncrementalAnalyzer _owner; private readonly TextDocument _document; private readonly SourceText _text; @@ -104,44 +100,6 @@ public static async Task CreateAsync( range, priorityProvider, isExplicit, logPerformanceInfo, incrementalAnalysis, diagnosticKinds); } - private static async Task GetOrCreateCompilationWithAnalyzersAsync( - Project project, - ImmutableArray stateSets, - bool crashOnAnalyzerException, - CancellationToken cancellationToken) - { - if (s_lastProjectAndCompilationWithAnalyzers.TryGetTarget(out var projectAndCompilationWithAnalyzers) && - projectAndCompilationWithAnalyzers?.Project == project) - { - if (projectAndCompilationWithAnalyzers.CompilationWithAnalyzers == null) - { - return null; - } - - if (HasAllAnalyzers(stateSets, projectAndCompilationWithAnalyzers.CompilationWithAnalyzers)) - { - return projectAndCompilationWithAnalyzers.CompilationWithAnalyzers; - } - } - - var compilationWithAnalyzers = await CreateCompilationWithAnalyzersAsync(project, stateSets, crashOnAnalyzerException, cancellationToken).ConfigureAwait(false); - s_lastProjectAndCompilationWithAnalyzers.SetTarget(new ProjectAndCompilationWithAnalyzers(project, compilationWithAnalyzers)); - return compilationWithAnalyzers; - - static bool HasAllAnalyzers(IEnumerable stateSets, CompilationWithAnalyzersPair compilationWithAnalyzers) - { - foreach (var stateSet in stateSets) - { - if (stateSet.IsHostAnalyzer && !compilationWithAnalyzers.HostAnalyzers.Contains(stateSet.Analyzer)) - return false; - else if (!stateSet.IsHostAnalyzer && !compilationWithAnalyzers.ProjectAnalyzers.Contains(stateSet.Analyzer)) - return false; - } - - return true; - } - } - private LatestDiagnosticsForSpanGetter( DiagnosticIncrementalAnalyzer owner, CompilationWithAnalyzersPair? compilationWithAnalyzers, diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs index c8d7b5f15551c..7b95519eefe55 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -60,7 +60,7 @@ public async Task> ForceAnalyzeProjectAsync(Proje static (stateSet, arg) => arg.self.IsCandidateForFullSolutionAnalysis(stateSet.Analyzer, stateSet.IsHostAnalyzer, arg.project), (self: this, project)); - var compilationWithAnalyzers = await CreateCompilationWithAnalyzersAsync( + var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync( project, fullSolutionAnalysisStateSets, AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); var projectAnalysisData = await ComputeDiagnosticAnalysisResultsAsync(compilationWithAnalyzers, project, fullSolutionAnalysisStateSets, cancellationToken).ConfigureAwait(false);