File: SourceGeneratorTelemetry\SourceGeneratorTelemetryCollectorWorkspaceService.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.SourceGeneratorTelemetry;
 
internal sealed class SourceGeneratorTelemetryCollectorWorkspaceService : ISourceGeneratorTelemetryCollectorWorkspaceService
{
    private sealed record GeneratorTelemetryKey
    {
        [SetsRequiredMembers]
        public GeneratorTelemetryKey(ISourceGenerator generator, AnalyzerReference analyzerReference)
        {
            Identity = SourceGeneratorIdentity.Create(generator, analyzerReference);
            FileVersion = "(null)";
 
            if (Identity.AssemblyPath != null)
            {
                FileVersion = IOUtilities.PerformIO(() => FileVersionInfo.GetVersionInfo(Identity.AssemblyPath).FileVersion, defaultValue: "(reading version threw exception)")!;
            }
        }
 
        public required SourceGeneratorIdentity Identity { get; init; }
        public required string FileVersion { get; init; }
    }
 
    /// <summary>
    /// Cache of the <see cref="GeneratorTelemetryKey"/> for a generator to avoid repeatedly reading version information from disk;
    /// this is a ConditionalWeakTable so having telemetry for older runs doesn't keep the generator itself alive.
    /// </summary>
    private readonly ConditionalWeakTable<ISourceGenerator, GeneratorTelemetryKey> _generatorTelemetryKeys = new();
 
    private readonly StatisticLogAggregator<GeneratorTelemetryKey> _elapsedTimeByGenerator = new();
    private readonly StatisticLogAggregator<GeneratorTelemetryKey> _producedFilesByGenerator = new();
 
    private GeneratorTelemetryKey GetTelemetryKey(ISourceGenerator generator, Func<ISourceGenerator, AnalyzerReference> getAnalyzerReference)
        => _generatorTelemetryKeys.GetValue(generator, g => new GeneratorTelemetryKey(g, getAnalyzerReference(g)));
 
    public void CollectRunResult(
        GeneratorDriverRunResult driverRunResult,
        GeneratorDriverTimingInfo driverTimingInfo,
        Func<ISourceGenerator, AnalyzerReference> getAnalyzerReference)
    {
        foreach (var generatorTime in driverTimingInfo.GeneratorTimes)
        {
            _elapsedTimeByGenerator.AddDataPoint(GetTelemetryKey(generatorTime.Generator, getAnalyzerReference), generatorTime.ElapsedTime);
        }
 
        foreach (var generatorResult in driverRunResult.Results)
        {
            _producedFilesByGenerator.AddDataPoint(GetTelemetryKey(generatorResult.Generator, getAnalyzerReference), generatorResult.GeneratedSources.Length);
        }
    }
 
    public void ReportStatisticsAndClear(FunctionId functionId)
    {
        foreach (var (telemetryKey, elapsedTimeCounter) in _elapsedTimeByGenerator)
        {
            // We'll log one event per generator
            Logger.Log(functionId, KeyValueLogMessage.Create(map =>
            {
                // TODO: have a policy for when we don't have to hash them
                map[nameof(telemetryKey.Identity.AssemblyName) + "Hashed"] = AnalyzerNameForTelemetry.ComputeSha256Hash(telemetryKey.Identity.AssemblyName);
                map[nameof(telemetryKey.Identity.AssemblyVersion)] = telemetryKey.Identity.AssemblyVersion.ToString();
                map[nameof(telemetryKey.Identity.TypeName) + "Hashed"] = AnalyzerNameForTelemetry.ComputeSha256Hash(telemetryKey.Identity.TypeName);
                map[nameof(telemetryKey.FileVersion)] = telemetryKey.FileVersion;
 
                var result = elapsedTimeCounter.GetStatisticResult();
                result.WriteTelemetryPropertiesTo(map, prefix: "ElapsedTimePerRun");
 
                var producedFileCount = _producedFilesByGenerator.GetStatisticResult(telemetryKey);
                producedFileCount.WriteTelemetryPropertiesTo(map, prefix: "GeneratedFileCountPerRun");
            }));
        }
    }
}