File: Workspace\VisualStudioSourceGeneratorTelemetryCollectorWorkspaceServiceFactory.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_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.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.SourceGeneratorTelemetry;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
 
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 class VisualStudioSourceGeneratorTelemetryCollectorWorkspaceServiceFactory : IWorkspaceServiceFactory, IVsSolutionEvents
{
    /// <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 IThreadingContext _threadingContext;
    private readonly IAsyncServiceProvider _serviceProvider;
    private volatile int _subscribedToSolutionEvents;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public VisualStudioSourceGeneratorTelemetryCollectorWorkspaceServiceFactory(IThreadingContext threadingContext, SVsServiceProvider serviceProvider)
    {
        _threadingContext = threadingContext;
        _serviceProvider = (IAsyncServiceProvider)serviceProvider;
    }
 
    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. This allows us to report the telemetry
        // from the primary workspace on solution closed, while not letting the unrelated runs pollute those numbers.
        if (workspaceServices.Workspace is VisualStudioWorkspace)
        {
            EnsureSubscribedToSolutionEvents();
            return _visualStudioWorkspaceInstance;
        }
        else
        {
            return _otherWorkspacesInstance;
        }
    }
 
    private void EnsureSubscribedToSolutionEvents()
    {
        if (Interlocked.CompareExchange(ref _subscribedToSolutionEvents, 1, 0) == 0)
        {
            Task.Run(async () =>
            {
                var shellService = await _serviceProvider.GetServiceAsync<SVsSolution, IVsSolution>(_threadingContext.JoinableTaskFactory).ConfigureAwait(true);
                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(_threadingContext.DisposalToken);
                shellService.AdviseSolutionEvents(this, out _);
            }, _threadingContext.DisposalToken);
        }
    }
 
    public void ReportOtherWorkspaceTelemetry()
    {
        _otherWorkspacesInstance.ReportStatisticsAndClear(FunctionId.SourceGenerator_OtherWorkspaceSessionStatistics);
    }
 
    int IVsSolutionEvents.OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) => VSConstants.E_NOTIMPL;
    int IVsSolutionEvents.OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) => VSConstants.E_NOTIMPL;
    int IVsSolutionEvents.OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) => VSConstants.E_NOTIMPL;
    int IVsSolutionEvents.OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) => VSConstants.E_NOTIMPL;
    int IVsSolutionEvents.OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) => VSConstants.E_NOTIMPL;
    int IVsSolutionEvents.OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) => VSConstants.E_NOTIMPL;
    int IVsSolutionEvents.OnAfterOpenSolution(object pUnkReserved, int fNewSolution) => VSConstants.E_NOTIMPL;
    int IVsSolutionEvents.OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) => VSConstants.E_NOTIMPL;
 
    int IVsSolutionEvents.OnBeforeCloseSolution(object pUnkReserved)
    {
        // Report the telemetry now before the solution is closed; since this will be reported per solution session ID, it means
        // we can distinguish how many solutions have generators versus just overall sessions.
        _visualStudioWorkspaceInstance.ReportStatisticsAndClear(FunctionId.SourceGenerator_SolutionStatistics);
 
        return VSConstants.S_OK;
    }
 
    int IVsSolutionEvents.OnAfterCloseSolution(object pUnkReserved) => VSConstants.E_NOTIMPL;
}