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