File: Diagnostics\Service\DiagnosticAnalyzerService_DeprioritizationCandidates.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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;
using System.Collections.Immutable;
using System.Linq;
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, ImmutableHashSet<string>?> s_analyzerToDeprioritizedDiagnosticIds = new();
 
    private async Task<bool> IsDeprioritizedAnalyzerAsync(
        Project project, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken)
    {
        await PopulateDeprioritizedDiagnosticIdMapAsync(project, cancellationToken).ConfigureAwait(false);
 
        // this can't fail as the above call populates the CWT entries for all analyzers within that project if missing.
        Contract.ThrowIfFalse(s_analyzerToDeprioritizedDiagnosticIds.TryGetValue(analyzer, out var set));
 
        return set != null;
    }
 
    private async ValueTask PopulateDeprioritizedDiagnosticIdMapAsync(Project project, CancellationToken cancellationToken)
    {
        await IsAnyDiagnosticIdDeprioritizedAsync(project, diagnosticIds: [], cancellationToken).ConfigureAwait(false);
    }
 
    public async Task<bool> IsAnyDeprioritizedDiagnosticIdInProcessAsync(
        Project project, ImmutableArray<string> diagnosticIds, CancellationToken cancellationToken)
    {
        CompilationWithAnalyzers? compilationWithAnalyzers = null;
 
        var analyzers = GetProjectAnalyzers_OnlyCallInProcess(project);
        foreach (var analyzer in analyzers)
        {
            if (!s_analyzerToDeprioritizedDiagnosticIds.TryGetValue(analyzer, out var deprioritizedIds))
            {
                if (compilationWithAnalyzers is null)
                {
                    compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzers_OnlyCallInProcessAsync(
                        project, analyzers, GetOrCreateHostAnalyzerInfo_OnlyCallInProcess(project), this.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false);
                }
 
                deprioritizedIds = await ComputeDeprioritizedDiagnosticIdsAsync(analyzer).ConfigureAwait(false);
 
#if NET
                s_analyzerToDeprioritizedDiagnosticIds.TryAdd(analyzer, deprioritizedIds);
#else
                lock (s_analyzerToDeprioritizedDiagnosticIds)
                {
                    if (!s_analyzerToDeprioritizedDiagnosticIds.TryGetValue(analyzer, out var existing))
                        s_analyzerToDeprioritizedDiagnosticIds.Add(analyzer, deprioritizedIds);
                }
#endif
 
            }
 
            if (deprioritizedIds != null)
            {
                foreach (var id in diagnosticIds)
                {
                    if (deprioritizedIds.Contains(id))
                        return true;
                }
            }
        }
 
        return false;
 
        async ValueTask<ImmutableHashSet<string>?> ComputeDeprioritizedDiagnosticIdsAsync(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 null;
            }
 
            var telemetryInfo = await compilationWithAnalyzers.GetAnalyzerTelemetryInfoAsync(analyzer, cancellationToken).ConfigureAwait(false);
            if (telemetryInfo == null)
                return null;
 
            if (telemetryInfo.SymbolStartActionsCount == 0 && telemetryInfo.SemanticModelActionsCount == 0)
                return null;
 
            return [.. analyzer.SupportedDiagnostics.Select(d => d.Id)];
        }
    }
}