Skip to content
Merged
36 changes: 18 additions & 18 deletions src/Build.UnitTests/BackEnd/ProjectTelemetry_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand All @@ -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);
}
Expand All @@ -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");
Expand All @@ -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);
}

Expand All @@ -112,7 +112,7 @@ public void TrackTaskSubclassing_HandlesNull()
/// </summary>
private System.Collections.Generic.Dictionary<string, string> 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<string, string>)method!.Invoke(telemetry, null)!;
}
Expand Down Expand Up @@ -169,15 +169,15 @@ public void MSBuildTaskTelemetry_IsLoggedDuringBuild()

var events = new System.Collections.Generic.List<BuildEventArgs>();
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);
var project = new Project(xmlReader, null, null, projectCollection);

// Build the project
var result = project.Build();

result.ShouldBeTrue();
}
}
Expand Down
88 changes: 88 additions & 0 deletions src/Build.UnitTests/Telemetry/Telemetry_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<Project>
<UsingTask
TaskName="InlineTask01"
TaskFactory="RoslynCodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
<ParameterGroup />
<Task>
<Code Type="Fragment" Language="cs">
Log.LogMessage(MessageImportance.Low, "Hello from inline task!");
</Code>
</Task>
</UsingTask>
<Target Name="Build">
<Message Text="Hello World"/>
<InlineTask01 />
</Target>
</Project>
""";

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<TaskOrTargetTelemetryKey, TaskExecutionStats>
{
{ 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<TaskOrTargetTelemetryKey, bool>();
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<TaskDetailInfo>;
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]
Expand Down Expand Up @@ -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;
Expand Down
77 changes: 26 additions & 51 deletions src/Build/BackEnd/Components/Logging/ProjectTelemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -162,7 +159,7 @@ public void LogProjectTelemetry(ILoggingService loggingService, BuildEventContex
Clean();
}
}

private void Clean()
{
_assemblyTaskFactoryTasksExecutedCount = 0;
Expand All @@ -177,63 +174,41 @@ private void Clean()
_msbuildTaskSubclassUsage.Clear();
}

private static void AddIfNotEmpty(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();

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;
}

private Dictionary<string, string> GetTaskProperties()
{
Dictionary<string, string> 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;
}
Expand Down
10 changes: 6 additions & 4 deletions src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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);

/// <summary>
/// Saves the current operating environment (working directory and environment variables)
Expand Down
4 changes: 3 additions & 1 deletion src/Build/TelemetryInfra/ITelemetryForwarder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ void AddTask(
short executionsCount,
long totalMemoryConsumed,
bool isCustom,
bool isFromNugetCache);
bool isFromNugetCache,
string? taskFactoryName,
string? taskHostRuntime);

/// <summary>
/// Add info about target execution to the telemetry.
Expand Down
3 changes: 3 additions & 0 deletions src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,22 @@ private void FlushDataIntoConsoleIfRequested()
{
Console.WriteLine($"{target.Key} : {target.Value}");
}

Console.WriteLine("==========================================");
Console.WriteLine($"Tasks: ({_workerNodeTelemetryData.TasksExecutionData.Count})");
Console.WriteLine("Custom tasks:");
foreach (var task in _workerNodeTelemetryData.TasksExecutionData.Where(t => t.Key.IsCustom))
{
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))
Expand Down
Loading
Loading