From e75e0e136196d0641218c9708b9adc6808cd9728 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:09:57 +0000 Subject: [PATCH 01/10] Initial plan From ebc3ce6aa41c9bc525007dcbadee93fd7ecee3f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:21:54 +0000 Subject: [PATCH 02/10] Add telemetry tracking for custom task factories and CodeTaskFactory Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> --- .../BackEnd/ProjectTelemetry_Tests.cs | 98 +++++++++++++++++++ .../Components/Logging/ProjectTelemetry.cs | 36 +++++++ 2 files changed, 134 insertions(+) diff --git a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs index 51d20a9b041..81855ae335e 100644 --- a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs +++ b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs @@ -117,6 +117,16 @@ private System.Collections.Generic.Dictionary GetMSBuildTaskSubc return (System.Collections.Generic.Dictionary)method!.Invoke(telemetry, null)!; } + /// + /// Helper method to get custom task factory properties from telemetry using reflection + /// + private System.Collections.Generic.Dictionary GetCustomTaskFactoryProperties(ProjectTelemetry telemetry) + { + var method = typeof(ProjectTelemetry).GetMethod("GetCustomTaskFactoryProperties", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + return (System.Collections.Generic.Dictionary)method!.Invoke(telemetry, null)!; + } + /// /// Non-sealed user task that inherits from Microsoft.Build.Utilities.Task /// @@ -180,5 +190,93 @@ public void MSBuildTaskTelemetry_IsLoggedDuringBuild() result.ShouldBeTrue(); } + + /// + /// Test that AddTaskExecution tracks custom task factory usage individually + /// + [Fact] + public void AddTaskExecution_TracksCustomTaskFactoryIndividually() + { + var telemetry = new ProjectTelemetry(); + + // Add executions from custom task factories + telemetry.AddTaskExecution("CustomFactory.MyTaskFactory", isTaskHost: false); + telemetry.AddTaskExecution("AnotherCustomFactory", isTaskHost: false); + telemetry.AddTaskExecution("CustomFactory.MyTaskFactory", isTaskHost: false); + + var properties = GetCustomTaskFactoryProperties(telemetry); + + // Should track each custom factory separately + properties.Count.ShouldBe(2); + properties.ShouldContainKey("CustomFactory_MyTaskFactory"); + properties["CustomFactory_MyTaskFactory"].ShouldBe("2"); + properties.ShouldContainKey("AnotherCustomFactory"); + properties["AnotherCustomFactory"].ShouldBe("1"); + } + + /// + /// Test that AddTaskExecution does not track built-in factories as custom + /// + [Fact] + public void AddTaskExecution_DoesNotTrackBuiltInFactoriesAsCustom() + { + var telemetry = new ProjectTelemetry(); + + // Add executions from built-in task factories + telemetry.AddTaskExecution("Microsoft.Build.BackEnd.AssemblyTaskFactory", isTaskHost: false); + telemetry.AddTaskExecution("Microsoft.Build.Tasks.CodeTaskFactory", isTaskHost: false); + telemetry.AddTaskExecution("Microsoft.Build.Tasks.RoslynCodeTaskFactory", isTaskHost: false); + + var properties = GetCustomTaskFactoryProperties(telemetry); + + // Should not track any custom factories + properties.Count.ShouldBe(0); + } + + /// + /// Test that AddTaskExecution tracks CodeTaskFactory separately + /// + [Fact] + public void AddTaskExecution_TracksCodeTaskFactorySeparately() + { + var telemetry = new ProjectTelemetry(); + + // Add execution from CodeTaskFactory + telemetry.AddTaskExecution("Microsoft.Build.Tasks.CodeTaskFactory", isTaskHost: false); + telemetry.AddTaskExecution("Microsoft.Build.Tasks.CodeTaskFactory", isTaskHost: false); + + // Use reflection to get task factory properties + var method = typeof(ProjectTelemetry).GetMethod("GetTaskFactoryProperties", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var properties = (System.Collections.Generic.Dictionary)method!.Invoke(telemetry, null)!; + + // Should track CodeTaskFactory separately from custom factories + properties.ShouldContainKey("CodeTaskFactoryTasksExecutedCount"); + properties["CodeTaskFactoryTasksExecutedCount"].ShouldBe("2"); + + // Should not be in custom factory properties + var customProperties = GetCustomTaskFactoryProperties(telemetry); + customProperties.Count.ShouldBe(0); + } + + /// + /// Test that AddTaskExecution handles null or empty factory names gracefully + /// + [Fact] + public void AddTaskExecution_HandlesNullFactoryNameGracefully() + { + var telemetry = new ProjectTelemetry(); + + // Add execution with null or empty factory name +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type + telemetry.AddTaskExecution(null, isTaskHost: false); +#pragma warning restore CS8625 + telemetry.AddTaskExecution(string.Empty, isTaskHost: false); + + var properties = GetCustomTaskFactoryProperties(telemetry); + + // Should handle null/empty gracefully without adding entries + properties.Count.ShouldBe(0); + } } } diff --git a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs index 89984c15b9d..db49ee3b9db 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -27,6 +27,7 @@ internal class ProjectTelemetry private const string TaskFactoryEventName = "build/tasks/taskfactory"; private const string TasksEventName = "build/tasks"; private const string MSBuildTaskSubclassedEventName = "build/tasks/msbuild-subclassed"; + private const string CustomTaskFactoryEventName = "build/tasks/custom-taskfactory"; private int _assemblyTaskFactoryTasksExecutedCount = 0; private int _intrinsicTaskFactoryTasksExecutedCount = 0; @@ -41,6 +42,10 @@ internal class ProjectTelemetry // Maps Microsoft task names to counts of their non-sealed usage private readonly Dictionary _msbuildTaskSubclassUsage = new(); + // Telemetry for custom (non-MSBuild) task factory usage + // Maps custom task factory type names to execution counts + private readonly Dictionary _customTaskFactoryUsage = new(); + /// /// Adds a task execution to the telemetry data. /// @@ -74,7 +79,16 @@ public void AddTaskExecution(string taskFactoryTypeName, bool isTaskHost) break; default: + // Track custom (non-MSBuild) task factories individually _customTaskFactoryTasksExecutedCount++; + if (!string.IsNullOrEmpty(taskFactoryTypeName)) + { + if (!_customTaskFactoryUsage.ContainsKey(taskFactoryTypeName)) + { + _customTaskFactoryUsage[taskFactoryTypeName] = 0; + } + _customTaskFactoryUsage[taskFactoryTypeName]++; + } break; } } @@ -149,6 +163,12 @@ public void LogProjectTelemetry(ILoggingService loggingService, BuildEventContex { loggingService.LogTelemetry(buildEventContext, MSBuildTaskSubclassedEventName, msbuildTaskSubclassProperties); } + + Dictionary customTaskFactoryProperties = GetCustomTaskFactoryProperties(); + if (customTaskFactoryProperties.Count > 0) + { + loggingService.LogTelemetry(buildEventContext, CustomTaskFactoryEventName, customTaskFactoryProperties); + } } catch { @@ -175,6 +195,7 @@ private void Clean() _taskHostTasksExecutedCount = 0; _msbuildTaskSubclassUsage.Clear(); + _customTaskFactoryUsage.Clear(); } private Dictionary GetTaskFactoryProperties() @@ -252,5 +273,20 @@ private Dictionary GetMSBuildTaskSubclassProperties() return properties; } + + private Dictionary GetCustomTaskFactoryProperties() + { + Dictionary properties = new(); + + // Add each custom task factory type name with its usage count + foreach (var kvp in _customTaskFactoryUsage) + { + // Use a sanitized property name (replace dots with underscores for telemetry) + string propertyName = kvp.Key.Replace(".", "_"); + properties[propertyName] = kvp.Value.ToString(CultureInfo.InvariantCulture); + } + + return properties; + } } } From c87d27d4669bf37cbe51d255b632f1be05b81667 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:30:22 +0000 Subject: [PATCH 03/10] Address code review feedback: optimize dictionary lookup and improve sanitization Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> --- .../BackEnd/ProjectTelemetry_Tests.cs | 22 ++++++++++++ .../Components/Logging/ProjectTelemetry.cs | 36 +++++++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs index 81855ae335e..1605950e02f 100644 --- a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs +++ b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs @@ -278,5 +278,27 @@ public void AddTaskExecution_HandlesNullFactoryNameGracefully() // Should handle null/empty gracefully without adding entries properties.Count.ShouldBe(0); } + + /// + /// Test that custom task factory names with special characters are properly sanitized + /// + [Fact] + public void AddTaskExecution_SanitizesCustomTaskFactoryNames() + { + var telemetry = new ProjectTelemetry(); + + // Add executions from custom task factories with special characters + telemetry.AddTaskExecution("My.Custom-Factory Task", isTaskHost: false); + telemetry.AddTaskExecution("Another.Factory", isTaskHost: false); + + var properties = GetCustomTaskFactoryProperties(telemetry); + + // Should sanitize special characters to underscores + properties.Count.ShouldBe(2); + properties.ShouldContainKey("My_Custom_Factory_Task"); + properties["My_Custom_Factory_Task"].ShouldBe("1"); + properties.ShouldContainKey("Another_Factory"); + properties["Another_Factory"].ShouldBe("1"); + } } } diff --git a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs index db49ee3b9db..e39ca733c0f 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -83,11 +83,8 @@ public void AddTaskExecution(string taskFactoryTypeName, bool isTaskHost) _customTaskFactoryTasksExecutedCount++; if (!string.IsNullOrEmpty(taskFactoryTypeName)) { - if (!_customTaskFactoryUsage.ContainsKey(taskFactoryTypeName)) - { - _customTaskFactoryUsage[taskFactoryTypeName] = 0; - } - _customTaskFactoryUsage[taskFactoryTypeName]++; + _customTaskFactoryUsage.TryGetValue(taskFactoryTypeName, out int count); + _customTaskFactoryUsage[taskFactoryTypeName] = count + 1; } break; } @@ -281,12 +278,37 @@ private Dictionary GetCustomTaskFactoryProperties() // Add each custom task factory type name with its usage count foreach (var kvp in _customTaskFactoryUsage) { - // Use a sanitized property name (replace dots with underscores for telemetry) - string propertyName = kvp.Key.Replace(".", "_"); + // Sanitize property name for telemetry: replace dots with underscores + // and remove any other characters that might be problematic + string propertyName = SanitizePropertyName(kvp.Key); properties[propertyName] = kvp.Value.ToString(CultureInfo.InvariantCulture); } return properties; } + + /// + /// Sanitizes a string to make it suitable for use as a telemetry property name. + /// Replaces dots with underscores and removes other potentially problematic characters. + /// + private static string SanitizePropertyName(string name) + { + // Replace dots with underscores and remove other special characters + // Keep alphanumeric characters and underscores only + var sanitized = new System.Text.StringBuilder(name.Length); + foreach (char c in name) + { + if (char.IsLetterOrDigit(c)) + { + sanitized.Append(c); + } + else if (c == '.' || c == '-' || c == ' ') + { + sanitized.Append('_'); + } + // Skip other special characters + } + return sanitized.ToString(); + } } } From 44c1aa3e1699f65669cedfe50107fd7ac13c5b58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:34:31 +0000 Subject: [PATCH 04/10] Add System.Text using statement and use StringBuilder directly Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> --- src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs index e39ca733c0f..94f96781628 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Text; using Microsoft.Build.Framework; namespace Microsoft.Build.BackEnd.Logging @@ -295,7 +296,7 @@ private static string SanitizePropertyName(string name) { // Replace dots with underscores and remove other special characters // Keep alphanumeric characters and underscores only - var sanitized = new System.Text.StringBuilder(name.Length); + var sanitized = new StringBuilder(name.Length); foreach (char c in name) { if (char.IsLetterOrDigit(c)) From 9a10f625d240234677747c484093e6ddd5704962 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:39:05 +0000 Subject: [PATCH 05/10] Improve consistency: use TryGetValue and SanitizePropertyName throughout Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> --- .../BackEnd/Components/Logging/ProjectTelemetry.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs index 94f96781628..420d39a19fb 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -119,11 +119,8 @@ public void TrackTaskSubclassing(Type taskType, bool isMicrosoftOwned) // 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; @@ -264,8 +261,8 @@ private Dictionary GetMSBuildTaskSubclassProperties() // Add each Microsoft task name with its non-sealed subclass usage count foreach (var kvp in _msbuildTaskSubclassUsage) { - // Use a sanitized property name (replace dots with underscores for telemetry) - string propertyName = kvp.Key.Replace(".", "_"); + // Use the same sanitization logic as custom task factories for consistency + string propertyName = SanitizePropertyName(kvp.Key); properties[propertyName] = kvp.Value.ToString(CultureInfo.InvariantCulture); } From 8a33e5b1746c80f9f617ba86f2dd7e2957d9a7c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:43:37 +0000 Subject: [PATCH 06/10] Add null safety to SanitizePropertyName and comprehensive edge case tests Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> --- .../BackEnd/ProjectTelemetry_Tests.cs | 25 +++++++++++++++++++ .../Components/Logging/ProjectTelemetry.cs | 5 ++++ 2 files changed, 30 insertions(+) diff --git a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs index 1605950e02f..44f1a33990e 100644 --- a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs +++ b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs @@ -300,5 +300,30 @@ public void AddTaskExecution_SanitizesCustomTaskFactoryNames() properties.ShouldContainKey("Another_Factory"); properties["Another_Factory"].ShouldBe("1"); } + + /// + /// Test that sanitization handles edge cases correctly + /// + [Fact] + public void SanitizePropertyName_HandlesEdgeCases() + { + // Use reflection to access the private SanitizePropertyName method + var method = typeof(ProjectTelemetry).GetMethod("SanitizePropertyName", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + // Test null input +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type + var resultNull = (string)method!.Invoke(null, [null])!; +#pragma warning restore CS8625 + resultNull.ShouldBe(string.Empty); + + // Test empty string + var resultEmpty = (string)method.Invoke(null, [string.Empty])!; + resultEmpty.ShouldBe(string.Empty); + + // Test string with only special characters + var resultSpecial = (string)method.Invoke(null, ["...--- "])!; + resultSpecial.ShouldBe("_________"); + } } } diff --git a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs index 420d39a19fb..c2415d47de9 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -291,6 +291,11 @@ private Dictionary GetCustomTaskFactoryProperties() /// private static string SanitizePropertyName(string name) { + if (string.IsNullOrEmpty(name)) + { + return string.Empty; + } + // Replace dots with underscores and remove other special characters // Keep alphanumeric characters and underscores only var sanitized = new StringBuilder(name.Length); From 3584c4c0385f0a7934cb645a1c66a15cc619700e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:48:09 +0000 Subject: [PATCH 07/10] Final polish: improve compatibility and add clarifying comments Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> --- src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs | 6 +++--- src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs index 44f1a33990e..abf503d1f2b 100644 --- a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs +++ b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs @@ -313,16 +313,16 @@ public void SanitizePropertyName_HandlesEdgeCases() // Test null input #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type - var resultNull = (string)method!.Invoke(null, [null])!; + var resultNull = (string)method!.Invoke(null, new object[] { null })!; #pragma warning restore CS8625 resultNull.ShouldBe(string.Empty); // Test empty string - var resultEmpty = (string)method.Invoke(null, [string.Empty])!; + var resultEmpty = (string)method.Invoke(null, new object[] { string.Empty })!; resultEmpty.ShouldBe(string.Empty); // Test string with only special characters - var resultSpecial = (string)method.Invoke(null, ["...--- "])!; + var resultSpecial = (string)method.Invoke(null, new object[] { "...--- " })!; resultSpecial.ShouldBe("_________"); } } diff --git a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs index c2415d47de9..aaf33dde368 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -298,6 +298,7 @@ private static string SanitizePropertyName(string name) // Replace dots with underscores and remove other special characters // Keep alphanumeric characters and underscores only + // Use same length as input since we're not adding characters, only replacing or skipping var sanitized = new StringBuilder(name.Length); foreach (char c in name) { @@ -309,7 +310,7 @@ private static string SanitizePropertyName(string name) { sanitized.Append('_'); } - // Skip other special characters + // Skip other special characters (reduces length) } return sanitized.ToString(); } From c82429261a093dbe9cc68f4b44fa13c6c32a6c4e Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 9 Jan 2026 16:36:33 +0100 Subject: [PATCH 08/10] add collection of factory name and runtime used in tasks --- .../Telemetry/Telemetry_Tests.cs | 88 +++++++++++ .../Components/Logging/ProjectTelemetry.cs | 139 ++++-------------- .../RequestBuilder/RequestBuilder.cs | 10 +- .../TelemetryInfra/ITelemetryForwarder.cs | 4 +- .../InternalTelemetryConsumingLogger.cs | 3 + .../TelemetryForwarderProvider.cs | 7 +- .../WorkerNodeTelemetryEventArgs_Tests.cs | 6 +- src/Framework/Telemetry/TaskExecutionStats.cs | 36 ++++- src/Framework/Telemetry/TelemetryDataUtils.cs | 33 ++++- .../Telemetry/WorkerNodeTelemetryData.cs | 13 +- .../Telemetry/WorkerNodeTelemetryEventArgs.cs | 20 ++- src/Framework/TelemetryEventArgs.cs | 1 + src/Tasks.UnitTests/TelemetryTaskTests.cs | 2 - 13 files changed, 218 insertions(+), 144 deletions(-) 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 aaf33dde368..877e8053b3d 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Text; using Microsoft.Build.Framework; namespace Microsoft.Build.BackEnd.Logging @@ -28,7 +27,6 @@ internal class ProjectTelemetry private const string TaskFactoryEventName = "build/tasks/taskfactory"; private const string TasksEventName = "build/tasks"; private const string MSBuildTaskSubclassedEventName = "build/tasks/msbuild-subclassed"; - private const string CustomTaskFactoryEventName = "build/tasks/custom-taskfactory"; private int _assemblyTaskFactoryTasksExecutedCount = 0; private int _intrinsicTaskFactoryTasksExecutedCount = 0; @@ -43,10 +41,6 @@ internal class ProjectTelemetry // Maps Microsoft task names to counts of their non-sealed usage private readonly Dictionary _msbuildTaskSubclassUsage = new(); - // Telemetry for custom (non-MSBuild) task factory usage - // Maps custom task factory type names to execution counts - private readonly Dictionary _customTaskFactoryUsage = new(); - /// /// Adds a task execution to the telemetry data. /// @@ -80,13 +74,7 @@ public void AddTaskExecution(string taskFactoryTypeName, bool isTaskHost) break; default: - // Track custom (non-MSBuild) task factories individually _customTaskFactoryTasksExecutedCount++; - if (!string.IsNullOrEmpty(taskFactoryTypeName)) - { - _customTaskFactoryUsage.TryGetValue(taskFactoryTypeName, out int count); - _customTaskFactoryUsage[taskFactoryTypeName] = count + 1; - } break; } } @@ -111,8 +99,8 @@ 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 @@ -158,12 +146,6 @@ public void LogProjectTelemetry(ILoggingService loggingService, BuildEventContex { loggingService.LogTelemetry(buildEventContext, MSBuildTaskSubclassedEventName, msbuildTaskSubclassProperties); } - - Dictionary customTaskFactoryProperties = GetCustomTaskFactoryProperties(); - if (customTaskFactoryProperties.Count > 0) - { - loggingService.LogTelemetry(buildEventContext, CustomTaskFactoryEventName, customTaskFactoryProperties); - } } catch { @@ -177,7 +159,7 @@ public void LogProjectTelemetry(ILoggingService loggingService, BuildEventContex Clean(); } } - + private void Clean() { _assemblyTaskFactoryTasksExecutedCount = 0; @@ -190,42 +172,26 @@ private void Clean() _taskHostTasksExecutedCount = 0; _msbuildTaskSubclassUsage.Clear(); - _customTaskFactoryUsage.Clear(); + } + + private static void AddIfNotEmpty(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); - } + AddIfNotEmpty(properties, "AssemblyTaskFactoryTasksExecutedCount", _assemblyTaskFactoryTasksExecutedCount); + AddIfNotEmpty(properties, "IntrinsicTaskFactoryTasksExecutedCount", _intrinsicTaskFactoryTasksExecutedCount); + AddIfNotEmpty(properties, "CodeTaskFactoryTasksExecutedCount", _codeTaskFactoryTasksExecutedCount); + AddIfNotEmpty(properties, "RoslynCodeTaskFactoryTasksExecutedCount", _roslynCodeTaskFactoryTasksExecutedCount); + AddIfNotEmpty(properties, "XamlTaskFactoryTasksExecutedCount", _xamlTaskFactoryTasksExecutedCount); + AddIfNotEmpty(properties, "CustomTaskFactoryTasksExecutedCount", _customTaskFactoryTasksExecutedCount); return properties; } @@ -233,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); - } + + AddIfNotEmpty(properties, "TasksExecutedCount", totalTasksExecuted); + AddIfNotEmpty(properties, "TaskHostTasksExecutedCount", _taskHostTasksExecutedCount); return properties; } @@ -261,58 +220,12 @@ private Dictionary GetMSBuildTaskSubclassProperties() // Add each Microsoft task name with its non-sealed subclass usage count foreach (var kvp in _msbuildTaskSubclassUsage) { - // Use the same sanitization logic as custom task factories for consistency - string propertyName = SanitizePropertyName(kvp.Key); - properties[propertyName] = kvp.Value.ToString(CultureInfo.InvariantCulture); - } - - return properties; - } - - private Dictionary GetCustomTaskFactoryProperties() - { - Dictionary properties = new(); - - // Add each custom task factory type name with its usage count - foreach (var kvp in _customTaskFactoryUsage) - { - // Sanitize property name for telemetry: replace dots with underscores - // and remove any other characters that might be problematic - string propertyName = SanitizePropertyName(kvp.Key); + // Use a sanitized property name (replace dots with underscores for telemetry) + string propertyName = kvp.Key.Replace(".", "_"); properties[propertyName] = kvp.Value.ToString(CultureInfo.InvariantCulture); } return properties; } - - /// - /// Sanitizes a string to make it suitable for use as a telemetry property name. - /// Replaces dots with underscores and removes other potentially problematic characters. - /// - private static string SanitizePropertyName(string name) - { - if (string.IsNullOrEmpty(name)) - { - return string.Empty; - } - - // Replace dots with underscores and remove other special characters - // Keep alphanumeric characters and underscores only - // Use same length as input since we're not adding characters, only replacing or skipping - var sanitized = new StringBuilder(name.Length); - foreach (char c in name) - { - if (char.IsLetterOrDigit(c)) - { - sanitized.Append(c); - } - else if (c == '.' || c == '-' || c == ' ') - { - sanitized.Append('_'); - } - // Skip other special characters (reduces length) - } - return sanitized.ToString(); - } } } 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..60bc09c282c 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,18 @@ 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", + }; + /// /// Transforms collected telemetry data to format recognized by the telemetry infrastructure. /// @@ -76,6 +89,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 +97,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 +159,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..ff3d2ec4e5f 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,12 +27,12 @@ public void Add(IWorkerNodeTelemetryData other) } } - public void AddTask(TaskOrTargetTelemetryKey task, TimeSpan cumulativeExectionTime, int executionsCount, long totalMemoryConsumption) + public void AddTask(TaskOrTargetTelemetryKey task, TimeSpan cumulativeExectionTime, int executionsCount, long totalMemoryConsumption, string? factoryName, string? taskHostRuntime) { TaskExecutionStats? taskExecutionStats; if (!TasksExecutionData.TryGetValue(task, out taskExecutionStats)) { - taskExecutionStats = new(cumulativeExectionTime, executionsCount, totalMemoryConsumption); + taskExecutionStats = new(cumulativeExectionTime, executionsCount, totalMemoryConsumption, factoryName, taskHostRuntime); TasksExecutionData[task] = taskExecutionStats; } else @@ -40,6 +40,8 @@ public void AddTask(TaskOrTargetTelemetryKey task, TimeSpan cumulativeExectionTi taskExecutionStats.CumulativeExecutionTime += cumulativeExectionTime; 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([], new Dictionary()) { } 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); } } From dfd217ef1abc17dd5b094f1974497a740da62631 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 9 Jan 2026 16:37:30 +0100 Subject: [PATCH 09/10] undo extra change --- .../BackEnd/ProjectTelemetry_Tests.cs | 181 ++---------------- 1 file changed, 18 insertions(+), 163 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs index abf503d1f2b..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,17 +112,7 @@ public void TrackTaskSubclassing_HandlesNull() /// private System.Collections.Generic.Dictionary GetMSBuildTaskSubclassProperties(ProjectTelemetry telemetry) { - var method = typeof(ProjectTelemetry).GetMethod("GetMSBuildTaskSubclassProperties", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - return (System.Collections.Generic.Dictionary)method!.Invoke(telemetry, null)!; - } - - /// - /// Helper method to get custom task factory properties from telemetry using reflection - /// - private System.Collections.Generic.Dictionary GetCustomTaskFactoryProperties(ProjectTelemetry telemetry) - { - var method = typeof(ProjectTelemetry).GetMethod("GetCustomTaskFactoryProperties", + var method = typeof(ProjectTelemetry).GetMethod("GetMSBuildTaskSubclassProperties", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); return (System.Collections.Generic.Dictionary)method!.Invoke(telemetry, null)!; } @@ -179,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); @@ -187,143 +177,8 @@ public void MSBuildTaskTelemetry_IsLoggedDuringBuild() // Build the project var result = project.Build(); - - result.ShouldBeTrue(); - } - - /// - /// Test that AddTaskExecution tracks custom task factory usage individually - /// - [Fact] - public void AddTaskExecution_TracksCustomTaskFactoryIndividually() - { - var telemetry = new ProjectTelemetry(); - - // Add executions from custom task factories - telemetry.AddTaskExecution("CustomFactory.MyTaskFactory", isTaskHost: false); - telemetry.AddTaskExecution("AnotherCustomFactory", isTaskHost: false); - telemetry.AddTaskExecution("CustomFactory.MyTaskFactory", isTaskHost: false); - - var properties = GetCustomTaskFactoryProperties(telemetry); - - // Should track each custom factory separately - properties.Count.ShouldBe(2); - properties.ShouldContainKey("CustomFactory_MyTaskFactory"); - properties["CustomFactory_MyTaskFactory"].ShouldBe("2"); - properties.ShouldContainKey("AnotherCustomFactory"); - properties["AnotherCustomFactory"].ShouldBe("1"); - } - - /// - /// Test that AddTaskExecution does not track built-in factories as custom - /// - [Fact] - public void AddTaskExecution_DoesNotTrackBuiltInFactoriesAsCustom() - { - var telemetry = new ProjectTelemetry(); - - // Add executions from built-in task factories - telemetry.AddTaskExecution("Microsoft.Build.BackEnd.AssemblyTaskFactory", isTaskHost: false); - telemetry.AddTaskExecution("Microsoft.Build.Tasks.CodeTaskFactory", isTaskHost: false); - telemetry.AddTaskExecution("Microsoft.Build.Tasks.RoslynCodeTaskFactory", isTaskHost: false); - - var properties = GetCustomTaskFactoryProperties(telemetry); - - // Should not track any custom factories - properties.Count.ShouldBe(0); - } - - /// - /// Test that AddTaskExecution tracks CodeTaskFactory separately - /// - [Fact] - public void AddTaskExecution_TracksCodeTaskFactorySeparately() - { - var telemetry = new ProjectTelemetry(); - - // Add execution from CodeTaskFactory - telemetry.AddTaskExecution("Microsoft.Build.Tasks.CodeTaskFactory", isTaskHost: false); - telemetry.AddTaskExecution("Microsoft.Build.Tasks.CodeTaskFactory", isTaskHost: false); - - // Use reflection to get task factory properties - var method = typeof(ProjectTelemetry).GetMethod("GetTaskFactoryProperties", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var properties = (System.Collections.Generic.Dictionary)method!.Invoke(telemetry, null)!; - - // Should track CodeTaskFactory separately from custom factories - properties.ShouldContainKey("CodeTaskFactoryTasksExecutedCount"); - properties["CodeTaskFactoryTasksExecutedCount"].ShouldBe("2"); - - // Should not be in custom factory properties - var customProperties = GetCustomTaskFactoryProperties(telemetry); - customProperties.Count.ShouldBe(0); - } - - /// - /// Test that AddTaskExecution handles null or empty factory names gracefully - /// - [Fact] - public void AddTaskExecution_HandlesNullFactoryNameGracefully() - { - var telemetry = new ProjectTelemetry(); - - // Add execution with null or empty factory name -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type - telemetry.AddTaskExecution(null, isTaskHost: false); -#pragma warning restore CS8625 - telemetry.AddTaskExecution(string.Empty, isTaskHost: false); - - var properties = GetCustomTaskFactoryProperties(telemetry); - - // Should handle null/empty gracefully without adding entries - properties.Count.ShouldBe(0); - } - - /// - /// Test that custom task factory names with special characters are properly sanitized - /// - [Fact] - public void AddTaskExecution_SanitizesCustomTaskFactoryNames() - { - var telemetry = new ProjectTelemetry(); - - // Add executions from custom task factories with special characters - telemetry.AddTaskExecution("My.Custom-Factory Task", isTaskHost: false); - telemetry.AddTaskExecution("Another.Factory", isTaskHost: false); - - var properties = GetCustomTaskFactoryProperties(telemetry); - - // Should sanitize special characters to underscores - properties.Count.ShouldBe(2); - properties.ShouldContainKey("My_Custom_Factory_Task"); - properties["My_Custom_Factory_Task"].ShouldBe("1"); - properties.ShouldContainKey("Another_Factory"); - properties["Another_Factory"].ShouldBe("1"); - } - /// - /// Test that sanitization handles edge cases correctly - /// - [Fact] - public void SanitizePropertyName_HandlesEdgeCases() - { - // Use reflection to access the private SanitizePropertyName method - var method = typeof(ProjectTelemetry).GetMethod("SanitizePropertyName", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - - // Test null input -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type - var resultNull = (string)method!.Invoke(null, new object[] { null })!; -#pragma warning restore CS8625 - resultNull.ShouldBe(string.Empty); - - // Test empty string - var resultEmpty = (string)method.Invoke(null, new object[] { string.Empty })!; - resultEmpty.ShouldBe(string.Empty); - - // Test string with only special characters - var resultSpecial = (string)method.Invoke(null, new object[] { "...--- " })!; - resultSpecial.ShouldBe("_________"); + result.ShouldBeTrue(); } } } From f64e7d2438a6c193bd8217228e617628fc30ff15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 09:18:59 +0000 Subject: [PATCH 10/10] Address code review feedback: rename method, fix typo, add IntrinsicTaskFactory, use consistent collection expressions Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> --- .../Components/Logging/ProjectTelemetry.cs | 18 +++++++++--------- src/Framework/Telemetry/TelemetryDataUtils.cs | 1 + .../Telemetry/WorkerNodeTelemetryData.cs | 8 ++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs index 877e8053b3d..37bbc129150 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs @@ -174,7 +174,7 @@ private void Clean() _msbuildTaskSubclassUsage.Clear(); } - private static void AddIfNotEmpty(Dictionary properties, string propertyName, int count) + private static void AddCountIfNonZero(Dictionary properties, string propertyName, int count) { if (count > 0) { @@ -186,12 +186,12 @@ private Dictionary GetTaskFactoryProperties() { Dictionary properties = new(); - AddIfNotEmpty(properties, "AssemblyTaskFactoryTasksExecutedCount", _assemblyTaskFactoryTasksExecutedCount); - AddIfNotEmpty(properties, "IntrinsicTaskFactoryTasksExecutedCount", _intrinsicTaskFactoryTasksExecutedCount); - AddIfNotEmpty(properties, "CodeTaskFactoryTasksExecutedCount", _codeTaskFactoryTasksExecutedCount); - AddIfNotEmpty(properties, "RoslynCodeTaskFactoryTasksExecutedCount", _roslynCodeTaskFactoryTasksExecutedCount); - AddIfNotEmpty(properties, "XamlTaskFactoryTasksExecutedCount", _xamlTaskFactoryTasksExecutedCount); - AddIfNotEmpty(properties, "CustomTaskFactoryTasksExecutedCount", _customTaskFactoryTasksExecutedCount); + 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; } @@ -207,8 +207,8 @@ private Dictionary GetTaskProperties() _xamlTaskFactoryTasksExecutedCount + _customTaskFactoryTasksExecutedCount; - AddIfNotEmpty(properties, "TasksExecutedCount", totalTasksExecuted); - AddIfNotEmpty(properties, "TaskHostTasksExecutedCount", _taskHostTasksExecutedCount); + AddCountIfNonZero(properties, "TasksExecutedCount", totalTasksExecuted); + AddCountIfNonZero(properties, "TaskHostTasksExecutedCount", _taskHostTasksExecutedCount); return properties; } diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index 60bc09c282c..66a56c8f497 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -21,6 +21,7 @@ internal static class TelemetryDataUtils "CodeTaskFactory", "RoslynCodeTaskFactory", "XamlTaskFactory", + "IntrinsicTaskFactory", }; /// diff --git a/src/Framework/Telemetry/WorkerNodeTelemetryData.cs b/src/Framework/Telemetry/WorkerNodeTelemetryData.cs index ff3d2ec4e5f..ae143f52ed4 100644 --- a/src/Framework/Telemetry/WorkerNodeTelemetryData.cs +++ b/src/Framework/Telemetry/WorkerNodeTelemetryData.cs @@ -27,17 +27,17 @@ public void Add(IWorkerNodeTelemetryData other) } } - public void AddTask(TaskOrTargetTelemetryKey task, TimeSpan cumulativeExectionTime, int executionsCount, long totalMemoryConsumption, string? factoryName, string? taskHostRuntime) + 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, factoryName, taskHostRuntime); + 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; @@ -52,7 +52,7 @@ public void AddTarget(TaskOrTargetTelemetryKey target, bool wasExecuted) wasExecuted || (TargetsExecutionData.TryGetValue(target, out bool wasAlreadyExecuted) && wasAlreadyExecuted); } - public WorkerNodeTelemetryData() : this([], new Dictionary()) { } + public WorkerNodeTelemetryData() : this([], []) { } public Dictionary TasksExecutionData { get; }