File: Services\DiagnosticAnalyzer\PerformanceTrackerService.cs
Web Access
Project: src\src\Workspaces\Remote\ServiceHub\Microsoft.CodeAnalysis.Remote.ServiceHub.csproj (Microsoft.CodeAnalysis.Remote.ServiceHub)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Telemetry;
 
namespace Microsoft.CodeAnalysis.Remote.Diagnostics;
 
/// <summary>
/// Track diagnostic performance 
/// </summary>
[ExportWorkspaceService(typeof(IPerformanceTrackerService), [WorkspaceKind.RemoteWorkspace]), Shared]
internal class PerformanceTrackerService : IPerformanceTrackerService
{
    // We require at least 100 samples for background document analysis result to be stable.
    private const int MinSampleSizeForDocumentAnalysis = 100;
    // We require at least 20 samples for span/lightbulb analysis result to be stable.
    // Note that each lightbulb invocation produces 4 samples, one for each of the below diagnostic computaion:
    //      1. Compiler syntax diagnostics
    //      2. Analyzer syntax diagnostics
    //      3. Compiler semantic diagnostics
    //      4. Analyzer semantic diagnostics
    private const int MinSampleSizeForSpanAnalysis = 20;
 
    private static readonly Func<IEnumerable<AnalyzerPerformanceInfo>, int, bool, string> s_snapshotLogger = SnapshotLogger;
 
    private readonly PerformanceQueue _queueForDocumentAnalysis, _queueForSpanAnalysis;
    private readonly ConcurrentDictionary<string, bool> _builtInMap = new ConcurrentDictionary<string, bool>(concurrencyLevel: 2, capacity: 10);
 
    public event EventHandler SnapshotAdded;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public PerformanceTrackerService()
        : this(MinSampleSizeForDocumentAnalysis, MinSampleSizeForSpanAnalysis)
    {
    }
 
    // internal for testing
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")]
    internal PerformanceTrackerService(int minSampleSizeForDocumentAnalysis, int minSampleSizeForSpanAnalysis)
    {
        _queueForDocumentAnalysis = new PerformanceQueue(minSampleSizeForDocumentAnalysis);
 
        _queueForSpanAnalysis = new PerformanceQueue(minSampleSizeForSpanAnalysis);
    }
 
    private PerformanceQueue GetQueue(bool forSpanAnalysis)
        => forSpanAnalysis ? _queueForSpanAnalysis : _queueForDocumentAnalysis;
 
    public void AddSnapshot(IEnumerable<AnalyzerPerformanceInfo> snapshot, int unitCount, bool forSpanAnalysis)
    {
        foreach (var perfInfo in snapshot)
        {
            const int PerformAnalysisTelemetryDelay = 250;
 
            var delay = (long)perfInfo.TimeSpan.TotalMilliseconds;
 
            TelemetryLogging.LogAggregatedHistogram(FunctionId.PerformAnalysis_Summary, $"IndividualTimes", delay);
 
            if (delay > PerformAnalysisTelemetryDelay)
            {
                const string AnalyzerId = nameof(AnalyzerId);
                const string Delay = nameof(Delay);
                const string ForSpanAnalysis = nameof(ForSpanAnalysis);
 
                var analyzerId = perfInfo.BuiltIn ? perfInfo.AnalyzerId : perfInfo.BuiltIn.GetHashCode().ToString();
 
                var logMessage = KeyValueLogMessage.Create(m =>
                {
                    m[AnalyzerId] = analyzerId;
                    m[Delay] = delay;
                    m[ForSpanAnalysis] = forSpanAnalysis;
                });
 
                TelemetryLogging.Log(FunctionId.PerformAnalysis_Delay, logMessage);
            }
        }
 
        Logger.Log(FunctionId.PerformanceTrackerService_AddSnapshot, s_snapshotLogger, snapshot, unitCount, forSpanAnalysis);
 
        RecordBuiltInAnalyzers(snapshot);
 
        var queue = GetQueue(forSpanAnalysis);
        lock (queue)
        {
            queue.Add(snapshot.Select(entry => (entry.AnalyzerId, entry.TimeSpan)), unitCount);
        }
 
        OnSnapshotAdded();
    }
 
    public void GenerateReport(List<AnalyzerInfoForPerformanceReporting> analyzerInfos, bool forSpanAnalysis)
    {
        using var pooledRaw = SharedPools.Default<List<(string analyzerId, double average, double stddev)>>().GetPooledObject();
 
        var rawPerformanceData = pooledRaw.Object;
 
        var queue = GetQueue(forSpanAnalysis);
        lock (queue)
        {
            // first get raw aggregated peformance data from the queue
            queue.GetPerformanceData(rawPerformanceData);
        }
 
        // make sure there are some data
        if (rawPerformanceData.Count == 0)
        {
            return;
        }
 
        foreach (var (analyzerId, average, stddev) in rawPerformanceData.OrderByDescending(k => k.average))
        {
            analyzerInfos.Add(new AnalyzerInfoForPerformanceReporting(AllowTelemetry(analyzerId), analyzerId, average, stddev));
        }
    }
 
    private void RecordBuiltInAnalyzers(IEnumerable<AnalyzerPerformanceInfo> snapshot)
    {
        foreach (var entry in snapshot)
        {
            _builtInMap[entry.AnalyzerId] = entry.BuiltIn;
        }
    }
 
    private bool AllowTelemetry(string analyzerId)
    {
        if (_builtInMap.TryGetValue(analyzerId, out var builtIn))
        {
            return builtIn;
        }
 
        return false;
    }
 
    private void OnSnapshotAdded()
        => SnapshotAdded?.Invoke(this, EventArgs.Empty);
 
    private static string SnapshotLogger(IEnumerable<AnalyzerPerformanceInfo> snapshots, int unitCount, bool forSpan)
    {
        using var pooledObject = SharedPools.Default<StringBuilder>().GetPooledObject();
        var sb = pooledObject.Object;
 
        sb.Append(unitCount);
 
        sb.Append('(');
        sb.Append(forSpan ? "SpanAnalysis" : "DocumentAnalysis");
        sb.Append(')');
 
        foreach (var snapshot in snapshots)
        {
            sb.Append('|');
            sb.Append(snapshot.AnalyzerId);
            sb.Append(':');
            sb.Append(snapshot.BuiltIn);
            sb.Append(':');
            sb.Append(snapshot.TimeSpan.TotalMilliseconds);
        }
 
        sb.Append('*');
 
        return sb.ToString();
    }
}