File: Workspace\VisualStudioSourceGeneratorTelemetryCollectorWorkspaceServiceFactory.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_hh2lotzg_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SourceGeneration;
using Microsoft.CodeAnalysis.SourceGeneratorTelemetry;
using Microsoft.CodeAnalysis.Threading;
 
namespace Microsoft.VisualStudio.LanguageServices;
 
/// <summary>
/// Exports a <see cref="ISourceGeneratorTelemetryCollectorWorkspaceService"/> which is watched across all workspaces. This lets us collect
/// statistics for all workspaces (including things like interactive, preview, etc.) so we can get the overall counts to report.
/// </summary>
[Export]
[ExportWorkspaceServiceFactory(typeof(ISourceGeneratorTelemetryCollectorWorkspaceService)), Shared]
internal sealed class VisualStudioSourceGeneratorTelemetryCollectorWorkspaceServiceFactory : IWorkspaceServiceFactory, ISourceGeneratorTelemetryReporterWorkspaceService
{
    /// <summary>
    /// The collector that's used to collect all the telemetry for operations within <see
    /// cref="VisualStudioWorkspace"/>. We'll report this when the solution is closed, so the telemetry is linked to
    /// that.
    /// </summary>
    private readonly SourceGeneratorTelemetryCollectorWorkspaceService _visualStudioWorkspaceInstance = new();
 
    /// <summary>
    /// The collector used to collect telemetry for any other workspaces that might be created; we'll report this at
    /// the end of the session since nothing here is necessarily linked to a specific solution. The expectation is
    /// this may be empty for many/most sessions, but we don't want a hole in our reporting and discover that the
    /// hard way.
    /// </summary>
    private readonly SourceGeneratorTelemetryCollectorWorkspaceService _otherWorkspacesInstance = new();
 
    private readonly AsyncBatchingWorkQueue _workQueue;
    private readonly IThreadingContext _threadingContext;
 
    private readonly Lazy<VisualStudioWorkspace> _visualStudioWorkspace;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public VisualStudioSourceGeneratorTelemetryCollectorWorkspaceServiceFactory(IThreadingContext threadingContext, IAsynchronousOperationListenerProvider listenerProvider, Lazy<VisualStudioWorkspace> visualStudioWorkspace)
    {
        _threadingContext = threadingContext;
        _visualStudioWorkspace = visualStudioWorkspace;
 
        // We will report telemetry every five minutes, if we have any to report
        _workQueue = new AsyncBatchingWorkQueue(
            TimeSpan.FromMinutes(5),
            SendSourceGeneratorTelemetryAsync,
            listenerProvider.GetListener(FeatureAttribute.Telemetry),
            _threadingContext.DisposalToken);
    }
 
    public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
    {
        // We will record all generators for the main workspace in one bucket, and any other generators running in other
        // workspaces (interactive, for example) will be put in a different bucket.
        if (workspaceServices.Workspace is VisualStudioWorkspace)
        {
            return _visualStudioWorkspaceInstance;
        }
        else
        {
            return _otherWorkspacesInstance;
        }
    }
 
    public void QueueReportingOfTelemetry()
    {
        _workQueue.AddWork();
    }
 
    private async ValueTask SendSourceGeneratorTelemetryAsync(CancellationToken cancellationToken)
    {
        ReportData(FunctionId.SourceGenerator_SolutionInProcStatistics, _visualStudioWorkspaceInstance.FetchKeysAndAndClear());
        ReportData(FunctionId.SourceGenerator_OtherWorkspaceSessionStatistics, _otherWorkspacesInstance.FetchKeysAndAndClear());
 
        var client = await RemoteHostClient.TryGetClientAsync(_visualStudioWorkspace.Value, cancellationToken).ConfigureAwait(false);
        if (client is not null)
        {
            // We'll still report this telemetry in-process, which gives it the benefit of being scoped under the same telemetry context
            // that is associated with the current open solution. This would allow for better correlation of the telemetry to the size of the solutions, etc.
            var remoteTelemetryData = await client.TryInvokeAsync(static (IRemoteSourceGenerationService service, CancellationToken ct) => service.FetchAndClearTelemetryKeyValuePairsAsync(ct), cancellationToken).ConfigureAwait(false);
            if (remoteTelemetryData.HasValue)
                ReportData(FunctionId.SourceGenerator_SolutionStatistics, remoteTelemetryData.Value);
        }
 
        void ReportData(FunctionId functionId, ImmutableArray<ImmutableDictionary<string, object?>> data)
        {
            // Don't send empty events if we don't have any data
            if (data.Length != 0)
            {
                foreach (var map in data)
                {
                    Logger.Log(functionId, KeyValueLogMessage.Create(map =>
                    {
                        foreach (var kvp in map)
                            map[kvp.Key] = kvp.Value;
                    }));
                }
            }
        }
    }
}