Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ internal interface IDiagnosticAnalyzerService : IWorkspaceService
/// <inheritdoc cref="IRemoteDiagnosticAnalyzerService.ForceAnalyzeProjectAsync"/>
Task<ImmutableArray<DiagnosticData>> ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken);

/// <inheritdoc cref="IRemoteDiagnosticAnalyzerService.GetDeprioritizationCandidatesAsync"/>
Task<ImmutableArray<DiagnosticAnalyzer>> GetDeprioritizationCandidatesAsync(
Project project, ImmutableArray<DiagnosticAnalyzer> analyzers, CancellationToken cancellationToken);

/// <summary>
/// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeStyle;
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;
Expand Down Expand Up @@ -176,22 +178,47 @@ public async Task<ImmutableArray<DiagnosticData>> GetProjectDiagnosticsForIdsAsy
cancellationToken).ConfigureAwait(false);
}

private Task<ImmutableArray<DiagnosticData>> ProduceProjectDiagnosticsAsync(
internal async Task<ImmutableArray<DiagnosticData>> ProduceProjectDiagnosticsAsync(
Project project,
ImmutableArray<DiagnosticAnalyzer> analyzers,
ImmutableHashSet<string>? diagnosticIds,
IReadOnlyList<DocumentId> documentIds,
ImmutableArray<DocumentId> 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<IRemoteDiagnosticAnalyzerService, ImmutableArray<DiagnosticData>>(
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<ImmutableArray<DiagnosticAnalyzer>> GetProjectAnalyzersAsync(
Project project, CancellationToken cancellationToken)
{
return _stateManager.GetOrCreateAnalyzersAsync(
project.Solution.SolutionState, project.State, cancellationToken);
}

private async Task<ImmutableArray<DiagnosticAnalyzer>> GetDiagnosticAnalyzersAsync(
Expand All @@ -200,9 +227,7 @@ private async Task<ImmutableArray<DiagnosticAnalyzer>> GetDiagnosticAnalyzersAsy
Func<DiagnosticAnalyzer, bool>? 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;
Expand Down Expand Up @@ -355,6 +380,59 @@ public async Task<ImmutableDictionary<string, ImmutableArray<DiagnosticDescripto
return project.Solution.SolutionState.Analyzers.GetDiagnosticDescriptorsPerReference(this._analyzerInfoCache, project);
}

public async Task<ImmutableArray<DiagnosticAnalyzer>> GetDeprioritizationCandidatesAsync(
Project project, ImmutableArray<DiagnosticAnalyzer> 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<IRemoteDiagnosticAnalyzerService, ImmutableHashSet<string>>(
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<DiagnosticAnalyzer>.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<bool> 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<DiagnosticAnalyzer>
{
public static readonly DiagnosticAnalyzerComparer Instance = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public async Task<ImmutableArray<DiagnosticData>> ProduceProjectDiagnosticsAsync
Project project,
ImmutableArray<DiagnosticAnalyzer> analyzers,
ImmutableHashSet<string>? diagnosticIds,
IReadOnlyList<DocumentId> documentIds,
ImmutableArray<DocumentId> documentIds,
bool includeLocalDocumentDiagnostics,
bool includeNonLocalDocumentDiagnostics,
bool includeProjectNonLocalResult,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -203,18 +204,21 @@ async Task ComputeDocumentDiagnosticsAsync(
Debug.Assert(!incrementalAnalysis || kind == AnalysisKind.Semantic);
Debug.Assert(!incrementalAnalysis || analyzers.All(analyzer => analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis()));

using var _ = ArrayBuilder<DiagnosticAnalyzer>.GetInstance(analyzers.Length, out var filteredAnalyzers);
using var _1 = ArrayBuilder<DiagnosticAnalyzer>.GetInstance(analyzers.Length, out var filteredAnalyzers);
using var _2 = PooledHashSet<DiagnosticAnalyzer>.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));

// 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);
}
Expand All @@ -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);
Expand All @@ -235,7 +237,7 @@ async Task ComputeDocumentDiagnosticsAsync(
ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<DiagnosticData>> 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,
Expand All @@ -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);
}
Expand All @@ -260,8 +262,9 @@ async Task ComputeDocumentDiagnosticsAsync(
_incrementalMemberEditAnalyzer.UpdateDocumentWithCachedDiagnostics((Document)document);
}

async Task<bool> TryDeprioritizeAnalyzerAsync(
DiagnosticAnalyzer analyzer, AnalysisKind kind, TextSpan? span)
bool TryDeprioritizeAnalyzer(
DiagnosticAnalyzer analyzer, AnalysisKind kind, TextSpan? span,
HashSet<DiagnosticAnalyzer> 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,
Expand All @@ -284,10 +287,8 @@ async Task<bool> 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
Expand All @@ -301,31 +302,6 @@ async Task<bool> 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<bool> 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 &&
Expand Down
16 changes: 16 additions & 0 deletions src/Workspaces/Core/Portable/Diagnostics/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -568,4 +568,20 @@ public static bool IsReportedInDocument(Diagnostic diagnostic, TextDocument targ

return false;
}

public static ImmutableArray<DiagnosticAnalyzer> FilterAnalyzers(
this ImmutableArray<DiagnosticAnalyzer> analyzers,
ImmutableHashSet<string> analyzerIds)
{
using var _ = PooledDictionary<string, DiagnosticAnalyzer>.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];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;

namespace Microsoft.CodeAnalysis.Diagnostics;

Expand All @@ -17,7 +18,27 @@ internal interface IRemoteDiagnosticAnalyzerService
/// </summary>
ValueTask<ImmutableArray<DiagnosticData>> ForceAnalyzeProjectAsync(Checksum solutionChecksum, ProjectId projectId, CancellationToken cancellationToken);

/// <summary>
/// Returns the analyzers that are candidates to be de-prioritized to
/// <see cref="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.
/// </summary>
ValueTask<ImmutableHashSet<string>> GetDeprioritizationCandidatesAsync(
Checksum solutionChecksum, ProjectId projectId, ImmutableHashSet<string> analyzerIds, CancellationToken cancellationToken);

ValueTask<SerializableDiagnosticAnalysisResults> CalculateDiagnosticsAsync(Checksum solutionChecksum, DiagnosticArguments arguments, CancellationToken cancellationToken);
ValueTask<ImmutableArray<DiagnosticData>> ProduceProjectDiagnosticsAsync(
Checksum solutionChecksum, ProjectId projectId,
ImmutableHashSet<string> analyzerIds,
ImmutableHashSet<string>? diagnosticIds,
ImmutableArray<DocumentId> documentIds,
bool includeLocalDocumentDiagnostics,
bool includeNonLocalDocumentDiagnostics,
bool includeProjectNonLocalResult,
CancellationToken cancellationToken);

ValueTask<ImmutableArray<DiagnosticData>> GetSourceGeneratorDiagnosticsAsync(Checksum solutionChecksum, ProjectId projectId, CancellationToken cancellationToken);
ValueTask ReportAnalyzerPerformanceAsync(ImmutableArray<AnalyzerPerformanceInfo> snapshot, int unitCount, bool forSpanAnalysis, CancellationToken cancellationToken);
Expand Down
Loading
Loading