Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
3 changes: 2 additions & 1 deletion eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
<!-- Not auto-updated. -->
<MicrosoftDiaSymReaderVersion>2.0.0</MicrosoftDiaSymReaderVersion>
<MicrosoftDiaSymReaderNativeVersion>17.10.0-beta1.24272.1</MicrosoftDiaSymReaderNativeVersion>
<TraceEventVersion>3.1.16</TraceEventVersion>
<TraceEventVersion>3.1.28</TraceEventVersion>
<MicrosoftDiagnosticsNetCoreClientVersion>0.2.621003</MicrosoftDiagnosticsNetCoreClientVersion>
<NETStandardLibraryRefVersion>2.1.0</NETStandardLibraryRefVersion>
<NetStandardLibraryVersion>2.0.3</NetStandardLibraryVersion>
Expand All @@ -124,6 +124,7 @@
<!-- Testing -->
<MicrosoftNETCoreCoreDisToolsVersion>1.6.0</MicrosoftNETCoreCoreDisToolsVersion>
<MicrosoftNETTestSdkVersion>17.4.0-preview-20220707-01</MicrosoftNETTestSdkVersion>
<MicrosoftOneCollectRecordTraceVersion>0.1.32221</MicrosoftOneCollectRecordTraceVersion>
<NUnitVersion>3.12.0</NUnitVersion>
<NUnit3TestAdapterVersion>4.5.0</NUnit3TestAdapterVersion>
<CoverletCollectorVersion>6.0.4</CoverletCollectorVersion>
Expand Down
2 changes: 2 additions & 0 deletions src/tests/tracing/eventpipe/userevents/dotnet-common.script
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let Microsoft_Windows_DotNETRuntime_flags = new_dotnet_provider_flags();
record_dotnet_provider("Microsoft-Windows-DotNETRuntime", 0x100003801D, 4, Microsoft_Windows_DotNETRuntime_flags);
120 changes: 120 additions & 0 deletions src/tests/tracing/eventpipe/userevents/userevents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Etlx;

namespace Tracing.Tests.UserEvents
{
public class UserEventsTest
{
private static readonly string trace = "trace.nettrace";
private const int SIGINT = 2;

[DllImport("libc", SetLastError = true)]
private static extern int kill(int pid, int sig);

public static int Main(string[] args)
{
if (args.Length > 0 && args[0] == "tracee")
{
UserEventsTracee.Run();
return 0;
}

return TestEntryPoint();
}

public static int TestEntryPoint()
{
Copy link
Member

Choose a reason for hiding this comment

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

We should add checks for:

  1. process is elevated
  2. OS is Linux
  3. user_events are supported

Its likely at some point this test will be run in the wrong environment and the logs should make it trivial to diagnose.

Copy link
Member

@lateralusX lateralusX Nov 4, 2025

Choose a reason for hiding this comment

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

Maybe we should not even build the test on none linux platforms. CLRTestTargetUnsupported msbuild property could be used to exclude a test on specific platforms.

Copy link
Member Author

Choose a reason for hiding this comment

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

The CLRTestTargetUnsupported is in the csproj, so it should hopefully prevent this test from running on non linux-x64/linux-arm64 platforms. Then again, I think more logic is needed to check for Alpine.

Added checks for geteuid and checking if sys/kernel/tracing/user_events_data exists

string appBaseDir = AppContext.BaseDirectory;
string recordTracePath = Path.Combine(appBaseDir, "record-trace");
string scriptFilePath = Path.Combine(appBaseDir, "dotnet-common.script");
string traceFilePath = Path.Combine(appBaseDir, trace);
Copy link
Member

Choose a reason for hiding this comment

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

We probably want to create a path in some randomly generated temp file (Path.GetTempFileName()). This helps avoid issues if the test runs multiple times and doesn't correctly clean up or if there is ever a scenario where the directory holding the test binary isn't writable.

Copy link
Member Author

Choose a reason for hiding this comment

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

Switched to Path.GetTempFileName(),


if (!File.Exists(recordTracePath) || !File.Exists(scriptFilePath))
{
Console.WriteLine("record-trace or dotnet-common.script not found. Test cannot run.");
Copy link
Member

Choose a reason for hiding this comment

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

Its helpful to print the exact path we didn't find. (In general its helpful for the test logging to err towards being overly verbose to make failure diagnosis easier)

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, split into separate if blocks to have a more specific error message.

return -1;
}

ProcessStartInfo traceeStartInfo = new();
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest logging the command-line for both processes being launched.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added the commands and args near Process.Start and also added logs for the PID associated with the tracee and record-trace.

traceeStartInfo.FileName = Process.GetCurrentProcess().MainModule.FileName;
traceeStartInfo.Arguments = $"{typeof(UserEventsTest).Assembly.Location} tracee";
traceeStartInfo.WorkingDirectory = appBaseDir;

ProcessStartInfo recordTraceStartInfo = new();
recordTraceStartInfo.FileName = recordTracePath;
recordTraceStartInfo.Arguments = $"--script-file {scriptFilePath}";
recordTraceStartInfo.WorkingDirectory = appBaseDir;
recordTraceStartInfo.RedirectStandardOutput = true;
recordTraceStartInfo.RedirectStandardError = true;

using Process traceeProcess = Process.Start(traceeStartInfo);
using Process recordTraceProcess = Process.Start(recordTraceStartInfo);
Copy link
Member

Choose a reason for hiding this comment

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

To ensure tracer observes the tracee we should start the tracer process first.

Copy link
Member Author

Choose a reason for hiding this comment

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

Switched. Originally I was wondering if we should pass --pid {traceePid}, but the process isolation should prevent this test from tracing another runtime test and mistaking the events should the tracee process crash. But I guess even in that case... it showed that user_events were collected.

recordTraceProcess.OutputDataReceived += (_, args) => Console.WriteLine($"[record-trace] {args.Data}");
Copy link
Member

Choose a reason for hiding this comment

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

We should redirect the tracee output as well to ensure we aren't losing useful error diagnostics that it might print.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added

recordTraceProcess.BeginOutputReadLine();
recordTraceProcess.ErrorDataReceived += (_, args) => Console.Error.WriteLine($"[record-trace] {args.Data}");
recordTraceProcess.BeginErrorReadLine();

if (!traceeProcess.HasExited && !traceeProcess.WaitForExit(15000))
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest logging that the test is waiting for the tracee to exit, and certainly log if the wait times out and we kill it.

Copy link
Member Author

Choose a reason for hiding this comment

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

good point, added more diagnostics

{
traceeProcess.Kill();
}

// Until record-trace supports duration, the only way to stop it is to send SIGINT (ctrl+c)
kill(recordTraceProcess.Id, SIGINT);
Copy link
Member

Choose a reason for hiding this comment

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

Add more logging that we sent SIGINT, waiting for exit, and if we killed the process after timeout.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added, thanks!

if (!recordTraceProcess.HasExited && !recordTraceProcess.WaitForExit(20000))
{
// record-trace needs to stop gracefully to generate the trace file
recordTraceProcess.Kill();
}

if (!File.Exists(traceFilePath))
{
Console.Error.WriteLine($"Expected trace file not found at `{traceFilePath}`");
return -1;
}

if (!ValidateTraceeEvents(traceFilePath))
{
Console.Error.WriteLine($"Trace file `{traceFilePath}` does not contain expected events.");
return -1;
}

return 100;
}

private static bool ValidateTraceeEvents(string traceFilePath)
{
string etlxPath = TraceLog.CreateFromEventPipeDataFile(traceFilePath);
Copy link
Member

Choose a reason for hiding this comment

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

You can parse the .nettrace file directly using EventPipeEventSource in TraceEvent. This avoids creating a 2nd file that the test also needs to clean up.

Copy link
Member Author

Choose a reason for hiding this comment

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

For some reason when I tried it last, it didn't work (pilot error), switched to using EventPipeEventSource. Right now the events are "unknown" with id. Maybe its cause I'm using the Dynamic parser? I'll look into TraceEvent more closely

using TraceLog log = new(etlxPath);
using TraceLogEventSource source = log.Events.GetSource();
bool startEventFound = false;
bool stopEventFound = false;

source.AllEvents += (TraceEvent e) =>
Copy link
Member

@lateralusX lateralusX Nov 4, 2025

Choose a reason for hiding this comment

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

Is the plan to add more tests here that checks for metadata/fields, rundown events, callstacks etc or should we add some basic verification to this test or plan to extend existing EventPipe tests to also work over UserEvents?

Copy link
Member Author

Choose a reason for hiding this comment

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

This was mainly to add a basic verification that the end to end runtime side (from accepting the ipc message to writing to the tracepoints worked). Since User_events is built on EventPipe, my initial thought is that duplicating the existing eventpipe tests for user events wouldn't be adding anything. I think we can add more tests later on, but not sure what coverage is good and reasonable. I'm not even sure yet if our CI machines have user_events, or if they run with elevated privileges, so this was mainly to see if we can have a basic E2E test going.

Copy link
Member

Choose a reason for hiding this comment

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

We should definitely not duplicate but maybe look on extending the existing event pipe tests to run over user events + additional validate logic but agree that is something we could look at later.

So, if this is mainly a smoke test then maybe we should make sure we at least hit things we know is handled in the one-collect library, during the work we hit a number of things that needed special attention, like activity id's, custom metadata and potential stack traces. Right now, these only tests one runtime start/stop event fired under very unique circumstances. Maybe we should do some short multi-threading scenario as well, making sure we won't hit any races in the code path unique to user events?

{
if (e.ProviderName == "Microsoft-Windows-DotNETRuntime")
{
if (e.EventName == "GC/Start")
{
startEventFound = true;
}
else if (e.EventName == "GC/Stop")
{
stopEventFound = true;
}
}
};

source.Process();
return startEventFound && stopEventFound;
}
}
}
34 changes: 34 additions & 0 deletions src/tests/tracing/eventpipe/userevents/userevents.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<CLRTestTargetUnsupported Condition="'$(TargetOS)' != 'linux' or ('$(TargetArchitecture)' != 'x64' and '$(TargetArchitecture)' != 'arm64')">true</CLRTestTargetUnsupported>
<RequiresProcessIsolation>true</RequiresProcessIsolation>
<ReferenceXUnitWrapperGenerator>false</ReferenceXUnitWrapperGenerator>
<IlasmRoundTripIncompatible>true</IlasmRoundTripIncompatible>
</PropertyGroup>

<PropertyGroup>
<RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-diagnostics-tests/nuget/v3/index.json</RestoreAdditionalProjectSources>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.OneCollect.RecordTrace" Version="$(MicrosoftOneCollectRecordTraceVersion)" PrivateAssets="All" Condition="'$(TargetOS)' == 'linux'" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(MSBuildProjectName).cs" />
<Compile Include="usereventstracee.cs" />
</ItemGroup>

<ItemGroup>
<None Include="dotnet-common.script" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<Target Name="CopyRecordTraceBinary" AfterTargets="Build" Condition="'$(TargetOS)' == 'linux'">
<PropertyGroup>
<RecordTracePath>$(NuGetPackageRoot)microsoft.onecollect.recordtrace/$(MicrosoftOneCollectRecordTraceVersion)/runtimes/$(TargetOS)-$(TargetArchitecture)/native/record-trace</RecordTracePath>
</PropertyGroup>

<Copy SourceFiles="$(RecordTracePath)" DestinationFolder="$(OutputPath)" Condition="Exists('$(RecordTracePath)')" />
<Error Text="record-trace not found at $(RecordTracePath)" Condition="!Exists('$(RecordTracePath)')" />

Check failure on line 32 in src/tests/tracing/eventpipe/userevents/userevents.csproj

View check run for this annotation

Azure Pipelines / runtime (Build linux-x64 Release AllSubsets_Mono_LLVMAot_RuntimeTests llvmaot)

src/tests/tracing/eventpipe/userevents/userevents.csproj#L32

src/tests/tracing/eventpipe/userevents/userevents.csproj(32,5): error : record-trace not found at /__w/1/s/.packages/microsoft.onecollect.recordtrace/0.1.32221/runtimes/linux-x64/native/record-trace

Check failure on line 32 in src/tests/tracing/eventpipe/userevents/userevents.csproj

View check run for this annotation

Azure Pipelines / runtime (Build linux-arm64 Release AllSubsets_Mono_Minijit_RuntimeTests minijit)

src/tests/tracing/eventpipe/userevents/userevents.csproj#L32

src/tests/tracing/eventpipe/userevents/userevents.csproj(32,5): error : record-trace not found at /__w/1/s/.packages/microsoft.onecollect.recordtrace/0.1.32221/runtimes/linux-arm64/native/record-trace

Check failure on line 32 in src/tests/tracing/eventpipe/userevents/userevents.csproj

View check run for this annotation

Azure Pipelines / runtime

src/tests/tracing/eventpipe/userevents/userevents.csproj#L32

src/tests/tracing/eventpipe/userevents/userevents.csproj(32,5): error : record-trace not found at /__w/1/s/.packages/microsoft.onecollect.recordtrace/0.1.32221/runtimes/linux-arm64/native/record-trace

Check failure on line 32 in src/tests/tracing/eventpipe/userevents/userevents.csproj

View check run for this annotation

Azure Pipelines / runtime

src/tests/tracing/eventpipe/userevents/userevents.csproj#L32

src/tests/tracing/eventpipe/userevents/userevents.csproj(32,5): error : record-trace not found at /__w/1/s/.packages/microsoft.onecollect.recordtrace/0.1.32221/runtimes/linux-x64/native/record-trace
</Target>
</Project>
31 changes: 31 additions & 0 deletions src/tests/tracing/eventpipe/userevents/usereventstracee.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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.Diagnostics;
using System.Threading;

namespace Tracing.Tests.UserEvents
{
public class UserEventsTracee
{
private static byte[] s_array;

public static void Run()
{
long startTimestamp = Stopwatch.GetTimestamp();
long targetTicks = Stopwatch.Frequency * 10; // 10s
Copy link
Member

Choose a reason for hiding this comment

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

How about 1 second instead? Ideally we want tests to run quickly whenever possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed


while (Stopwatch.GetTimestamp() - startTimestamp < targetTicks)
{
for (int i = 0; i < 100; i++)
{
s_array = new byte[1024 * 10];
}

GC.Collect();
Thread.Sleep(100);
}
}
}
}
Loading