File: TelemetryInfra\TelemetryDataUtils.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Build.Framework.Telemetry
{
    internal static class TelemetryDataUtils
    {
        public static IActivityTelemetryDataHolder? AsActivityDataHolder(this IWorkerNodeTelemetryData? telemetryData, bool includeTasksDetails, bool includeTargetDetails)
        {
            if (telemetryData == null)
            {
                return null;
            }
 
            List<TelemetryItem> telemetryItems = new(4);
 
            if (includeTasksDetails)
            {
                telemetryItems.Add(new TelemetryItem("Tasks",
                    JsonSerializer.Serialize(telemetryData.TasksExecutionData, _serializerOptions), false));
            }
 
            if (includeTargetDetails)
            {
                telemetryItems.Add(new TelemetryItem("Targets",
                    JsonSerializer.Serialize(telemetryData.TargetsExecutionData, _serializerOptions), false));
            }
 
            TargetsSummary targetsSummary = new();
            targetsSummary.Initialize(telemetryData.TargetsExecutionData);
            telemetryItems.Add(new TelemetryItem("TargetsSummary",
                JsonSerializer.Serialize(targetsSummary, _serializerOptions), false));
 
            TasksSummary tasksSummary = new();
            tasksSummary.Initialize(telemetryData.TasksExecutionData);
            telemetryItems.Add(new TelemetryItem("TasksSummary",
                JsonSerializer.Serialize(tasksSummary, _serializerOptions), false));
 
            return new NodeTelemetry(telemetryItems);
        }
 
        private static JsonSerializerOptions _serializerOptions = CreateSerializerOptions();
 
        private static JsonSerializerOptions CreateSerializerOptions()
        {
            var opt = new JsonSerializerOptions
            {
                // Add following if user-friendly indentation would be needed
                // WriteIndented = true,
                Converters =
                {
                    new TargetDataConverter(),
                    new TaskDataConverter(),
                    new TargetsSummary(),
                    new TasksSummary(),
                },
            };
 
            return opt;
        }
 
        private class TargetDataConverter : JsonConverter<Dictionary<TaskOrTargetTelemetryKey, bool>?>
        {
            public override Dictionary<TaskOrTargetTelemetryKey, bool>? Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
                =>
                    throw new NotImplementedException("Reading is not supported");
 
            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TaskOrTargetTelemetryKey, bool>? value,
                JsonSerializerOptions options)
            {
                if (value == null)
                {
                    throw new NotSupportedException("TaskOrTargetTelemetryKey cannot be null in telemetry data");
                }
 
                // Following needed - as System.Text.Json doesn't support indexing dictionary by composite types
 
                writer.WriteStartArray();
 
                foreach (KeyValuePair<TaskOrTargetTelemetryKey, bool> valuePair in value)
                {
                    writer.WriteStartObject(valuePair.Key.IsCustom || valuePair.Key.IsFromMetaProject ? ActivityExtensions.GetHashed(valuePair.Key.Name) : valuePair.Key.Name);
                    writer.WriteBoolean("WasExecuted", valuePair.Value);
                    writer.WriteBoolean("IsCustom", valuePair.Key.IsCustom);
                    writer.WriteBoolean("IsFromNuget", valuePair.Key.IsFromNugetCache);
                    writer.WriteBoolean("IsMetaproj", valuePair.Key.IsFromMetaProject);
                    writer.WriteEndObject();
                }
 
                writer.WriteEndArray();
            }
        }
 
        private class TaskDataConverter : JsonConverter<Dictionary<TaskOrTargetTelemetryKey, TaskExecutionStats>?>
        {
            public override Dictionary<TaskOrTargetTelemetryKey, TaskExecutionStats>? Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
                =>
                    throw new NotImplementedException("Reading is not supported");
 
            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TaskOrTargetTelemetryKey, TaskExecutionStats>? value,
                JsonSerializerOptions options)
            {
                if (value == null)
                {
                    throw new NotSupportedException("TaskOrTargetTelemetryKey cannot be null in telemetry data");
                }
 
                // Following needed - as System.Text.Json doesn't support indexing dictionary by composite types
 
                writer.WriteStartArray();
 
                foreach (KeyValuePair<TaskOrTargetTelemetryKey, TaskExecutionStats> valuePair in value)
                {
                    writer.WriteStartObject(valuePair.Key.IsCustom ? ActivityExtensions.GetHashed(valuePair.Key.Name) : valuePair.Key.Name);
                    // We do not want decimals
                    writer.WriteNumber("ExecTimeMs", valuePair.Value.CumulativeExecutionTime.TotalMilliseconds / 1);
                    writer.WriteNumber("ExecCnt", valuePair.Value.ExecutionsCount);
                    // We do not want decimals
                    writer.WriteNumber("MemKBs", valuePair.Value.TotalMemoryConsumption / 1024);
                    writer.WriteBoolean("IsCustom", valuePair.Key.IsCustom);
                    writer.WriteBoolean("IsFromNuget", valuePair.Key.IsFromNugetCache);
                    writer.WriteEndObject();
                }
 
                writer.WriteEndArray();
            }
        }
 
        private class TargetsSummary : JsonConverter<TargetsSummary>
        {
            public void Initialize(Dictionary<TaskOrTargetTelemetryKey, bool> targetsExecutionData)
            {
                foreach (var targetInfo in targetsExecutionData)
                {
                    UpdateStatistics(LoadedBuiltinTargetInfo, LoadedCustomTargetInfo, targetInfo.Key);
                    if (targetInfo.Value)
                    {
                        UpdateStatistics(ExecutedBuiltinTargetInfo, ExecutedCustomTargetInfo, targetInfo.Key);
                    }
                }
 
                void UpdateStatistics(
                    TargetInfo builtinTargetInfo,
                    TargetInfo customTargetInfo,
                    TaskOrTargetTelemetryKey key)
                {
                    UpdateSingleStatistics(key.IsCustom ? customTargetInfo : builtinTargetInfo, key);
 
                    void UpdateSingleStatistics(TargetInfo targetInfo, TaskOrTargetTelemetryKey kkey)
                    {
                        targetInfo.Total++;
                        if (kkey.IsFromNugetCache)
                        {
                            targetInfo.FromNuget++;
                        }
                        if (kkey.IsFromMetaProject)
                        {
                            targetInfo.FromMetaproj++;
                        }
                    }
                }
            }
 
            private TargetInfo LoadedBuiltinTargetInfo { get; } = new();
            private TargetInfo LoadedCustomTargetInfo { get; } = new();
            private TargetInfo ExecutedBuiltinTargetInfo { get; } = new();
            private TargetInfo ExecutedCustomTargetInfo { get; } = new();
 
            private class TargetInfo
            {
                public int Total { get; internal set; }
                public int FromNuget { get; internal set; }
                public int FromMetaproj { get; internal set; }
            }
 
            public override TargetsSummary? Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options) =>
            throw new NotImplementedException("Reading is not supported");
 
            public override void Write(
                Utf8JsonWriter writer,
                TargetsSummary value,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();
                writer.WriteStartObject("Loaded");
                WriteStat(writer, value.LoadedBuiltinTargetInfo, value.LoadedCustomTargetInfo);
                writer.WriteEndObject();
                writer.WriteStartObject("Executed");
                WriteStat(writer, value.ExecutedBuiltinTargetInfo, value.ExecutedCustomTargetInfo);
                writer.WriteEndObject();
                writer.WriteEndObject();
 
 
                void WriteStat(Utf8JsonWriter writer, TargetInfo customTargetsInfo, TargetInfo builtinTargetsInfo)
                {
                    writer.WriteNumber("Total", builtinTargetsInfo.Total + customTargetsInfo.Total);
                    WriteSingleStat(writer, builtinTargetsInfo, "Microsoft");
                    WriteSingleStat(writer, customTargetsInfo, "Custom");
                }
 
                void WriteSingleStat(Utf8JsonWriter writer, TargetInfo targetInfo, string name)
                {
                    if (targetInfo.Total > 0)
                    {
                        writer.WriteStartObject(name);
                        writer.WriteNumber("Total", targetInfo.Total);
                        writer.WriteNumber("FromNuget", targetInfo.FromNuget);
                        writer.WriteNumber("FromMetaproj", targetInfo.FromMetaproj);
                        writer.WriteEndObject();
                    }
                }
            }
        }
 
 
        private class TasksSummary : JsonConverter<TasksSummary>
        {
            public void Initialize(Dictionary<TaskOrTargetTelemetryKey, TaskExecutionStats> tasksExecutionData)
            {
                foreach (var taskInfo in tasksExecutionData)
                {
                    UpdateStatistics(BuiltinTasksInfo, CustomTasksInfo, taskInfo.Key, taskInfo.Value);
                }
 
                void UpdateStatistics(
                    TasksInfo builtinTaskInfo,
                    TasksInfo customTaskInfo,
                    TaskOrTargetTelemetryKey key,
                    TaskExecutionStats taskExecutionStats)
                {
                    UpdateSingleStatistics(key.IsCustom ? customTaskInfo : builtinTaskInfo, taskExecutionStats, key);
 
                    void UpdateSingleStatistics(TasksInfo summarizedTaskInfo, TaskExecutionStats infoToAdd, TaskOrTargetTelemetryKey kkey)
                    {
                        summarizedTaskInfo.Total.AddAnother(infoToAdd);
                        if (kkey.IsFromNugetCache)
                        {
                            summarizedTaskInfo.FromNuget.AddAnother(infoToAdd);
                        }
                    }
                }
            }
 
            private TasksInfo BuiltinTasksInfo { get; } = new TasksInfo();
            private TasksInfo CustomTasksInfo { get; } = new TasksInfo();
 
            private class TasksInfo
            {
                public TaskExecutionStats Total { get; } = TaskExecutionStats.CreateEmpty();
                public TaskExecutionStats FromNuget { get; } = TaskExecutionStats.CreateEmpty();
            }
 
            public override TasksSummary? Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options) =>
            throw new NotImplementedException("Reading is not supported");
 
            public override void Write(
                Utf8JsonWriter writer,
                TasksSummary value,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();
                WriteStat(writer, value.BuiltinTasksInfo, "Microsoft");
                WriteStat(writer, value.CustomTasksInfo, "Custom");
                writer.WriteEndObject();
 
                void WriteStat(Utf8JsonWriter writer, TasksInfo tasksInfo, string name)
                {
                    writer.WriteStartObject(name);
                    WriteSingleStat(writer, tasksInfo.Total, "Total", true);
                    WriteSingleStat(writer, tasksInfo.FromNuget, "FromNuget", false);
                    writer.WriteEndObject();
                }
 
                void WriteSingleStat(Utf8JsonWriter writer, TaskExecutionStats stats, string name, bool writeIfEmpty)
                {
                    if (stats.ExecutionsCount > 0)
                    {
                        writer.WriteStartObject(name);
                        writer.WriteNumber("TotalExecutionsCount", stats.ExecutionsCount);
                        // We do not want decimals
                        writer.WriteNumber("CumulativeExecutionTimeMs", (long)stats.CumulativeExecutionTime.TotalMilliseconds);
                        // We do not want decimals
                        writer.WriteNumber("CumulativeConsumedMemoryKB", stats.TotalMemoryConsumption / 1024);
                        writer.WriteEndObject();
                    }
                }
            }
        }
 
        private class NodeTelemetry : IActivityTelemetryDataHolder
        {
            private readonly IList<TelemetryItem> _items;
 
            public NodeTelemetry(IList<TelemetryItem> items) => _items = items;
 
            public IList<TelemetryItem> GetActivityProperties()
                => _items;
        }
    }
}