File: Services\ProcessTelemetry\RemoteProcessTelemetryService.PerformanceReporter.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.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Remote.Diagnostics;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.VisualStudio.Telemetry;
using RoslynLogger = Microsoft.CodeAnalysis.Internal.Log.Logger;
 
namespace Microsoft.CodeAnalysis.Remote
{
    internal partial class RemoteProcessTelemetryService
    {
        /// <summary>
        /// Track when last time report has sent and send new report if there is update after given internal
        /// </summary>
        private class PerformanceReporter : GlobalOperationAwareIdleProcessor
        {
            private readonly SemaphoreSlim _event;
 
            private readonly IPerformanceTrackerService _diagnosticAnalyzerPerformanceTracker;
            private readonly TelemetrySession _telemetrySession;
 
            public PerformanceReporter(
                TelemetrySession telemetrySession,
                IPerformanceTrackerService diagnosticAnalyzerPerformanceTracker,
                IGlobalOperationNotificationService globalOperationNotificationService,
                CancellationToken shutdownToken)
                : base(
                    AsynchronousOperationListenerProvider.NullListener,
                    globalOperationNotificationService,
                    backOffTimeSpan: TimeSpan.FromMinutes(2),
                    shutdownToken)
            {
                _event = new SemaphoreSlim(initialCount: 0);
 
                _telemetrySession = telemetrySession;
                _diagnosticAnalyzerPerformanceTracker = diagnosticAnalyzerPerformanceTracker;
                _diagnosticAnalyzerPerformanceTracker.SnapshotAdded += OnSnapshotAdded;
                Start();
            }
 
            protected override void OnPaused()
            {
                // we won't cancel report already running. we will just prevent
                // new one from starting.
            }
 
            protected override Task ExecuteAsync()
            {
                if (!_telemetrySession.IsOptedIn)
                    return Task.CompletedTask;
 
                using (RoslynLogger.LogBlock(FunctionId.Diagnostics_GeneratePerformaceReport, CancellationToken))
                {
                    foreach (var forSpanAnalysis in new[] { false, true })
                    {
                        using var pooledObject = SharedPools.Default<List<AnalyzerInfoForPerformanceReporting>>().GetPooledObject();
                        _diagnosticAnalyzerPerformanceTracker.GenerateReport(pooledObject.Object, forSpanAnalysis);
                        var isInternalUser = _telemetrySession.IsUserMicrosoftInternal;
 
                        foreach (var analyzerInfo in pooledObject.Object)
                        {
                            // this will report telemetry under VS. this will let us see how accurate our performance tracking is
                            RoslynLogger.Log(FunctionId.Diagnostics_AnalyzerPerformanceInfo2, KeyValueLogMessage.Create(m =>
                            {
                                // since it is telemetry, we hash analyzer name if it is not builtin analyzer
                                m[nameof(analyzerInfo.AnalyzerId)] = isInternalUser ? analyzerInfo.AnalyzerId : analyzerInfo.PIISafeAnalyzerId;
                                m[nameof(analyzerInfo.Average)] = analyzerInfo.Average;
                                m[nameof(analyzerInfo.AdjustedStandardDeviation)] = analyzerInfo.AdjustedStandardDeviation;
                                m[nameof(forSpanAnalysis)] = forSpanAnalysis;
                            }));
                        }
                    }
 
                    return Task.CompletedTask;
                }
            }
 
            protected override Task WaitAsync(CancellationToken cancellationToken)
            {
                return _event.WaitAsync(cancellationToken);
            }
 
            private void OnSnapshotAdded(object sender, EventArgs e)
            {
                // this acts like Monitor.Pulse. (wake up event if it is currently waiting
                // if not, ignore. this can have race, but that's fine for this usage case)
                // not using Monitor.Pulse since that doesn't support WaitAsync
                if (_event.CurrentCount > 0)
                {
                    return;
                }
 
                _event.Release();
            }
        }
    }
}