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);