Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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();

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

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

AddCountIfNonZero(properties, "TasksExecutedCount", totalTasksExecuted);
AddCountIfNonZero(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