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
@@ -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
{
/// <summary>
/// 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.
/// </summary>
private static readonly ConditionalWeakTable<DiagnosticAnalyzer, StrongBox<bool>> s_analyzerToIsDeprioritizationCandidateMap = new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiagnosticAnalyzer

Just want to make sure this is a sufficient key, as the result of IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync is determined by both the analyzer and compilationWithAnalyzers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I will doc. This is all part of a heuristics system that attempts to tell if an analyzer should be deprioritized when computing lightbulb actions. really all it does is ask "does this analyzer register any SymbolStart/SemanticModel actions". If so, then it should be deprioritized. So the only thing that matters here is if the same DiagAnalyzer instance might produce a different result when computed against a different compilation.

Teh answer is "yes it might". examples could include if this registered different actions in a VBComp vs a C# one (for DiagAnalyzers taht work on both langs), or if it sometimes registered those actions and sometimes did not, based on some aspect of the compilation it ran against.

However, the likelihood of this happening is super small. And in that case, all that that means is that we run the analyzer at a different priority class than we would otehrwise run it.

Again, in all cases, we run all analyzers. This jus taffects how we order them. And so being slightly wrong (because we cahced some earlier data that is EXTREMELY unlikely to ever change within a run of VS) is fine.


private async Task<ImmutableArray<DiagnosticAnalyzer>> GetDeprioritizationCandidatesInProcessAsync(
Project project, ImmutableArray<DiagnosticAnalyzer> analyzers, CancellationToken cancellationToken)
{
using var _ = ArrayBuilder<DiagnosticAnalyzer>.GetInstance(out var builder);

HostAnalyzerInfo? hostAnalyzerInfo = null;
CompilationWithAnalyzersPair? compilationWithAnalyzers = null;

foreach (var analyzer in analyzers)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this loop is virtually the same. just we avoid any work if we've already computed and cached the value for an analyzer in the past.

{
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<bool> IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(DiagnosticAnalyzer analyzer)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method is unchanged.

{
// 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,38 +118,7 @@ public async Task<ImmutableArray<DiagnosticAnalyzer>> GetDeprioritizationCandida
return analyzers.FilterAnalyzers(result.Value);
}

using var _ = ArrayBuilder<DiagnosticAnalyzer>.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<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;
}
return await GetDeprioritizationCandidatesInProcessAsync(project, analyzers, cancellationToken).ConfigureAwait(false);
}

internal async Task<ImmutableArray<DiagnosticData>> ProduceProjectDiagnosticsAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ public ValueTask<ImmutableArray<DiagnosticData>> ComputeDiagnosticsAsync(
solutionChecksum,
async solution =>
{
var document = solution.GetRequiredTextDocument(documentId);
var document = await solution.GetRequiredTextDocumentAsync(
documentId, cancellationToken).ConfigureAwait(false);
var service = (DiagnosticAnalyzerService)solution.Services.GetRequiredService<IDiagnosticAnalyzerService>();

var allProjectAnalyzers = service.GetProjectAnalyzers(document.Project);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading