Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
536cd50
[DotnetTrace] Update CLREventKeywords
mdh1418 Sep 16, 2025
49a875b
[DotnetTrace] Move MergeProfileAndProviders to Extensions
mdh1418 Sep 17, 2025
b4787fc
[DotnetTrace] Rename Extensions to ProviderUtils
mdh1418 Sep 17, 2025
c921869
[DotnetTrace] Add provider unifying helper
mdh1418 Sep 17, 2025
e1c15c7
[DotnetTrace] Move shared options to CommonOptions
mdh1418 Sep 17, 2025
b4112b5
[DotnetTrace] Add collect-linux skeleton
mdh1418 Sep 17, 2025
e5f3975
[DotnetTrace][CollectLinux] Start record-trace
mdh1418 Sep 17, 2025
677e54e
[DotnetTrace][CollectLinux] Build record-trace args
mdh1418 Sep 17, 2025
31186a8
[DotnetTrace] Update profiles
mdh1418 Sep 17, 2025
44593bb
[DotnetTrace] Update collect to new provider unifier
mdh1418 Sep 17, 2025
b190d73
[DotnetCounters] Remove Extensions reference
mdh1418 Sep 19, 2025
9f1ef6d
[DotnetTrace] Update tests
mdh1418 Sep 19, 2025
c8e53ea
Address Feedback
mdh1418 Sep 19, 2025
590a203
[DotnetTrace] Print profile effects and clrevents ignore warning
mdh1418 Sep 19, 2025
8e2453f
[DotnetTrace] Print PerfEvents and include in default condition
mdh1418 Sep 19, 2025
573d769
[DotnetTrace] Update OneCollect package with FFI
mdh1418 Sep 30, 2025
1b3b583
Fix dotnet-trace build for repo root build
mdh1418 Oct 3, 2025
e6d9de9
Add Linux events table
mdh1418 Oct 6, 2025
676efa9
Add Progress status and Address feedback
mdh1418 Oct 6, 2025
d90d1be
Merge remote-tracking branch 'upstream/main' into dotnet_trace_collec…
mdh1418 Oct 6, 2025
096f325
Update collect functional tests for ProviderUtils refactor
mdh1418 Oct 6, 2025
1f75125
[DotnetTrace] Add Collect Linux Functional Tests
mdh1418 Oct 6, 2025
55bc84a
Adjust CollectLinux tests for non-Linux platforms
mdh1418 Oct 7, 2025
4c1a361
[DotnetTrace][CollectLinux] Remove process specifier
mdh1418 Oct 7, 2025
208086f
Adjust functional tests and add failure cases
mdh1418 Oct 8, 2025
40c5905
[DotnetTrace][CollectLinux] Add ProgressStatus and output file to tests
mdh1418 Oct 8, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Diagnostics.Tools.Common;
using Microsoft.Internal.Common.Utils;

namespace Microsoft.Diagnostics.Tools.Trace
{
internal static partial class CollectLinuxCommandHandler
internal partial class CollectLinuxCommandHandler
{
private static bool s_stopTracing;
private static Stopwatch s_stopwatch = new();
private static LineRewriter s_rewriter = new() { LineToClear = Console.CursorTop - 1 };
private static LineRewriter s_rewriter;
private static bool s_printingStatus;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If testing caused multiple instances of CollectLinuxCommandHandler to run in parallel, would we want those instances to be sharing any of these static fields? I'm guessing we'd want all these fields to be instanced too.

Copy link
Member Author

@mdh1418 mdh1418 Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to non-static. From some learning, I'm seeing that parallelization occurs at the test class level, so it doesn't seem like the theory members would run in parallel.


internal sealed record CollectLinuxArgs(
Expand All @@ -34,11 +35,17 @@ internal sealed record CollectLinuxArgs(
string Name,
int ProcessId);

public CollectLinuxCommandHandler()
{
Console = new DefaultConsole(false);
s_rewriter = new LineRewriter(Console) { LineToClear = Console.CursorTop - 1 };
}

/// <summary>
/// Collects diagnostic traces using perf_events, a Linux OS technology. collect-linux requires admin privileges to capture kernel- and user-mode events, and by default, captures events from all processes.
/// This Linux-only command includes the same .NET events as dotnet-trace collect, and it uses the kernel’s user_events mechanism to emit .NET events as perf events, enabling unification of user-space .NET events with kernel-space system events.
/// </summary>
private static int CollectLinux(CollectLinuxArgs args)
internal int CollectLinux(CollectLinuxArgs args)
{
if (!OperatingSystem.IsLinux())
{
Expand Down Expand Up @@ -71,7 +78,7 @@ private static int CollectLinux(CollectLinuxArgs args)
durationTimer.Start();
}
s_stopwatch.Start();
ret = RunRecordTrace(command, (UIntPtr)command.Length, OutputHandler);
ret = RecordTraceInvoker(command, (UIntPtr)command.Length, OutputHandler);
}
finally
{
Expand Down Expand Up @@ -111,8 +118,9 @@ public static Command CollectLinuxCommand()
string providersValue = parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty;
string perfEventsValue = parseResult.GetValue(PerfEventsOption) ?? string.Empty;
string profilesValue = parseResult.GetValue(CommonOptions.ProfileOption) ?? string.Empty;
CollectLinuxCommandHandler handler = new();

int rc = CollectLinux(new CollectLinuxArgs(
int rc = handler.CollectLinux(new CollectLinuxArgs(
Ct: ct,
Providers: providersValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
ClrEventLevel: parseResult.GetValue(CommonOptions.CLREventLevelOption) ?? string.Empty,
Expand All @@ -129,7 +137,7 @@ public static Command CollectLinuxCommand()
return collectLinuxCommand;
}

private static byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath)
private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath)
{
scriptPath = null;
List<string> recordTraceArgs = new();
Expand All @@ -156,7 +164,7 @@ private static byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scr
}

StringBuilder scriptBuilder = new();
List<EventPipeProvider> providerCollection = ProviderUtils.ComputeProviderConfig(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true, "collect-linux");
List<EventPipeProvider> providerCollection = ProviderUtils.ComputeProviderConfig(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true, "collect-linux", Console);
foreach (EventPipeProvider provider in providerCollection)
{
string providerName = provider.Name;
Expand Down Expand Up @@ -237,7 +245,7 @@ private static string ResolveOutputPath(FileInfo output, int processId)
return $"trace_{now:yyyyMMdd}_{now:HHmmss}.nettrace";
}

private static int OutputHandler(uint type, IntPtr data, UIntPtr dataLen)
private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen)
{
OutputType ot = (OutputType)type;
if (dataLen != UIntPtr.Zero && (ulong)dataLen <= int.MaxValue)
Expand Down Expand Up @@ -295,7 +303,7 @@ private enum OutputType : uint
}

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int recordTraceCallback(
internal delegate int recordTraceCallback(
[In] uint type,
[In] IntPtr data,
[In] UIntPtr dataLen);
Expand All @@ -305,5 +313,10 @@ private static partial int RunRecordTrace(
byte[] command,
UIntPtr commandLen,
recordTraceCallback callback);

#region testing seams
internal Func<byte[], UIntPtr, recordTraceCallback, int> RecordTraceInvoker { get; set; } = RunRecordTrace;
internal IConsole Console { get; set; }
#endregion
}
}
200 changes: 200 additions & 0 deletions src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// 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.CommandLine;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.Diagnostics.Tests.Common;
using Microsoft.Diagnostics.Tools.Trace;
using Xunit;

namespace Microsoft.Diagnostics.Tools.Trace
{
public class CollectLinuxCommandFunctionalTests
{
private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs(
CancellationToken ct = default,
string[] providers = null,
string clrEventLevel = "",
string clrEvents = "",
string[] perfEvents = null,
string[] profiles = null,
FileInfo output = null,
TimeSpan duration = default,
string name = "",
int processId = -1)
{
return new CollectLinuxCommandHandler.CollectLinuxArgs(ct,
providers ?? Array.Empty<string>(),
clrEventLevel,
clrEvents,
perfEvents ?? Array.Empty<string>(),
profiles ?? Array.Empty<string>(),
output ?? new FileInfo(CommonOptions.DefaultTraceName),
duration,
name,
processId);
}

[Theory]
[MemberData(nameof(BasicCases))]
public void CollectLinuxCommandProviderConfigurationConsolidation(object testArgs, string[] expectedLines)
{
MockConsole console = new(200, 30);
RunAsync((CollectLinuxCommandHandler.CollectLinuxArgs)testArgs, console);
console.AssertSanitizedLinesEqual(null, expectedLines);
}

private static string[] RunAsync(CollectLinuxCommandHandler.CollectLinuxArgs args, MockConsole console)
{
var handler = new CollectLinuxCommandHandler();
handler.Console = console;
handler.RecordTraceInvoker = (cmd, len, cb) => 0;
int exit = handler.CollectLinux(args);
if (exit != 0)
{
throw new InvalidOperationException($"Collect exited with return code {exit}.");
}
return console.Lines;
}

public static IEnumerable<object[]> BasicCases()
{
yield return new object[] {
TestArgs(),
new string[] {
"No providers, profiles, ClrEvents, or PerfEvents were specified, defaulting to trace profiles 'dotnet-common' + 'cpu-sampling'.",
"",
ProviderHeader,
FormatProvider("Microsoft-Windows-DotNETRuntime","000000100003801D","Informational",4,"--profile"),
"",
LinuxHeader,
LinuxProfile("cpu-sampling"),
""
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't there be some output about where the test output is being written to and telling the user to press Enter to stop the trace?

Copy link
Member Author

@mdh1418 mdh1418 Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the output file to Collect-linux, thanks for the catch. As per the press enter/ctrl+c message, normally that is part of the status printing.

For collect-linux, I added the line rewriting/status printing to the callback, so to mock that for the tests, extended the RecordTraceInvoker test seam to invoke the callback once with the progress output type, causing the status+ Press Enter/Ctrl+C to emit once.

};
yield return new object[] {
TestArgs(providers: new[]{"Foo:0x1:4"}),
new string[] {
"", ProviderHeader,
FormatProvider("Foo","0000000000000001","Informational",4,"--providers"),
"",
LinuxHeader,
""
}
};
yield return new object[] {
TestArgs(providers: new[]{"Foo:0x1:4","Bar:0x2:4"}),
new string[] {
"", ProviderHeader,
FormatProvider("Foo","0000000000000001","Informational",4,"--providers"),
FormatProvider("Bar","0000000000000002","Informational",4,"--providers"),
"",
LinuxHeader,
""
}
};
yield return new object[] {
TestArgs(profiles: new[]{"cpu-sampling"}),
new string[] {
"No .NET providers were configured.",
"",
LinuxHeader,
LinuxProfile("cpu-sampling"),
""
}
};
yield return new object[] {
TestArgs(providers: new[]{"Foo:0x1:4"}, profiles: new[]{"cpu-sampling"}),
new string[] {
"", ProviderHeader,
FormatProvider("Foo","0000000000000001","Informational",4,"--providers"),
"",
LinuxHeader,
LinuxProfile("cpu-sampling"),
""
}
};
yield return new object[] {
TestArgs(clrEvents: "gc", profiles: new[]{"cpu-sampling"}),
new string[] {
"", ProviderHeader,
FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--clrevents"),
"",
LinuxHeader,
LinuxProfile("cpu-sampling"),
""
}
};
yield return new object[] {
TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, profiles: new[]{"cpu-sampling"}),
new string[] {
"", ProviderHeader,
FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers"),
"",
LinuxHeader,
LinuxProfile("cpu-sampling"),
""
}
};
yield return new object[] {
TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, clrEvents: "gc"),
new string[] {
"Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents.",
"", ProviderHeader,
FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers"),
"",
LinuxHeader,
""
}
};
yield return new object[] {
TestArgs(clrEvents: "gc+jit"),
new string[] {
"", ProviderHeader,
FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Informational",4,"--clrevents"),
"",
LinuxHeader,
""
}
};
yield return new object[] {
TestArgs(clrEvents: "gc+jit", clrEventLevel: "5"),
new string[] {
"", ProviderHeader,
FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Verbose",5,"--clrevents"),
"",
LinuxHeader,
""
}
};
yield return new object[] {
TestArgs(perfEvents: new[]{"sched:sched_switch"}),
new string[] {
"No .NET providers were configured.",
"",
LinuxHeader,
LinuxPerfEvent("sched:sched_switch"),
""
}
};
}

private const string ProviderHeader = "Provider Name Keywords Level Enabled By";
private static string LinuxHeader => $"{"Linux Events",-80}Enabled By";
private static string LinuxProfile(string name) => $"{name,-80}--profile";
private static string LinuxPerfEvent(string spec) => $"{spec,-80}--perf-events";
private static string FormatProvider(string name, string keywordsHex, string levelName, int levelValue, string enabledBy)
{
string display = string.Format("{0, -40}", name) +
string.Format("0x{0, -18}", keywordsHex) +
string.Format("{0, -8}", $"{levelName}({levelValue})");
return string.Format("{0, -80}", display) + enabledBy;
}
}
}
Loading