| File: BackEnd\Components\Logging\ProjectTelemetry.cs | Web Access |
| Project: src\msbuild\src\Build\Microsoft.Build.csproj (Microsoft.Build) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Globalization; using Microsoft.Build.Framework; namespace Microsoft.Build.BackEnd.Logging { /// <summary> /// Tracks telemetry data for a project build. /// </summary> internal class ProjectTelemetry { // We do not have dependency on Microsoft.Build.Tasks assembly, so using hard-coded type names private const string AssemblyTaskFactoryTypeName = "Microsoft.Build.BackEnd.AssemblyTaskFactory"; private const string IntrinsicTaskFactoryTypeName = "Microsoft.Build.BackEnd.IntrinsicTaskFactory"; private const string CodeTaskFactoryTypeName = "Microsoft.Build.Tasks.CodeTaskFactory"; private const string RoslynCodeTaskFactoryTypeName = "Microsoft.Build.Tasks.RoslynCodeTaskFactory"; private const string XamlTaskFactoryTypeName = "Microsoft.Build.Tasks.XamlTaskFactory"; // Important Note: these two telemetry events are not logged directly. // SDK aggregates them per build in https://github.com/dotnet/sdk/blob/main/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs. // Aggregation logic is very basic. Only integer properties are aggregated by summing values. Non-integer properties are ignored. // This means that if we ever add logging non-integer properties for these events, they will not be included in the telemetry. private const string TaskFactoryEventName = "build/tasks/taskfactory"; private const string TasksEventName = "build/tasks"; private const string MSBuildTaskSubclassedEventName = "build/tasks/msbuild-subclassed"; private int _assemblyTaskFactoryTasksExecutedCount = 0; private int _intrinsicTaskFactoryTasksExecutedCount = 0; private int _codeTaskFactoryTasksExecutedCount = 0; private int _roslynCodeTaskFactoryTasksExecutedCount = 0; private int _xamlTaskFactoryTasksExecutedCount = 0; private int _customTaskFactoryTasksExecutedCount = 0; private int _taskHostTasksExecutedCount = 0; // Telemetry for non-sealed subclasses of Microsoft-owned MSBuild tasks // Maps Microsoft task names to counts of their non-sealed usage private readonly Dictionary<string, int> _msbuildTaskSubclassUsage = new(); /// <summary> /// Adds a task execution to the telemetry data. /// </summary> public void AddTaskExecution(string taskFactoryTypeName, bool isTaskHost) { if (isTaskHost) { _taskHostTasksExecutedCount++; } switch (taskFactoryTypeName) { case AssemblyTaskFactoryTypeName: _assemblyTaskFactoryTasksExecutedCount++; break; case IntrinsicTaskFactoryTypeName: _intrinsicTaskFactoryTasksExecutedCount++; break; case CodeTaskFactoryTypeName: _codeTaskFactoryTasksExecutedCount++; break; case RoslynCodeTaskFactoryTypeName: _roslynCodeTaskFactoryTasksExecutedCount++; break; case XamlTaskFactoryTypeName: _xamlTaskFactoryTasksExecutedCount++; break; default: _customTaskFactoryTasksExecutedCount++; break; } } /// <summary> /// Tracks subclasses of Microsoft-owned MSBuild tasks. /// If the task is a subclass of a Microsoft-owned task, increments the usage count for that base task. /// </summary> /// <param name="taskType">The type of the task being loaded.</param> /// <param name="isMicrosoftOwned">Whether the task itself is Microsoft-owned.</param> public void TrackTaskSubclassing(Type taskType, bool isMicrosoftOwned) { if (taskType == null) { return; } // Walk the inheritance hierarchy to find Microsoft-owned base tasks Type? baseType = taskType.BaseType; while (baseType != null) { // 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.", StringComparison.Ordinal) || baseTypeName.StartsWith("Microsoft.Build.Utilities.", StringComparison.Ordinal))) { // 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) { _msbuildTaskSubclassUsage.TryGetValue(baseTypeName, out int count); _msbuildTaskSubclassUsage[baseTypeName] = count + 1; } // Stop at the first Microsoft-owned base class we find break; } baseType = baseType.BaseType; } } /// <summary> /// Logs telemetry data for a project /// </summary> public void LogProjectTelemetry(ILoggingService loggingService, BuildEventContext buildEventContext) { if (loggingService == null) { return; } try { Dictionary<string, string> taskFactoryProperties = GetTaskFactoryProperties(); if (taskFactoryProperties.Count > 0) { loggingService.LogTelemetry(buildEventContext, TaskFactoryEventName, taskFactoryProperties); } Dictionary<string, string> taskTotalProperties = GetTaskProperties(); if (taskTotalProperties.Count > 0) { loggingService.LogTelemetry(buildEventContext, TasksEventName, taskTotalProperties); } Dictionary<string, string> msbuildTaskSubclassProperties = GetMSBuildTaskSubclassProperties(); if (msbuildTaskSubclassProperties.Count > 0) { loggingService.LogTelemetry(buildEventContext, MSBuildTaskSubclassedEventName, msbuildTaskSubclassProperties); } } catch { // Ignore telemetry logging errors to avoid breaking builds } finally { // Reset counts after logging. // ProjectLoggingContext context and, as a result, this class should not be reused between projects builds, // however it is better to defensively clean up the stats if it ever happens in future. Clean(); } } private void Clean() { _assemblyTaskFactoryTasksExecutedCount = 0; _intrinsicTaskFactoryTasksExecutedCount = 0; _codeTaskFactoryTasksExecutedCount = 0; _roslynCodeTaskFactoryTasksExecutedCount = 0; _xamlTaskFactoryTasksExecutedCount = 0; _customTaskFactoryTasksExecutedCount = 0; _taskHostTasksExecutedCount = 0; _msbuildTaskSubclassUsage.Clear(); } private static void AddCountIfNonZero(Dictionary<string, string> properties, string propertyName, int count) { if (count > 0) { properties[propertyName] = count.ToString(CultureInfo.InvariantCulture); } } private Dictionary<string, string> GetTaskFactoryProperties() { Dictionary<string, string> properties = new(); 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; } private Dictionary<string, string> GetTaskProperties() { Dictionary<string, string> properties = new(); var totalTasksExecuted = _assemblyTaskFactoryTasksExecutedCount + _intrinsicTaskFactoryTasksExecutedCount + _codeTaskFactoryTasksExecutedCount + _roslynCodeTaskFactoryTasksExecutedCount + _xamlTaskFactoryTasksExecutedCount + _customTaskFactoryTasksExecutedCount; AddCountIfNonZero(properties, "TasksExecutedCount", totalTasksExecuted); AddCountIfNonZero(properties, "TaskHostTasksExecutedCount", _taskHostTasksExecutedCount); return properties; } private Dictionary<string, string> GetMSBuildTaskSubclassProperties() { Dictionary<string, string> properties = new(); // 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(".", "_"); properties[propertyName] = kvp.Value.ToString(CultureInfo.InvariantCulture); } return properties; } } }