diff --git a/eng/Versions.props b/eng/Versions.props
index 32eb6377308ad4..c70f7a05d68374 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -109,7 +109,7 @@
2.0.0
17.10.0-beta1.24272.1
- 3.1.16
+ 3.1.28
0.2.621003
2.1.0
2.0.3
@@ -124,6 +124,7 @@
1.6.0
17.4.0-preview-20220707-01
+ 0.1.32221
3.12.0
4.5.0
6.0.4
diff --git a/src/tests/Common/helixpublishwitharcade.proj b/src/tests/Common/helixpublishwitharcade.proj
index 2d896fe0342d47..3e311881383240 100644
--- a/src/tests/Common/helixpublishwitharcade.proj
+++ b/src/tests/Common/helixpublishwitharcade.proj
@@ -412,12 +412,27 @@
$(XUnitLogCheckerHelixPath)XUnitLogChecker$(ExeSuffix) $(XUnitLogCheckerArgs)
+
+ <_ExtraExecutableListFiles Include="@(_MergedPayloadFiles)"
+ Condition="$([System.String]::Copy('%(Identity)').ToLower().EndsWith('helix-extra-executables.list'))" />
+ <_ExtraExecutables Remove="@(_ExtraExecutables)" />
+
+
+
+
+
+ <_ExtraExecutables Remove="@(_ExtraExecutables)" Condition="'%(Identity)' == ''" />
+
+
+
+
+
@@ -458,6 +473,7 @@
+
diff --git a/src/tests/build.proj b/src/tests/build.proj
index ca195bf8041862..1d429a942fbf16 100644
--- a/src/tests/build.proj
+++ b/src/tests/build.proj
@@ -39,6 +39,7 @@
+
diff --git a/src/tests/tracing/eventpipe/userevents/NuGet.config b/src/tests/tracing/eventpipe/userevents/NuGet.config
new file mode 100644
index 00000000000000..583af9d11bb787
--- /dev/null
+++ b/src/tests/tracing/eventpipe/userevents/NuGet.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/tests/tracing/eventpipe/userevents/UserEventsRequirements.cs b/src/tests/tracing/eventpipe/userevents/UserEventsRequirements.cs
new file mode 100644
index 00000000000000..d7bc0e812294d2
--- /dev/null
+++ b/src/tests/tracing/eventpipe/userevents/UserEventsRequirements.cs
@@ -0,0 +1,185 @@
+// 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;
+
+namespace Tracing.Tests.UserEvents
+{
+ internal static class UserEventsRequirements
+ {
+ private static readonly Version s_minKernelVersion = new(6, 4);
+ private const string TracefsPath = "/sys/kernel/tracing";
+ private const string UserEventsDataPath = "/sys/kernel/tracing/user_events_data";
+ private static readonly Version s_minGlibcVersion = new(2, 35);
+
+ internal static bool IsSupported()
+ {
+ if (Environment.OSVersion.Version < s_minKernelVersion)
+ {
+ Console.WriteLine($"Kernel version '{Environment.OSVersion.Version}' is less than the minimum required '{s_minKernelVersion}'");
+ return false;
+ }
+
+ return IsGlibcAtLeast(s_minGlibcVersion) &&
+ IsTracefsMounted() &&
+ IsUserEventsDataAvailable();
+ }
+
+ private static bool IsTracefsMounted()
+ {
+ ProcessStartInfo checkTracefsStartInfo = new();
+ checkTracefsStartInfo.FileName = "sudo";
+ checkTracefsStartInfo.Arguments = $"-n test -d {TracefsPath}";
+ checkTracefsStartInfo.UseShellExecute = false;
+ checkTracefsStartInfo.RedirectStandardOutput = true;
+ checkTracefsStartInfo.RedirectStandardError = true;
+
+ using Process checkTracefs = new() { StartInfo = checkTracefsStartInfo };
+ checkTracefs.OutputDataReceived += (_, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ Console.WriteLine($"[tracefs-check] {e.Data}");
+ }
+ };
+ checkTracefs.ErrorDataReceived += (_, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ Console.Error.WriteLine($"[tracefs-check] {e.Data}");
+ }
+ };
+
+ checkTracefs.Start();
+ checkTracefs.BeginOutputReadLine();
+ checkTracefs.BeginErrorReadLine();
+ checkTracefs.WaitForExit();
+ if (checkTracefs.ExitCode == 0)
+ {
+ return true;
+ }
+
+ Console.WriteLine($"Tracefs not mounted at '{TracefsPath}'.");
+ return false;
+ }
+
+ private static bool IsUserEventsDataAvailable()
+ {
+ ProcessStartInfo checkUserEventsDataStartInfo = new();
+ checkUserEventsDataStartInfo.FileName = "sudo";
+ checkUserEventsDataStartInfo.Arguments = $"-n test -e {UserEventsDataPath}";
+ checkUserEventsDataStartInfo.UseShellExecute = false;
+ checkUserEventsDataStartInfo.RedirectStandardOutput = true;
+ checkUserEventsDataStartInfo.RedirectStandardError = true;
+
+ using Process checkUserEventsData = new() { StartInfo = checkUserEventsDataStartInfo };
+ checkUserEventsData.OutputDataReceived += (_, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ Console.WriteLine($"[user-events-check] {e.Data}");
+ }
+ };
+ checkUserEventsData.ErrorDataReceived += (_, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ Console.Error.WriteLine($"[user-events-check] {e.Data}");
+ }
+ };
+
+ checkUserEventsData.Start();
+ checkUserEventsData.BeginOutputReadLine();
+ checkUserEventsData.BeginErrorReadLine();
+ checkUserEventsData.WaitForExit();
+ if (checkUserEventsData.ExitCode == 0)
+ {
+ return true;
+ }
+
+ Console.WriteLine($"User events data not available at '{UserEventsDataPath}'.");
+ return false;
+ }
+
+ private static bool IsGlibcAtLeast(Version minVersion)
+ {
+ ProcessStartInfo lddStartInfo = new();
+ lddStartInfo.FileName = "ldd";
+ lddStartInfo.Arguments = "--version";
+ lddStartInfo.UseShellExecute = false;
+ lddStartInfo.RedirectStandardOutput = true;
+ lddStartInfo.RedirectStandardError = true;
+
+ using Process lddProcess = new() { StartInfo = lddStartInfo };
+ string? detectedVersionLine = null;
+
+ lddProcess.OutputDataReceived += (_, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data) && detectedVersionLine is null)
+ {
+ detectedVersionLine = e.Data;
+ }
+ };
+ lddProcess.ErrorDataReceived += (_, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ Console.Error.WriteLine($"[ldd] {e.Data}");
+ }
+ };
+
+ try
+ {
+ lddProcess.Start();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to start 'ldd --version': {ex.Message}");
+ return false;
+ }
+
+ lddProcess.BeginOutputReadLine();
+ lddProcess.BeginErrorReadLine();
+ lddProcess.WaitForExit();
+
+ if (lddProcess.ExitCode != 0)
+ {
+ Console.WriteLine($"'ldd --version' exited with code {lddProcess.ExitCode}.");
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(detectedVersionLine))
+ {
+ Console.WriteLine("Could not read glibc version from 'ldd --version' output.");
+ return false;
+ }
+
+ string[] tokens = detectedVersionLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ Version? glibcVersion = null;
+ foreach (string token in tokens)
+ {
+ if (Version.TryParse(token, out Version? parsedVersion))
+ {
+ glibcVersion = parsedVersion;
+ break;
+ }
+ }
+
+ if (glibcVersion is null)
+ {
+ Console.WriteLine($"Failed to parse glibc version from 'ldd --version' output line: {detectedVersionLine}");
+ return false;
+ }
+
+ if (glibcVersion < minVersion)
+ {
+ Console.WriteLine($"glibc version '{glibcVersion}' is less than required '{minVersion}'.");
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/tests/tracing/eventpipe/userevents/dotnet-common.script b/src/tests/tracing/eventpipe/userevents/dotnet-common.script
new file mode 100644
index 00000000000000..d6ac4f6ee89060
--- /dev/null
+++ b/src/tests/tracing/eventpipe/userevents/dotnet-common.script
@@ -0,0 +1,2 @@
+let Microsoft_Windows_DotNETRuntime_flags = new_dotnet_provider_flags();
+record_dotnet_provider("Microsoft-Windows-DotNETRuntime", 0x80000000000, 4, Microsoft_Windows_DotNETRuntime_flags);
diff --git a/src/tests/tracing/eventpipe/userevents/userevents.cs b/src/tests/tracing/eventpipe/userevents/userevents.cs
new file mode 100644
index 00000000000000..c42af630bcccda
--- /dev/null
+++ b/src/tests/tracing/eventpipe/userevents/userevents.cs
@@ -0,0 +1,221 @@
+// 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 const int SIGINT = 2;
+
+ [DllImport("libc", EntryPoint = "kill", 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()
+ {
+ string appBaseDir = AppContext.BaseDirectory;
+ string recordTracePath = Path.Combine(appBaseDir, "record-trace");
+ string scriptFilePath = Path.Combine(appBaseDir, "dotnet-common.script");
+ const string userEventsDataPath = "/sys/kernel/tracing/user_events_data";
+
+ if (!UserEventsRequirements.IsSupported())
+ {
+ Console.WriteLine("Skipping test: environment does not support user events.");
+ return 100;
+ }
+ if (!File.Exists(recordTracePath))
+ {
+ Console.Error.WriteLine($"record-trace not found at `{recordTracePath}`. Test cannot run.");
+ return -1;
+ }
+ if (!File.Exists(scriptFilePath))
+ {
+ Console.Error.WriteLine($"dotnet-common.script not found at `{scriptFilePath}`. Test cannot run.");
+ return -1;
+ }
+
+ string traceFilePath = Path.GetTempFileName();
+ File.Delete(traceFilePath); // record-trace requires the output file to not exist
+ traceFilePath = Path.ChangeExtension(traceFilePath, ".nettrace");
+
+ ProcessStartInfo recordTraceStartInfo = new();
+ recordTraceStartInfo.FileName = "sudo";
+ recordTraceStartInfo.Arguments = $"-n {recordTracePath} --script-file {scriptFilePath} --out {traceFilePath}";
+ recordTraceStartInfo.WorkingDirectory = appBaseDir;
+ recordTraceStartInfo.UseShellExecute = false;
+ recordTraceStartInfo.RedirectStandardOutput = true;
+ recordTraceStartInfo.RedirectStandardError = true;
+
+ Console.WriteLine($"Starting record-trace: {recordTraceStartInfo.FileName} {recordTraceStartInfo.Arguments}");
+ using Process recordTraceProcess = Process.Start(recordTraceStartInfo);
+ recordTraceProcess.OutputDataReceived += (_, args) =>
+ {
+ if (!string.IsNullOrEmpty(args.Data))
+ {
+ Console.WriteLine($"[record-trace] {args.Data}");
+ }
+ };
+ recordTraceProcess.BeginOutputReadLine();
+ recordTraceProcess.ErrorDataReceived += (_, args) =>
+ {
+ if (!string.IsNullOrEmpty(args.Data))
+ {
+ Console.Error.WriteLine($"[record-trace] {args.Data}");
+ }
+ };
+ recordTraceProcess.BeginErrorReadLine();
+ Console.WriteLine($"record-trace started with PID: {recordTraceProcess.Id}");
+
+ ProcessStartInfo traceeStartInfo = new();
+ traceeStartInfo.FileName = Process.GetCurrentProcess().MainModule.FileName;
+ traceeStartInfo.Arguments = $"{typeof(UserEventsTest).Assembly.Location} tracee";
+ traceeStartInfo.WorkingDirectory = appBaseDir;
+ traceeStartInfo.RedirectStandardOutput = true;
+ traceeStartInfo.RedirectStandardError = true;
+
+ // record-trace currently only searches /tmp/ for diagnostic ports https://github.com/microsoft/one-collect/issues/183
+ string diagnosticPortDir = "/tmp/";
+ traceeStartInfo.Environment["TMPDIR"] = diagnosticPortDir;
+
+ Console.WriteLine($"Starting tracee process: {traceeStartInfo.FileName} {traceeStartInfo.Arguments}");
+ using Process traceeProcess = Process.Start(traceeStartInfo);
+ int traceePid = traceeProcess.Id;
+ Console.WriteLine($"Tracee process started with PID: {traceePid}");
+ traceeProcess.OutputDataReceived += (_, args) =>
+ {
+ if (!string.IsNullOrEmpty(args.Data))
+ {
+ Console.WriteLine($"[tracee] {args.Data}");
+ }
+ };
+ traceeProcess.BeginOutputReadLine();
+ traceeProcess.ErrorDataReceived += (_, args) =>
+ {
+ if (!string.IsNullOrEmpty(args.Data))
+ {
+ Console.Error.WriteLine($"[tracee] {args.Data}");
+ }
+ };
+ traceeProcess.BeginErrorReadLine();
+
+ Console.WriteLine($"Waiting for tracee process to exit...");
+ if (!traceeProcess.HasExited && !traceeProcess.WaitForExit(5000))
+ {
+ Console.WriteLine($"Tracee process did not exit within the 5s timeout, killing it.");
+ traceeProcess.Kill();
+ }
+ traceeProcess.WaitForExit(); // flush async output
+
+ // Since TMPDIR was configured, the diagnostic port was created outside of helix's default temp datadisk path.
+ // The diagnostic port should be automatically cleaned up when the tracee shutsdown, but just in case of an
+ // abrupt exit, ensure cleanup to avoid leaving artifacts on helix machines.
+ // When https://github.com/microsoft/one-collect/issues/183 is fixed, this and the above TMPDIR should be removed.
+ CleanupTraceeDiagnosticPorts(diagnosticPortDir, traceePid);
+
+ if (!recordTraceProcess.HasExited)
+ {
+ // Until record-trace supports duration, the only way to stop it is to send SIGINT (ctrl+c)
+ Console.WriteLine($"Stopping record-trace with SIGINT.");
+ Kill(recordTraceProcess.Id, SIGINT);
+ Console.WriteLine($"Waiting for record-trace to exit...");
+ if (!recordTraceProcess.WaitForExit(20000))
+ {
+ // record-trace needs to stop gracefully to generate the trace file
+ Console.WriteLine($"record-trace did not exit within the 20s timeout, killing it.");
+ recordTraceProcess.Kill();
+ }
+ }
+ else
+ {
+ Console.WriteLine($"record-trace unexpectedly exited without SIGINT with code {recordTraceProcess.ExitCode}.");
+ }
+ recordTraceProcess.WaitForExit(); // flush async output
+
+ 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.");
+ UploadTraceFile(traceFilePath);
+ return -1;
+ }
+
+ return 100;
+ }
+
+ private static void CleanupTraceeDiagnosticPorts(string diagnosticPortDir, int traceePid)
+ {
+ try
+ {
+ string[] udsFiles = Directory.GetFiles(diagnosticPortDir, $"dotnet-diagnostic-{traceePid}-*-socket");
+ foreach (string udsFile in udsFiles)
+ {
+ Console.WriteLine($"Deleting tracee diagnostic port UDS file: {udsFile}");
+ File.Delete(udsFile);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Failed to cleanup tracee diagnostic ports: {ex}");
+ }
+ }
+
+ private static bool ValidateTraceeEvents(string traceFilePath)
+ {
+ using EventPipeEventSource source = new EventPipeEventSource(traceFilePath);
+ bool allocationSampledEventFound = false;
+
+ // TraceEvent's ClrTraceEventParser does not know about the AllocationSampled Event, so it shows up as "Unknown(303)"
+ source.Dynamic.All += (TraceEvent e) =>
+ {
+ if (e.ProviderName == "Microsoft-Windows-DotNETRuntime")
+ {
+ if (e.EventName == "AllocationSampled" || (e.ID == (TraceEventID)303 && e.EventName.StartsWith("Unknown")))
+ {
+ allocationSampledEventFound = true;
+ }
+ }
+ };
+
+ source.Process();
+ return allocationSampledEventFound;
+ }
+
+ private static void UploadTraceFile(string traceFilePath)
+ {
+ var helixWorkItemDirectory = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT");
+ if (helixWorkItemDirectory != null && Directory.Exists(helixWorkItemDirectory))
+ {
+ var destPath = Path.Combine(helixWorkItemDirectory, Path.GetFileName(traceFilePath));
+ Console.WriteLine($"Uploading trace file to Helix work item directory: {destPath}");
+ File.Copy(traceFilePath, destPath, overwrite: true);
+ }
+ else
+ {
+ Console.WriteLine($"Helix work item directory not found. Trace file remains at: {traceFilePath}");
+ }
+ }
+ }
+}
diff --git a/src/tests/tracing/eventpipe/userevents/userevents.csproj b/src/tests/tracing/eventpipe/userevents/userevents.csproj
new file mode 100644
index 00000000000000..e9f11e3444bc6d
--- /dev/null
+++ b/src/tests/tracing/eventpipe/userevents/userevents.csproj
@@ -0,0 +1,43 @@
+
+
+ true
+ true
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+ dotnet-common.script
+
+
+
+
+
+ <_DestDir>$(TargetDir)
+ <_DestDir Condition="'$(_DestDir)' == ''">$(OutputPath)
+ <_RecordTraceSource>$(NuGetPackageRoot)microsoft.onecollect.recordtrace/$(MicrosoftOneCollectRecordTraceVersion)/runtimes/linux-$(TargetArchitecture)/native/record-trace
+ <_RecordTraceRelative Condition="$(BuildProjectRelativeDir) != ''">$(BuildProjectRelativeDir)record-trace
+ <_RecordTraceRelative Condition="'$(_RecordTraceRelative)' == ''">$([System.IO.Path]::GetRelativePath('$(TestBinDir)', '$(_DestDir)record-trace'))
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tests/tracing/eventpipe/userevents/usereventstracee.cs b/src/tests/tracing/eventpipe/userevents/usereventstracee.cs
new file mode 100644
index 00000000000000..766f94e2e5d30c
--- /dev/null
+++ b/src/tests/tracing/eventpipe/userevents/usereventstracee.cs
@@ -0,0 +1,26 @@
+// 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; // 1s
+
+ while (Stopwatch.GetTimestamp() - startTimestamp < targetTicks)
+ {
+ s_array = new byte[1024 * 100];
+ Thread.Sleep(100);
+ }
+ }
+ }
+}