File: Services\DiagnosticAnalyzer\PerformanceTrackerService.cs
Web Access
Project: ..\..\..\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;
 
namespace Microsoft.CodeAnalysis.Remote.Diagnostics
{
    /// <summary>
    /// Track diagnostic performance 
    /// </summary>
    [ExportWorkspaceService(typeof(IPerformanceTrackerService), WorkspaceKind.Host), 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)
        {
            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();
        }
    }
}