File: Log\RoslynTelemetry.Workspaces.cs
Web Access
Project: 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 Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Internal.Log;

/// <summary>
/// Property names that telemetry consumers depend on by string.
/// </summary>
internal static class TelemetryKeys
{
    public const string Name = "Name";
    public const string Value = "Value";
    public const string LanguageName = "LanguageName";
}

/// <summary>
/// The parts of <see cref="RoslynTelemetry"/> that need <see cref="KeyValueLogMessage"/>, which lives in
/// the Workspaces layer and so is not available in the shared layer.
/// </summary>
internal sealed partial class RoslynTelemetry
{
    /// <summary>
    /// Posts a discrete event carrying the wall-clock duration of the returned scope, but only if it
    /// meets or exceeds <paramref name="minThresholdMs"/>. Unlike
    /// <see cref="RecordBlockTime(FunctionId, string)"/> this is not aggregated - each occurrence is
    /// its own event.
    /// </summary>
    public IDisposable? LogBlockTime(FunctionId functionId, KeyValueLogMessage logMessage, int minThresholdMs = -1)
    {
        if (TryGetEnabledSinks(functionId, out _))
            return new TimedEventBlock(this, functionId, logMessage, minThresholdMs);

        logMessage.Free();
        return null;
    }

    private sealed class TimedEventBlock(RoslynTelemetry telemetry, FunctionId functionId, KeyValueLogMessage logMessage, int minThresholdMs) : IDisposable
    {
        private readonly SharedStopwatch _stopwatch = SharedStopwatch.StartNew();

        public void Dispose()
        {
            var elapsed = (long)_stopwatch.Elapsed.TotalMilliseconds;
            if (elapsed >= minThresholdMs)
            {
                // Properties is read inside the setter so that the source message's map is only
                // materialized on the path that actually posts.
                var message = KeyValueLogMessage.Create(static (m, args) =>
                {
                    m[TelemetryKeys.Value] = args.elapsed;
                    m.AddRange(args.source.Properties);
                }, (elapsed, source: logMessage));

                // Don't skew telemetry results by logging in debug bits or under a debugger.
                if (IsDebugging)
                    message.Free();
                else
                    telemetry.Log(functionId, message);
            }

            logMessage.Free();
        }
    }
}