diff --git a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs index 51d20a9b041..b56a59aa4fd 100644 --- a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs +++ b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs @@ -23,12 +23,12 @@ public class ProjectTelemetry_Tests public void TrackTaskSubclassing_TracksSealedTasks() { var telemetry = new ProjectTelemetry(); - + // Sealed task should be tracked if it derives from Microsoft task telemetry.TrackTaskSubclassing(typeof(TestSealedTask), isMicrosoftOwned: false); - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + // Should track sealed tasks that inherit from Microsoft tasks properties.Count.ShouldBe(1); properties.ShouldContainKey("Microsoft_Build_Utilities_Task"); @@ -42,12 +42,12 @@ public void TrackTaskSubclassing_TracksSealedTasks() public void TrackTaskSubclassing_TracksSubclass() { var telemetry = new ProjectTelemetry(); - + // User task inheriting from Microsoft.Build.Utilities.Task telemetry.TrackTaskSubclassing(typeof(UserTask), isMicrosoftOwned: false); - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + // Should track the Microsoft.Build.Utilities.Task base class properties.Count.ShouldBe(1); properties.ShouldContainKey("Microsoft_Build_Utilities_Task"); @@ -61,12 +61,12 @@ public void TrackTaskSubclassing_TracksSubclass() public void TrackTaskSubclassing_IgnoresMicrosoftOwnedTasks() { var telemetry = new ProjectTelemetry(); - + // Microsoft-owned task should not be tracked even if non-sealed telemetry.TrackTaskSubclassing(typeof(UserTask), isMicrosoftOwned: true); - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + // Should not track Microsoft-owned tasks properties.Count.ShouldBe(0); } @@ -78,13 +78,13 @@ public void TrackTaskSubclassing_IgnoresMicrosoftOwnedTasks() public void TrackTaskSubclassing_TracksMultipleSubclasses() { var telemetry = new ProjectTelemetry(); - + // Track multiple user tasks telemetry.TrackTaskSubclassing(typeof(UserTask), isMicrosoftOwned: false); telemetry.TrackTaskSubclassing(typeof(AnotherUserTask), isMicrosoftOwned: false); - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + // Should aggregate counts for the same base class properties.Count.ShouldBe(1); properties["Microsoft_Build_Utilities_Task"].ShouldBe("2"); @@ -97,13 +97,13 @@ public void TrackTaskSubclassing_TracksMultipleSubclasses() public void TrackTaskSubclassing_HandlesNull() { var telemetry = new ProjectTelemetry(); - + #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type telemetry.TrackTaskSubclassing(null, isMicrosoftOwned: false); #pragma warning restore CS8625 - + var properties = GetMSBuildTaskSubclassProperties(telemetry); - + properties.Count.ShouldBe(0); } @@ -112,7 +112,7 @@ public void TrackTaskSubclassing_HandlesNull() /// private System.Collections.Generic.Dictionary GetMSBuildTaskSubclassProperties(ProjectTelemetry telemetry) { - var method = typeof(ProjectTelemetry).GetMethod("GetMSBuildTaskSubclassProperties", + var method = typeof(ProjectTelemetry).GetMethod("GetMSBuildTaskSubclassProperties", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); return (System.Collections.Generic.Dictionary)method!.Invoke(telemetry, null)!; } @@ -169,7 +169,7 @@ public void MSBuildTaskTelemetry_IsLoggedDuringBuild() var events = new System.Collections.Generic.List(); var logger = new Microsoft.Build.Logging.ConsoleLogger(LoggerVerbosity.Diagnostic); - + using var projectCollection = new ProjectCollection(); using var stringReader = new System.IO.StringReader(projectContent); using var xmlReader = System.Xml.XmlReader.Create(stringReader); @@ -177,7 +177,7 @@ public void MSBuildTaskTelemetry_IsLoggedDuringBuild() // Build the project var result = project.Build(); - + result.ShouldBeTrue(); } } diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs index fb2459d683b..de1f2c063d6 100644 --- a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs +++ b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs @@ -148,6 +148,90 @@ public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() workerNodeData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsNuget); } + [Fact] + public void WorkerNodeTelemetryCollection_TaskFactoryName() + { + WorkerNodeTelemetryData? workerNodeData = null; + InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeData = dt; + + var testProject = """ + + + + + + Log.LogMessage(MessageImportance.Low, "Hello from inline task!"); + + + + + + + + + """; + + MockLogger logger = new MockLogger(_output); + Helpers.BuildProjectContentUsingBuildManager( + testProject, + logger, + new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); + + workerNodeData!.ShouldNotBeNull(); + + // Verify built-in task has AssemblyTaskFactory + var messageTaskKey = (TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"; + workerNodeData.TasksExecutionData.ShouldContainKey(messageTaskKey); + workerNodeData.TasksExecutionData[messageTaskKey].TaskFactoryName.ShouldBe("AssemblyTaskFactory"); + + // Verify inline task has RoslynCodeTaskFactory + var inlineTaskKey = new TaskOrTargetTelemetryKey("InlineTask01", true, false); + workerNodeData.TasksExecutionData.ShouldContainKey(inlineTaskKey); + workerNodeData.TasksExecutionData[inlineTaskKey].TaskFactoryName.ShouldBe("RoslynCodeTaskFactory"); + workerNodeData.TasksExecutionData[inlineTaskKey].ExecutionsCount.ShouldBe(1); + } + + [Fact] + public void TelemetryDataUtils_HashesCustomFactoryName() + { + // Create telemetry data with a custom factory name + var tasksData = new Dictionary + { + { new TaskOrTargetTelemetryKey("CustomTask", true, false), new TaskExecutionStats(TimeSpan.FromMilliseconds(100), 1, 1000, "MyCompany.CustomTaskFactory", null) }, + { new TaskOrTargetTelemetryKey("BuiltInTask", false, false), new TaskExecutionStats(TimeSpan.FromMilliseconds(50), 2, 500, "AssemblyTaskFactory", null) }, + { new TaskOrTargetTelemetryKey("InlineTask", true, false), new TaskExecutionStats(TimeSpan.FromMilliseconds(75), 1, 750, "RoslynCodeTaskFactory", "CLR4") } + }; + var targetsData = new Dictionary(); + var telemetryData = new WorkerNodeTelemetryData(tasksData, targetsData); + + var activityData = telemetryData.AsActivityDataHolder(includeTasksDetails: true, includeTargetDetails: false); + activityData.ShouldNotBeNull(); + + var properties = activityData.GetActivityProperties(); + properties.ShouldContainKey("Tasks"); + + var taskDetails = properties["Tasks"] as List; + taskDetails.ShouldNotBeNull(); + + // Custom factory name should be hashed + var customTask = taskDetails!.FirstOrDefault(t => t.IsCustom && t.Name != GetHashed("InlineTask")); + customTask.ShouldNotBeNull(); + customTask!.FactoryName.ShouldBe(GetHashed("MyCompany.CustomTaskFactory")); + + // Known factory names should NOT be hashed + var builtInTask = taskDetails.FirstOrDefault(t => !t.IsCustom); + builtInTask.ShouldNotBeNull(); + builtInTask!.FactoryName.ShouldBe("AssemblyTaskFactory"); + + var inlineTask = taskDetails.FirstOrDefault(t => t.FactoryName == "RoslynCodeTaskFactory"); + inlineTask.ShouldNotBeNull(); + inlineTask!.FactoryName.ShouldBe("RoslynCodeTaskFactory"); + inlineTask.TaskHostRuntime.ShouldBe("CLR4"); + } + #if NET // test in .net core with telemetry opted in to avoid sending it but enable listening to it [Fact] @@ -263,6 +347,10 @@ public void NodeTelemetryE2E() createItemTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); createItemTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + // Verify TaskFactoryName is populated for built-in tasks + messageTaskData.FactoryName.ShouldBe("AssemblyTaskFactory"); + createItemTaskData.FactoryName.ShouldBe("AssemblyTaskFactory"); + // Verify Targets summary information var targetsSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TargetsSummary")); var targetsSummary = targetsSummaryTagObject.Value as TargetsSummaryInfo; diff --git a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs index 89984c15b9d..37bbc129150 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -99,19 +99,16 @@ public void TrackTaskSubclassing(Type taskType, bool isMicrosoftOwned) // Check if this base type is a Microsoft-owned task // We identify Microsoft tasks by checking if they're in the Microsoft.Build namespace string? baseTypeName = baseType.FullName; - if (!string.IsNullOrEmpty(baseTypeName) && - (baseTypeName.StartsWith("Microsoft.Build.Tasks.") || + if (!string.IsNullOrEmpty(baseTypeName) && + (baseTypeName.StartsWith("Microsoft.Build.Tasks.") || baseTypeName.StartsWith("Microsoft.Build.Utilities."))) { // This is a subclass of a Microsoft-owned task // Track it only if it's NOT itself Microsoft-owned (i.e., user-authored subclass) if (!isMicrosoftOwned) { - if (!_msbuildTaskSubclassUsage.ContainsKey(baseTypeName)) - { - _msbuildTaskSubclassUsage[baseTypeName] = 0; - } - _msbuildTaskSubclassUsage[baseTypeName]++; + _msbuildTaskSubclassUsage.TryGetValue(baseTypeName, out int count); + _msbuildTaskSubclassUsage[baseTypeName] = count + 1; } // Stop at the first Microsoft-owned base class we find break; @@ -162,7 +159,7 @@ public void LogProjectTelemetry(ILoggingService loggingService, BuildEventContex Clean(); } } - + private void Clean() { _assemblyTaskFactoryTasksExecutedCount = 0; @@ -177,39 +174,24 @@ private void Clean() _msbuildTaskSubclassUsage.Clear(); } + private static void AddCountIfNonZero(Dictionary properties, string propertyName, int count) + { + if (count > 0) + { + properties[propertyName] = count.ToString(CultureInfo.InvariantCulture); + } + } + private Dictionary GetTaskFactoryProperties() { Dictionary properties = new(); - if (_assemblyTaskFactoryTasksExecutedCount > 0) - { - properties["AssemblyTaskFactoryTasksExecutedCount"] = _assemblyTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_intrinsicTaskFactoryTasksExecutedCount > 0) - { - properties["IntrinsicTaskFactoryTasksExecutedCount"] = _intrinsicTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_codeTaskFactoryTasksExecutedCount > 0) - { - properties["CodeTaskFactoryTasksExecutedCount"] = _codeTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_roslynCodeTaskFactoryTasksExecutedCount > 0) - { - properties["RoslynCodeTaskFactoryTasksExecutedCount"] = _roslynCodeTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_xamlTaskFactoryTasksExecutedCount > 0) - { - properties["XamlTaskFactoryTasksExecutedCount"] = _xamlTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } - - if (_customTaskFactoryTasksExecutedCount > 0) - { - properties["CustomTaskFactoryTasksExecutedCount"] = _customTaskFactoryTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } + AddCountIfNonZero(properties, "AssemblyTaskFactoryTasksExecutedCount", _assemblyTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "IntrinsicTaskFactoryTasksExecutedCount", _intrinsicTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "CodeTaskFactoryTasksExecutedCount", _codeTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "RoslynCodeTaskFactoryTasksExecutedCount", _roslynCodeTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "XamlTaskFactoryTasksExecutedCount", _xamlTaskFactoryTasksExecutedCount); + AddCountIfNonZero(properties, "CustomTaskFactoryTasksExecutedCount", _customTaskFactoryTasksExecutedCount); return properties; } @@ -217,23 +199,16 @@ private Dictionary GetTaskFactoryProperties() private Dictionary GetTaskProperties() { Dictionary properties = new(); - - var totalTasksExecuted = _assemblyTaskFactoryTasksExecutedCount + + + var totalTasksExecuted = _assemblyTaskFactoryTasksExecutedCount + _intrinsicTaskFactoryTasksExecutedCount + - _codeTaskFactoryTasksExecutedCount + + _codeTaskFactoryTasksExecutedCount + _roslynCodeTaskFactoryTasksExecutedCount + - _xamlTaskFactoryTasksExecutedCount + + _xamlTaskFactoryTasksExecutedCount + _customTaskFactoryTasksExecutedCount; - - if (totalTasksExecuted > 0) - { - properties["TasksExecutedCount"] = totalTasksExecuted.ToString(CultureInfo.InvariantCulture); - } - - if (_taskHostTasksExecutedCount > 0) - { - properties["TaskHostTasksExecutedCount"] = _taskHostTasksExecutedCount.ToString(CultureInfo.InvariantCulture); - } + + AddCountIfNonZero(properties, "TasksExecutedCount", totalTasksExecuted); + AddCountIfNonZero(properties, "TaskHostTasksExecutedCount", _taskHostTasksExecutedCount); return properties; } diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs index f4eeca2c1ef..24dae3c05f3 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -1331,12 +1331,15 @@ void CollectTasksStats(TaskRegistry taskRegistry) foreach (TaskRegistry.RegisteredTaskRecord registeredTaskRecord in taskRegistry.TaskRegistrations.Values.SelectMany(record => record)) { - telemetryForwarder.AddTask(registeredTaskRecord.TaskIdentity.Name, + telemetryForwarder.AddTask( + registeredTaskRecord.TaskIdentity.Name, registeredTaskRecord.Statistics.ExecutedTime, registeredTaskRecord.Statistics.ExecutedCount, registeredTaskRecord.Statistics.TotalMemoryConsumption, registeredTaskRecord.ComputeIfCustom(), - registeredTaskRecord.IsFromNugetCache); + registeredTaskRecord.IsFromNugetCache, + registeredTaskRecord.TaskFactoryAttributeName, + registeredTaskRecord.TaskFactoryParameters.Runtime); registeredTaskRecord.Statistics.Reset(); } @@ -1345,8 +1348,7 @@ void CollectTasksStats(TaskRegistry taskRegistry) } } - private static bool IsMetaprojTargetPath(string targetPath) - => targetPath.EndsWith(".metaproj", StringComparison.OrdinalIgnoreCase); + private static bool IsMetaprojTargetPath(string targetPath) => targetPath.EndsWith(".metaproj", StringComparison.OrdinalIgnoreCase); /// /// Saves the current operating environment (working directory and environment variables) diff --git a/src/Build/TelemetryInfra/ITelemetryForwarder.cs b/src/Build/TelemetryInfra/ITelemetryForwarder.cs index 97735076593..98908828c52 100644 --- a/src/Build/TelemetryInfra/ITelemetryForwarder.cs +++ b/src/Build/TelemetryInfra/ITelemetryForwarder.cs @@ -20,7 +20,9 @@ void AddTask( short executionsCount, long totalMemoryConsumed, bool isCustom, - bool isFromNugetCache); + bool isFromNugetCache, + string? taskFactoryName, + string? taskHostRuntime); /// /// Add info about target execution to the telemetry. diff --git a/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs b/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs index d4a388d79ce..19feabfee3d 100644 --- a/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs +++ b/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs @@ -53,6 +53,7 @@ private void FlushDataIntoConsoleIfRequested() { Console.WriteLine($"{target.Key} : {target.Value}"); } + Console.WriteLine("=========================================="); Console.WriteLine($"Tasks: ({_workerNodeTelemetryData.TasksExecutionData.Count})"); Console.WriteLine("Custom tasks:"); @@ -60,12 +61,14 @@ private void FlushDataIntoConsoleIfRequested() { Console.WriteLine($"{task.Key}"); } + Console.WriteLine("=========================================="); Console.WriteLine("Tasks by time:"); foreach (var task in _workerNodeTelemetryData.TasksExecutionData.OrderByDescending(t => t.Value.CumulativeExecutionTime)) { Console.WriteLine($"{task.Key} - {task.Value.CumulativeExecutionTime}"); } + Console.WriteLine("=========================================="); Console.WriteLine("Tasks by memory consumption:"); foreach (var task in _workerNodeTelemetryData.TasksExecutionData.OrderByDescending(t => t.Value.TotalMemoryBytes)) diff --git a/src/Build/TelemetryInfra/TelemetryForwarderProvider.cs b/src/Build/TelemetryInfra/TelemetryForwarderProvider.cs index 717846204eb..3c832c5d515 100644 --- a/src/Build/TelemetryInfra/TelemetryForwarderProvider.cs +++ b/src/Build/TelemetryInfra/TelemetryForwarderProvider.cs @@ -55,10 +55,10 @@ public class TelemetryForwarder : ITelemetryForwarder // in future, this might be per event type public bool IsTelemetryCollected => true; - public void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, bool isFromNugetCache) + public void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, bool isFromNugetCache, string? taskFactoryName, string? taskHostRuntime) { var key = GetKey(name, isCustom, false, isFromNugetCache); - _workerNodeTelemetryData.AddTask(key, cumulativeExecutionTime, executionsCount, totalMemoryConsumed); + _workerNodeTelemetryData.AddTask(key, cumulativeExecutionTime, executionsCount, totalMemoryConsumed, taskFactoryName, taskHostRuntime); } public void AddTarget(string name, bool wasExecuted, bool isCustom, bool isMetaproj, bool isFromNugetCache) @@ -83,7 +83,8 @@ public class NullTelemetryForwarder : ITelemetryForwarder { public bool IsTelemetryCollected => false; - public void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, bool isFromNugetCache) { } + public void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, bool isFromNugetCache, string? taskFactoryName, string? taskHostRuntime) { } + public void AddTarget(string name, bool wasExecuted, bool isCustom, bool isMetaproj, bool isFromNugetCache) { } public void FinalizeProcessing(LoggingContext loggingContext) { } diff --git a/src/Framework.UnitTests/WorkerNodeTelemetryEventArgs_Tests.cs b/src/Framework.UnitTests/WorkerNodeTelemetryEventArgs_Tests.cs index 930ce27b496..651d0446b5d 100644 --- a/src/Framework.UnitTests/WorkerNodeTelemetryEventArgs_Tests.cs +++ b/src/Framework.UnitTests/WorkerNodeTelemetryEventArgs_Tests.cs @@ -18,9 +18,9 @@ public void SerializationDeserializationTest() WorkerNodeTelemetryData td = new WorkerNodeTelemetryData( new Dictionary() { - { (TaskOrTargetTelemetryKey)"task1", new TaskExecutionStats(TimeSpan.FromMinutes(1), 5, 1234) }, - { (TaskOrTargetTelemetryKey)"task2", new TaskExecutionStats(TimeSpan.Zero, 0, 0) }, - { (TaskOrTargetTelemetryKey)"task3", new TaskExecutionStats(TimeSpan.FromTicks(1234), 12, 987654321) } + { (TaskOrTargetTelemetryKey)"task1", new TaskExecutionStats(TimeSpan.FromMinutes(1), 5, 1234, "AssemblyTaskFactory", "CLR4") }, + { (TaskOrTargetTelemetryKey)"task2", new TaskExecutionStats(TimeSpan.Zero, 0, 0, null, null) }, + { (TaskOrTargetTelemetryKey)"task3", new TaskExecutionStats(TimeSpan.FromTicks(1234), 12, 987654321, "CodeTaskFactory", "NET") } }, new Dictionary() { { (TaskOrTargetTelemetryKey)"target1", false }, { (TaskOrTargetTelemetryKey)"target2", true }, }); diff --git a/src/Framework/Telemetry/TaskExecutionStats.cs b/src/Framework/Telemetry/TaskExecutionStats.cs index 533599734fd..a9dac1ab4bb 100644 --- a/src/Framework/Telemetry/TaskExecutionStats.cs +++ b/src/Framework/Telemetry/TaskExecutionStats.cs @@ -8,10 +8,15 @@ namespace Microsoft.Build.Framework.Telemetry; /// /// Represents the execution statistics of tasks executed on a node. /// -internal class TaskExecutionStats(TimeSpan cumulativeExecutionTime, int executionsCount, long totalMemoryConsumption) +internal class TaskExecutionStats( + TimeSpan cumulativeExecutionTime, + int executionsCount, + long totalMemoryConsumption, + string? taskFactoryName, + string? taskHostRuntime) { private TaskExecutionStats() - : this(TimeSpan.Zero, 0, 0) + : this(TimeSpan.Zero, 0, 0, null, null) { } /// @@ -36,15 +41,30 @@ internal static TaskExecutionStats CreateEmpty() /// public int ExecutionsCount { get; set; } = executionsCount; + /// + /// The name of the task factory used to create this task. + /// Examples: AssemblyTaskFactory, IntrinsicTaskFactory, CodeTaskFactory, + /// RoslynCodeTaskFactory, XamlTaskFactory, or a custom factory name. + /// + public string? TaskFactoryName { get; set; } = taskFactoryName; + + /// + /// The runtime specified for out-of-process task execution. + /// Values: "CLR2", "CLR4", "NET", or null if not specified. + /// + public string? TaskHostRuntime { get; set; } = taskHostRuntime; + /// /// Accumulates statistics from another instance into this one. /// /// Statistics to add to this instance. internal void Accumulate(TaskExecutionStats other) { - this.CumulativeExecutionTime += other.CumulativeExecutionTime; - this.TotalMemoryBytes += other.TotalMemoryBytes; - this.ExecutionsCount += other.ExecutionsCount; + CumulativeExecutionTime += other.CumulativeExecutionTime; + TotalMemoryBytes += other.TotalMemoryBytes; + ExecutionsCount += other.ExecutionsCount; + TaskFactoryName ??= other.TaskFactoryName; + TaskHostRuntime ??= other.TaskHostRuntime; } // We need custom Equals for easier assertions in tests @@ -60,7 +80,9 @@ public override bool Equals(object? obj) protected bool Equals(TaskExecutionStats other) => CumulativeExecutionTime.Equals(other.CumulativeExecutionTime) && TotalMemoryBytes == other.TotalMemoryBytes && - ExecutionsCount == other.ExecutionsCount; + ExecutionsCount == other.ExecutionsCount && + TaskFactoryName == other.TaskFactoryName && + TaskHostRuntime == other.TaskHostRuntime; // Needed since we override Equals public override int GetHashCode() @@ -70,6 +92,8 @@ public override int GetHashCode() var hashCode = CumulativeExecutionTime.GetHashCode(); hashCode = (hashCode * 397) ^ TotalMemoryBytes.GetHashCode(); hashCode = (hashCode * 397) ^ ExecutionsCount.GetHashCode(); + hashCode = (hashCode * 397) ^ (TaskFactoryName?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (TaskHostRuntime?.GetHashCode() ?? 0); return hashCode; } } diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index b7202bd897b..66a56c8f497 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -1,6 +1,7 @@ // 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.Security.Cryptography; using System.Text; @@ -10,6 +11,19 @@ namespace Microsoft.Build.Framework.Telemetry { internal static class TelemetryDataUtils { + /// + /// Known Microsoft task factory type names that should not be hashed. + /// + private static readonly HashSet KnownTaskFactoryNames = new(StringComparer.Ordinal) + { + "AssemblyTaskFactory", + "TaskHostFactory", + "CodeTaskFactory", + "RoslynCodeTaskFactory", + "XamlTaskFactory", + "IntrinsicTaskFactory", + }; + /// /// Transforms collected telemetry data to format recognized by the telemetry infrastructure. /// @@ -76,6 +90,7 @@ private static List GetTasksDetails( foreach (KeyValuePair valuePair in tasksDetails) { string taskName = valuePair.Key.IsCustom ? GetHashed(valuePair.Key.Name) : valuePair.Key.Name; + string? factoryName = GetFactoryNameForTelemetry(valuePair.Value.TaskFactoryName); result.Add(new TaskDetailInfo( taskName, @@ -83,12 +98,27 @@ private static List GetTasksDetails( valuePair.Value.ExecutionsCount, valuePair.Value.TotalMemoryBytes, valuePair.Key.IsCustom, - valuePair.Key.IsNuget)); + valuePair.Key.IsNuget, + factoryName, + valuePair.Value.TaskHostRuntime)); } return result; } + /// + /// Gets the factory name for telemetry, hashing custom factory names. + /// + private static string? GetFactoryNameForTelemetry(string? factoryName) + { + if (string.IsNullOrEmpty(factoryName)) + { + return null; + } + + return KnownTaskFactoryNames.Contains(factoryName!) ? factoryName : GetHashed(factoryName!); + } + /// /// Depending on the platform, hash the value using an available mechanism. /// @@ -130,7 +160,7 @@ public static string Hash(string text) } } - internal record TaskDetailInfo(string Name, double TotalMilliseconds, int ExecutionsCount, long TotalMemoryBytes, bool IsCustom, bool IsNuget); + internal record TaskDetailInfo(string Name, double TotalMilliseconds, int ExecutionsCount, long TotalMemoryBytes, bool IsCustom, bool IsNuget, string? FactoryName, string? TaskHostRuntime); /// /// Converts targets summary to a custom object for telemetry. diff --git a/src/Framework/Telemetry/WorkerNodeTelemetryData.cs b/src/Framework/Telemetry/WorkerNodeTelemetryData.cs index d643045ffe6..ae143f52ed4 100644 --- a/src/Framework/Telemetry/WorkerNodeTelemetryData.cs +++ b/src/Framework/Telemetry/WorkerNodeTelemetryData.cs @@ -18,7 +18,7 @@ public void Add(IWorkerNodeTelemetryData other) { foreach (var task in other.TasksExecutionData) { - AddTask(task.Key, task.Value.CumulativeExecutionTime, task.Value.ExecutionsCount, task.Value.TotalMemoryBytes); + AddTask(task.Key, task.Value.CumulativeExecutionTime, task.Value.ExecutionsCount, task.Value.TotalMemoryBytes, task.Value.TaskFactoryName, task.Value.TaskHostRuntime); } foreach (var target in other.TargetsExecutionData) @@ -27,19 +27,21 @@ public void Add(IWorkerNodeTelemetryData other) } } - public void AddTask(TaskOrTargetTelemetryKey task, TimeSpan cumulativeExectionTime, int executionsCount, long totalMemoryConsumption) + public void AddTask(TaskOrTargetTelemetryKey task, TimeSpan cumulativeExecutionTime, int executionsCount, long totalMemoryConsumption, string? factoryName, string? taskHostRuntime) { TaskExecutionStats? taskExecutionStats; if (!TasksExecutionData.TryGetValue(task, out taskExecutionStats)) { - taskExecutionStats = new(cumulativeExectionTime, executionsCount, totalMemoryConsumption); + taskExecutionStats = new(cumulativeExecutionTime, executionsCount, totalMemoryConsumption, factoryName, taskHostRuntime); TasksExecutionData[task] = taskExecutionStats; } else { - taskExecutionStats.CumulativeExecutionTime += cumulativeExectionTime; + taskExecutionStats.CumulativeExecutionTime += cumulativeExecutionTime; taskExecutionStats.ExecutionsCount += executionsCount; taskExecutionStats.TotalMemoryBytes += totalMemoryConsumption; + taskExecutionStats.TaskFactoryName ??= factoryName; + taskExecutionStats.TaskHostRuntime ??= taskHostRuntime; } } @@ -50,10 +52,9 @@ public void AddTarget(TaskOrTargetTelemetryKey target, bool wasExecuted) wasExecuted || (TargetsExecutionData.TryGetValue(target, out bool wasAlreadyExecuted) && wasAlreadyExecuted); } - public WorkerNodeTelemetryData() - : this(new Dictionary(), new Dictionary()) - { } + public WorkerNodeTelemetryData() : this([], []) { } public Dictionary TasksExecutionData { get; } + public Dictionary TargetsExecutionData { get; } } diff --git a/src/Framework/Telemetry/WorkerNodeTelemetryEventArgs.cs b/src/Framework/Telemetry/WorkerNodeTelemetryEventArgs.cs index 4eef343b196..d2f35e5eb4f 100644 --- a/src/Framework/Telemetry/WorkerNodeTelemetryEventArgs.cs +++ b/src/Framework/Telemetry/WorkerNodeTelemetryEventArgs.cs @@ -27,6 +27,8 @@ internal override void WriteToStream(BinaryWriter writer) writer.Write(entry.Value.CumulativeExecutionTime.Ticks); writer.Write(entry.Value.ExecutionsCount); writer.Write(entry.Value.TotalMemoryBytes); + writer.Write(entry.Value.TaskFactoryName ?? string.Empty); + writer.Write(entry.Value.TaskHostRuntime ?? string.Empty); } writer.Write7BitEncodedInt(WorkerNodeTelemetryData.TargetsExecutionData.Count); @@ -43,11 +45,21 @@ internal override void CreateFromStream(BinaryReader reader, int version) Dictionary tasksExecutionData = new(); for (int i = 0; i < count; i++) { - tasksExecutionData.Add(ReadFromStream(reader), + var key = ReadFromStream(reader); + var cumulativeExecutionTime = TimeSpan.FromTicks(reader.ReadInt64()); + var executionsCount = reader.ReadInt32(); + var totalMemoryBytes = reader.ReadInt64(); + var taskFactoryName = reader.ReadString(); + var taskHostRuntime = reader.ReadString(); + + tasksExecutionData.Add( + key, new TaskExecutionStats( - TimeSpan.FromTicks(reader.ReadInt64()), - reader.ReadInt32(), - reader.ReadInt64())); + cumulativeExecutionTime, + executionsCount, + totalMemoryBytes, + string.IsNullOrEmpty(taskFactoryName) ? null : taskFactoryName, + string.IsNullOrEmpty(taskHostRuntime) ? null : taskHostRuntime)); } count = reader.Read7BitEncodedInt(); diff --git a/src/Framework/TelemetryEventArgs.cs b/src/Framework/TelemetryEventArgs.cs index d3d57e9c5e5..645a72526d3 100644 --- a/src/Framework/TelemetryEventArgs.cs +++ b/src/Framework/TelemetryEventArgs.cs @@ -43,6 +43,7 @@ internal override void WriteToStream(BinaryWriter writer) writer.WriteOptionalString(kvp.Value); } } + internal override void CreateFromStream(BinaryReader reader, int version) { base.CreateFromStream(reader, version); diff --git a/src/Tasks.UnitTests/TelemetryTaskTests.cs b/src/Tasks.UnitTests/TelemetryTaskTests.cs index db374f1757d..4a1c82aeaa4 100644 --- a/src/Tasks.UnitTests/TelemetryTaskTests.cs +++ b/src/Tasks.UnitTests/TelemetryTaskTests.cs @@ -90,11 +90,9 @@ public void TelemetryTaskDuplicateEventDataProperty() Assert.True(retVal); // Should not contain the first value - // Assert.DoesNotContain("EE2493A167D24F00996DE7C8E769EAE6", engine.Log); // Should contain the second value - // Assert.Contains("4ADE3D2622CA400B8B95A039DF540037", engine.Log); } }