diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_DeprioritizationCandidates.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_DeprioritizationCandidates.cs new file mode 100644 index 0000000000000..3327c42192a28 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_DeprioritizationCandidates.cs @@ -0,0 +1,82 @@ +// 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. + +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal sealed partial class DiagnosticAnalyzerService +{ + /// + /// A cache from DiagnosticAnalyzer to whether or not it is a candidate for deprioritization when lightbulbs + /// compute diagnostics for a particular priority class. Note: as this caches data, it may technically be + /// inaccurate as things change in the system. For example, this is based on the registered actions made + /// by an analyzer. Hypothetically, such an analyzer might register different actions based on on things + /// like appearing in a different language's compilation, or a compilation with different references, etc. + /// We accept that this cache may be inaccurate in such scenarios as they are likely rare, and this only + /// serves as a simple heuristic to order analyzer execution. If wrong, it's not a major deal. + /// + private static readonly ConditionalWeakTable> s_analyzerToIsDeprioritizationCandidateMap = new(); + + private async Task> GetDeprioritizationCandidatesInProcessAsync( + Project project, ImmutableArray analyzers, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var builder); + + HostAnalyzerInfo? hostAnalyzerInfo = null; + CompilationWithAnalyzersPair? compilationWithAnalyzers = null; + + foreach (var analyzer in analyzers) + { + if (!s_analyzerToIsDeprioritizationCandidateMap.TryGetValue(analyzer, out var boxedBool)) + { + if (hostAnalyzerInfo is null) + { + hostAnalyzerInfo = GetOrCreateHostAnalyzerInfo(project); + compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync( + project, analyzers, hostAnalyzerInfo, this.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); + } + + boxedBool = new(await IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(analyzer).ConfigureAwait(false)); +#if NET + s_analyzerToIsDeprioritizationCandidateMap.TryAdd(analyzer, boxedBool); +#else + lock (s_analyzerToIsDeprioritizationCandidateMap) + { + if (!s_analyzerToIsDeprioritizationCandidateMap.TryGetValue(analyzer, out var existing)) + s_analyzerToIsDeprioritizationCandidateMap.Add(analyzer, boxedBool); + } +#endif + } + + if (boxedBool.Value) + builder.Add(analyzer); + } + + return builder.ToImmutableAndClear(); + + async Task IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(DiagnosticAnalyzer analyzer) + { + // We deprioritize SymbolStart/End and SemanticModel analyzers from 'Normal' to 'Low' priority bucket, + // as these are computationally more expensive. + // Note that we never de-prioritize compiler analyzer, even though it registers a SemanticModel action. + if (compilationWithAnalyzers == null || + analyzer.IsWorkspaceDiagnosticAnalyzer() || + analyzer.IsCompilerAnalyzer()) + { + return false; + } + + var telemetryInfo = await compilationWithAnalyzers.GetAnalyzerTelemetryInfoAsync(analyzer, cancellationToken).ConfigureAwait(false); + if (telemetryInfo == null) + return false; + + return telemetryInfo.SymbolStartActionsCount > 0 || telemetryInfo.SemanticModelActionsCount > 0; + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_RemoteOrLocalDispatcher.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_RemoteOrLocalDispatcher.cs index cc549b40ae847..c9d7b5c3512b9 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_RemoteOrLocalDispatcher.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_RemoteOrLocalDispatcher.cs @@ -118,38 +118,7 @@ public async Task> GetDeprioritizationCandida return analyzers.FilterAnalyzers(result.Value); } - using var _ = ArrayBuilder.GetInstance(out var builder); - - var hostAnalyzerInfo = GetOrCreateHostAnalyzerInfo(project); - var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync( - project, analyzers, hostAnalyzerInfo, this.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); - - foreach (var analyzer in analyzers) - { - if (await IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(analyzer).ConfigureAwait(false)) - builder.Add(analyzer); - } - - return builder.ToImmutableAndClear(); - - async Task IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(DiagnosticAnalyzer analyzer) - { - // We deprioritize SymbolStart/End and SemanticModel analyzers from 'Normal' to 'Low' priority bucket, - // as these are computationally more expensive. - // Note that we never de-prioritize compiler analyzer, even though it registers a SemanticModel action. - if (compilationWithAnalyzers == null || - analyzer.IsWorkspaceDiagnosticAnalyzer() || - analyzer.IsCompilerAnalyzer()) - { - return false; - } - - var telemetryInfo = await compilationWithAnalyzers.GetAnalyzerTelemetryInfoAsync(analyzer, cancellationToken).ConfigureAwait(false); - if (telemetryInfo == null) - return false; - - return telemetryInfo.SymbolStartActionsCount > 0 || telemetryInfo.SemanticModelActionsCount > 0; - } + return await GetDeprioritizationCandidatesInProcessAsync(project, analyzers, cancellationToken).ConfigureAwait(false); } internal async Task> ProduceProjectDiagnosticsAsync( diff --git a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs index 125df37ef758e..38009e14e6642 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs @@ -223,7 +223,8 @@ public ValueTask> ComputeDiagnosticsAsync( solutionChecksum, async solution => { - var document = solution.GetRequiredTextDocument(documentId); + var document = await solution.GetRequiredTextDocumentAsync( + documentId, cancellationToken).ConfigureAwait(false); var service = (DiagnosticAnalyzerService)solution.Services.GetRequiredService(); var allProjectAnalyzers = service.GetProjectAnalyzers(document.Project); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/ISolutionExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/ISolutionExtensions.cs index 6b0ae9b15bfe1..322c15e27c206 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/ISolutionExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/ISolutionExtensions.cs @@ -104,7 +104,18 @@ public static TextDocument GetRequiredAnalyzerConfigDocument(this Solution solut => solution.GetAnalyzerConfigDocument(documentId) ?? throw CreateDocumentNotFoundException(); public static TextDocument GetRequiredTextDocument(this Solution solution, DocumentId documentId) - => solution.GetTextDocument(documentId) ?? throw CreateDocumentNotFoundException(); + { + var document = solution.GetTextDocument(documentId); + if (document != null) + return document; + +#if WORKSPACE + if (documentId.IsSourceGenerated) + throw new InvalidOperationException($"Use {nameof(GetRequiredTextDocumentAsync)} to get the {nameof(TextDocument)} for a `.{nameof(DocumentId.IsSourceGenerated)}=true` {nameof(DocumentId)}"); +#endif + + throw CreateDocumentNotFoundException(); + } private static Exception CreateDocumentNotFoundException() => new InvalidOperationException(WorkspaceExtensionsResources.The_solution_does_not_contain_the_specified_document);