diff --git a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs
index efeaa71523050..442357b8d10b2 100644
--- a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs
+++ b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs
@@ -25,6 +25,10 @@ internal interface IDiagnosticAnalyzerService : IWorkspaceService
///
Task> ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken);
+ ///
+ Task> GetDeprioritizationCandidatesAsync(
+ Project project, ImmutableArray analyzers, CancellationToken cancellationToken);
+
///
/// Get diagnostics of the given diagnostic ids and/or analyzers from the given solution. all diagnostics returned
/// should be up-to-date with respect to the given solution. Note that for project case, this method returns
diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs
index 510662171a7a9..c7068a7efadf4 100644
--- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs
+++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs
@@ -9,6 +9,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Reflection.Metadata;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
@@ -16,6 +17,7 @@
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
+using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SolutionCrawler;
@@ -176,22 +178,47 @@ public async Task> GetProjectDiagnosticsForIdsAsy
cancellationToken).ConfigureAwait(false);
}
- private Task> ProduceProjectDiagnosticsAsync(
+ internal async Task> ProduceProjectDiagnosticsAsync(
Project project,
ImmutableArray analyzers,
ImmutableHashSet? diagnosticIds,
- IReadOnlyList documentIds,
+ ImmutableArray documentIds,
bool includeLocalDocumentDiagnostics,
bool includeNonLocalDocumentDiagnostics,
bool includeProjectNonLocalResult,
CancellationToken cancellationToken)
{
- return _incrementalAnalyzer.ProduceProjectDiagnosticsAsync(
+ var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false);
+ if (client is not null)
+ {
+ var analyzerIds = analyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet();
+ var result = await client.TryInvokeAsync>(
+ project,
+ (service, solution, cancellationToken) => service.ProduceProjectDiagnosticsAsync(
+ solution, project.Id, analyzerIds, diagnosticIds, documentIds,
+ includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, includeProjectNonLocalResult,
+ cancellationToken),
+ cancellationToken).ConfigureAwait(false);
+ if (!result.HasValue)
+ return [];
+
+ return result.Value;
+ }
+
+ // Fallback to proccessing in proc.
+ return await _incrementalAnalyzer.ProduceProjectDiagnosticsAsync(
project, analyzers, diagnosticIds, documentIds,
includeLocalDocumentDiagnostics,
includeNonLocalDocumentDiagnostics,
includeProjectNonLocalResult,
- cancellationToken);
+ cancellationToken).ConfigureAwait(false);
+ }
+
+ internal Task> GetProjectAnalyzersAsync(
+ Project project, CancellationToken cancellationToken)
+ {
+ return _stateManager.GetOrCreateAnalyzersAsync(
+ project.Solution.SolutionState, project.State, cancellationToken);
}
private async Task> GetDiagnosticAnalyzersAsync(
@@ -200,9 +227,7 @@ private async Task> GetDiagnosticAnalyzersAsy
Func? shouldIncludeAnalyzer,
CancellationToken cancellationToken)
{
- var analyzersForProject = await _stateManager.GetOrCreateAnalyzersAsync(
- project.Solution.SolutionState, project.State, cancellationToken).ConfigureAwait(false);
-
+ var analyzersForProject = await GetProjectAnalyzersAsync(project, cancellationToken).ConfigureAwait(false);
var analyzers = analyzersForProject.WhereAsArray(a => ShouldIncludeAnalyzer(project, a));
return analyzers;
@@ -355,6 +380,59 @@ public async Task> GetDeprioritizationCandidatesAsync(
+ Project project, ImmutableArray analyzers, CancellationToken cancellationToken)
+ {
+ var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false);
+ if (client is not null)
+ {
+ var analyzerIds = analyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet();
+ var result = await client.TryInvokeAsync>(
+ project,
+ (service, solution, cancellationToken) => service.GetDeprioritizationCandidatesAsync(
+ solution, project.Id, analyzerIds, cancellationToken),
+ cancellationToken).ConfigureAwait(false);
+ if (!result.HasValue)
+ return [];
+
+ return analyzers.FilterAnalyzers(result.Value);
+ }
+
+ using var _ = ArrayBuilder.GetInstance(out var builder);
+
+ var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(
+ project.Solution.SolutionState, project.State, cancellationToken).ConfigureAwait(false);
+ 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;
+ }
+ }
+
private sealed class DiagnosticAnalyzerComparer : IEqualityComparer
{
public static readonly DiagnosticAnalyzerComparer Instance = new();
diff --git a/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs b/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs
index 610a35c51de6e..a92029adb1f53 100644
--- a/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs
+++ b/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs
@@ -22,7 +22,7 @@ public async Task> ProduceProjectDiagnosticsAsync
Project project,
ImmutableArray analyzers,
ImmutableHashSet? diagnosticIds,
- IReadOnlyList documentIds,
+ ImmutableArray documentIds,
bool includeLocalDocumentDiagnostics,
bool includeNonLocalDocumentDiagnostics,
bool includeProjectNonLocalResult,
diff --git a/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs b/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs
index dd42ee832573a..569477bcc3789 100644
--- a/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs
+++ b/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
@@ -203,7 +204,12 @@ async Task ComputeDocumentDiagnosticsAsync(
Debug.Assert(!incrementalAnalysis || kind == AnalysisKind.Semantic);
Debug.Assert(!incrementalAnalysis || analyzers.All(analyzer => analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis()));
- using var _ = ArrayBuilder.GetInstance(analyzers.Length, out var filteredAnalyzers);
+ using var _1 = ArrayBuilder.GetInstance(analyzers.Length, out var filteredAnalyzers);
+ using var _2 = PooledHashSet.GetInstance(out var deprioritizationCandidates);
+
+ deprioritizationCandidates.AddRange(await this.AnalyzerService.GetDeprioritizationCandidatesAsync(
+ project, analyzers, cancellationToken).ConfigureAwait(false));
+
foreach (var analyzer in analyzers)
{
Debug.Assert(priorityProvider.MatchesPriority(analyzer));
@@ -211,10 +217,8 @@ async Task ComputeDocumentDiagnosticsAsync(
// Check if this is an expensive analyzer that needs to be de-prioritized to a lower priority bucket.
// If so, we skip this analyzer from execution in the current priority bucket.
// We will subsequently execute this analyzer in the lower priority bucket.
- if (await TryDeprioritizeAnalyzerAsync(analyzer, kind, span).ConfigureAwait(false))
- {
+ if (TryDeprioritizeAnalyzer(analyzer, kind, span, deprioritizationCandidates))
continue;
- }
filteredAnalyzers.Add(analyzer);
}
@@ -224,8 +228,6 @@ async Task ComputeDocumentDiagnosticsAsync(
analyzers = filteredAnalyzers.ToImmutable();
- var hostAnalyzerInfo = await StateManager.GetOrCreateHostAnalyzerInfoAsync(solutionState, project.State, cancellationToken).ConfigureAwait(false);
-
var projectAnalyzers = analyzers.WhereAsArray(static (a, info) => !info.IsHostAnalyzer(a), hostAnalyzerInfo);
var hostAnalyzers = analyzers.WhereAsArray(static (a, info) => info.IsHostAnalyzer(a), hostAnalyzerInfo);
var analysisScope = new DocumentAnalysisScope(document, span, projectAnalyzers, hostAnalyzers, kind);
@@ -235,7 +237,7 @@ async Task ComputeDocumentDiagnosticsAsync(
ImmutableDictionary> diagnosticsMap;
if (incrementalAnalysis)
{
- using var _2 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}.Incremental");
+ using var _3 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}.Incremental");
diagnosticsMap = await _incrementalMemberEditAnalyzer.ComputeDiagnosticsAsync(
executor,
@@ -245,7 +247,7 @@ async Task ComputeDocumentDiagnosticsAsync(
}
else
{
- using var _2 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}.Document");
+ using var _3 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}.Document");
diagnosticsMap = await ComputeDocumentDiagnosticsCoreAsync(executor, cancellationToken).ConfigureAwait(false);
}
@@ -260,8 +262,9 @@ async Task ComputeDocumentDiagnosticsAsync(
_incrementalMemberEditAnalyzer.UpdateDocumentWithCachedDiagnostics((Document)document);
}
- async Task TryDeprioritizeAnalyzerAsync(
- DiagnosticAnalyzer analyzer, AnalysisKind kind, TextSpan? span)
+ bool TryDeprioritizeAnalyzer(
+ DiagnosticAnalyzer analyzer, AnalysisKind kind, TextSpan? span,
+ HashSet deprioritizationCandidates)
{
// PERF: In order to improve lightbulb performance, we perform de-prioritization optimization for certain analyzers
// that moves the analyzer to a lower priority bucket. However, to ensure that de-prioritization happens for very rare cases,
@@ -284,10 +287,8 @@ async Task TryDeprioritizeAnalyzerAsync(
// Condition 3.
// Check if this is a candidate analyzer that can be de-prioritized into a lower priority bucket based on registered actions.
- if (!await IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(analyzer).ConfigureAwait(false))
- {
+ if (!deprioritizationCandidates.Contains(analyzer))
return false;
- }
// 'LightbulbSkipExecutingDeprioritizedAnalyzers' option determines if we want to execute this analyzer
// in low priority bucket or skip it completely. If the option is not set, track the de-prioritized
@@ -301,31 +302,6 @@ async Task TryDeprioritizeAnalyzerAsync(
return true;
}
- // Returns true if this is an analyzer that is a candidate to be de-prioritized to
- // 'CodeActionRequestPriority.Low' priority for improvement in analyzer
- // execution performance for priority buckets above 'Low' priority.
- // Based on performance measurements, currently only analyzers which register SymbolStart/End actions
- // or SemanticModel actions are considered candidates to be de-prioritized. However, these semantics
- // could be changed in future based on performance measurements.
- 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;
- }
-
bool ShouldInclude(DiagnosticData diagnostic)
{
return diagnostic.DocumentId == document.Id &&
diff --git a/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs b/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs
index 129fbb242b633..31a212d3489d2 100644
--- a/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs
+++ b/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs
@@ -568,4 +568,20 @@ public static bool IsReportedInDocument(Diagnostic diagnostic, TextDocument targ
return false;
}
+
+ public static ImmutableArray FilterAnalyzers(
+ this ImmutableArray analyzers,
+ ImmutableHashSet analyzerIds)
+ {
+ using var _ = PooledDictionary.GetInstance(out var analyzerMap);
+ foreach (var analyzer in analyzers)
+ {
+ // In the case of multiple analyzers with the same ID, we keep the last one.
+ var analyzerId = analyzer.GetAnalyzerId();
+ if (analyzerIds.Contains(analyzerId))
+ analyzerMap[analyzerId] = analyzer;
+ }
+
+ return [.. analyzerMap.Values];
+ }
}
diff --git a/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs b/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs
index 2365b25d274a1..2a9840c22fef5 100644
--- a/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs
+++ b/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs
@@ -7,6 +7,7 @@
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CodeActions;
namespace Microsoft.CodeAnalysis.Diagnostics;
@@ -17,7 +18,27 @@ internal interface IRemoteDiagnosticAnalyzerService
///
ValueTask> ForceAnalyzeProjectAsync(Checksum solutionChecksum, ProjectId projectId, CancellationToken cancellationToken);
+ ///
+ /// Returns the analyzers that are candidates to be de-prioritized to
+ /// priority for improvement in analyzer
+ /// execution performance for priority buckets above 'Low' priority.
+ /// Based on performance measurements, currently only analyzers which register SymbolStart/End actions
+ /// or SemanticModel actions are considered candidates to be de-prioritized. However, these semantics
+ /// could be changed in future based on performance measurements.
+ ///
+ ValueTask> GetDeprioritizationCandidatesAsync(
+ Checksum solutionChecksum, ProjectId projectId, ImmutableHashSet analyzerIds, CancellationToken cancellationToken);
+
ValueTask CalculateDiagnosticsAsync(Checksum solutionChecksum, DiagnosticArguments arguments, CancellationToken cancellationToken);
+ ValueTask> ProduceProjectDiagnosticsAsync(
+ Checksum solutionChecksum, ProjectId projectId,
+ ImmutableHashSet analyzerIds,
+ ImmutableHashSet? diagnosticIds,
+ ImmutableArray documentIds,
+ bool includeLocalDocumentDiagnostics,
+ bool includeNonLocalDocumentDiagnostics,
+ bool includeProjectNonLocalResult,
+ CancellationToken cancellationToken);
ValueTask> GetSourceGeneratorDiagnosticsAsync(Checksum solutionChecksum, ProjectId projectId, CancellationToken cancellationToken);
ValueTask ReportAnalyzerPerformanceAsync(ImmutableArray snapshot, int unitCount, bool forSpanAnalysis, CancellationToken cancellationToken);
diff --git a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs
index 4c35590421bb8..ed78aa2e3ed87 100644
--- a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs
+++ b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs
@@ -10,6 +10,7 @@
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Internal.Log;
+using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Telemetry;
@@ -75,6 +76,33 @@ public async ValueTask CalculateDiagnosti
}
}
+ public ValueTask> ProduceProjectDiagnosticsAsync(
+ Checksum solutionChecksum, ProjectId projectId,
+ ImmutableHashSet analyzerIds,
+ ImmutableHashSet? diagnosticIds,
+ ImmutableArray documentIds,
+ bool includeLocalDocumentDiagnostics,
+ bool includeNonLocalDocumentDiagnostics,
+ bool includeProjectNonLocalResult,
+ CancellationToken cancellationToken)
+ {
+ return RunWithSolutionAsync(
+ solutionChecksum,
+ async solution =>
+ {
+ var project = solution.GetRequiredProject(projectId);
+ var service = (DiagnosticAnalyzerService)solution.Services.GetRequiredService();
+
+ var allProjectAnalyzers = await service.GetProjectAnalyzersAsync(project, cancellationToken).ConfigureAwait(false);
+
+ return await service.ProduceProjectDiagnosticsAsync(
+ project, allProjectAnalyzers.FilterAnalyzers(analyzerIds), diagnosticIds, documentIds,
+ includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, includeProjectNonLocalResult,
+ cancellationToken).ConfigureAwait(false);
+ },
+ cancellationToken);
+ }
+
public ValueTask> ForceAnalyzeProjectAsync(
Checksum solutionChecksum,
ProjectId projectId,
@@ -254,4 +282,24 @@ public ValueTask> GetDeprioritizationCandidatesAsync(
+ Checksum solutionChecksum, ProjectId projectId, ImmutableHashSet analyzerIds, CancellationToken cancellationToken)
+ {
+ return RunWithSolutionAsync(
+ solutionChecksum,
+ async solution =>
+ {
+ var project = solution.GetRequiredProject(projectId);
+ var service = (DiagnosticAnalyzerService)solution.Services.GetRequiredService();
+
+ var allProjectAnalyzers = await service.GetProjectAnalyzersAsync(project, cancellationToken).ConfigureAwait(false);
+
+ var candidates = await service.GetDeprioritizationCandidatesAsync(
+ project, allProjectAnalyzers.FilterAnalyzers(analyzerIds), cancellationToken).ConfigureAwait(false);
+
+ return candidates.Select(c => c.GetAnalyzerId()).ToImmutableHashSet();
+ },
+ cancellationToken);
+ }
}