Skip to content

Commit d31a701

Browse files
committed
Add EventPipe support
1 parent abd3e73 commit d31a701

5 files changed

+345
-31
lines changed

src/Ultra.Core/DiagnosticsClientHelper.cs

+19-2
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,29 @@ namespace Ultra.Core;
1111
// - `ApplyStartupHook`: https://github.com/dotnet/diagnostics/pull/5086
1212
internal static class DiagnosticsClientHelper
1313
{
14-
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ApplyStartupHook")]
14+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(ApplyStartupHook))]
1515
public static extern void ApplyStartupHook(this DiagnosticsClient client, string assemblyPath);
1616

17-
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ApplyStartupHookAsync")]
17+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(ApplyStartupHookAsync))]
1818
public static extern Task ApplyStartupHookAsync(this DiagnosticsClient client, string assemblyPath, CancellationToken token);
1919

20+
/// <summary>
21+
/// Wait for an available diagnostic endpoint to the runtime instance.
22+
/// </summary>
23+
/// <param name="timeout">The amount of time to wait before cancelling the wait for the connection.</param>
24+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(WaitForConnection))]
25+
public static extern void WaitForConnection(this DiagnosticsClient client, TimeSpan timeout);
26+
27+
/// <summary>
28+
/// Wait for an available diagnostic endpoint to the runtime instance.
29+
/// </summary>
30+
/// <param name="token">The token to monitor for cancellation requests.</param>
31+
/// <returns>
32+
/// A task the completes when a diagnostic endpoint to the runtime instance becomes available.
33+
/// </returns>
34+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(WaitForConnectionAsync))]
35+
public static extern Task WaitForConnectionAsync(this DiagnosticsClient client, CancellationToken token);
36+
2037
public static DiagnosticsClient Create(IpcEndpointBridge endPoint)
2138
{
2239
var ctor = typeof(DiagnosticsClient).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance,

src/Ultra.Core/Ultra.Core.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@
3939
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4040
</PackageReference>
4141
<ProjectReference Include="..\Ultra.ProcessHook\Ultra.ProcessHook.csproj" />
42+
<ProjectReference Include="..\Ultra.Sampler\Ultra.Sampler.csproj" />
4243
</ItemGroup>
4344
</Project>

src/Ultra.Core/UltraProfiler.cs

+63-27
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Diagnostics;
66
using System.IO.Compression;
7+
using System.Runtime.InteropServices;
78
using System.Text.Json;
89
using ByteSizeLib;
910
using Microsoft.Diagnostics.Tracing.Session;
@@ -20,15 +21,19 @@ public abstract class UltraProfiler : IDisposable
2021
private protected bool StopRequested;
2122
private protected readonly Stopwatch ProfilerClock;
2223
private protected TimeSpan LastTimeProgress;
24+
private readonly CancellationTokenSource _cancellationTokenSource;
2325

2426
/// <summary>
2527
/// Initializes a new instance of the <see cref="UltraProfiler"/> class.
2628
/// </summary>
2729
protected UltraProfiler()
2830
{
2931
ProfilerClock = new Stopwatch();
32+
_cancellationTokenSource = new CancellationTokenSource();
3033
}
3134

35+
protected CancellationToken CancellationToken => _cancellationTokenSource.Token;
36+
3237
/// <summary>
3338
/// Creates a new instance of the <see cref="UltraProfiler"/> class.
3439
/// </summary>
@@ -41,7 +46,12 @@ public static UltraProfiler Create()
4146
return new UltraProfilerEtw();
4247
}
4348

44-
throw new PlatformNotSupportedException("Only Windows is supported");
49+
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
50+
{
51+
return new UltraProfilerEventPipe();
52+
}
53+
54+
throw new PlatformNotSupportedException("Only Windows or macOS+ARM64 are supported");
4555
}
4656

4757
/// <summary>
@@ -170,12 +180,12 @@ public async Task<string> Run(UltraProfilerOptions ultraProfilerOptions)
170180
{
171181
await runner.OnStart();
172182
{
173-
var startTheRequestedProgramIfRequired = () =>
183+
var startTheRequestedProgramIfRequired = async () =>
174184
{
175185
// Start a command line process if needed
176186
if (ultraProfilerOptions.ProgramPath is not null)
177187
{
178-
var processState = StartProcess(ultraProfilerOptions);
188+
var processState = await StartProcess(runner, ultraProfilerOptions);
179189
processList.Add(processState.Process);
180190
// Append the pid for a single process that we are attaching to
181191
if (singleProcess is null)
@@ -188,10 +198,11 @@ public async Task<string> Run(UltraProfilerOptions ultraProfilerOptions)
188198
};
189199

190200
// If we have a delay, or we are asked to start paused, we start the process before the profiling starts
191-
bool hasExplicitProgramHasStarted = ultraProfilerOptions.DelayInSeconds != 0.0 || ultraProfilerOptions.Paused;
201+
// On macOS we always need to start the program before enabling profiling
202+
bool hasExplicitProgramHasStarted = ultraProfilerOptions.DelayInSeconds != 0.0 || ultraProfilerOptions.Paused || OperatingSystem.IsMacOS();
192203
if (hasExplicitProgramHasStarted)
193204
{
194-
startTheRequestedProgramIfRequired();
205+
await startTheRequestedProgramIfRequired();
195206
}
196207

197208
// Wait for the process to start
@@ -213,7 +224,7 @@ public async Task<string> Run(UltraProfilerOptions ultraProfilerOptions)
213224
// If we haven't started the program yet, we start it now (for explicit program path)
214225
if (!hasExplicitProgramHasStarted)
215226
{
216-
startTheRequestedProgramIfRequired();
227+
await startTheRequestedProgramIfRequired();
217228
}
218229

219230
foreach (var process in processList)
@@ -282,32 +293,17 @@ public async Task<string> Run(UltraProfilerOptions ultraProfilerOptions)
282293

283294
var fileToConvert = await runner.FinishFileToConvert();
284295

285-
var jsonFinalFile = await Convert(fileToConvert, processList.Select(x => x.Id).ToList(), ultraProfilerOptions);
296+
string jsonFinalFile = string.Empty;
297+
if (!string.IsNullOrEmpty(fileToConvert))
298+
{
299+
jsonFinalFile = await Convert(fileToConvert, processList.Select(x => x.Id).ToList(), ultraProfilerOptions);
300+
}
286301

287302
await runner.OnFinalCleanup();
288303

289304
return jsonFinalFile;
290305
}
291306

292-
293-
private protected class ProfilerRunner
294-
{
295-
public required Func<Task> OnStart;
296-
297-
public required Func<Task> OnEnablingProfiling;
298-
299-
public required Func<long> OnProfiling;
300-
301-
public required Func<Task> OnStop;
302-
303-
public required Func<Task> OnCatch;
304-
305-
public required Func<Task> OnFinally;
306-
307-
public required Func<Task<string>> FinishFileToConvert;
308-
309-
public required Func<Task> OnFinalCleanup;
310-
}
311307

312308
private protected abstract ProfilerRunner CreateRunner(UltraProfilerOptions ultraProfilerOptions, List<Process> processList, string baseName, Process? singleProcess);
313309

@@ -415,7 +411,7 @@ private protected async Task WaitForStaleFile(string file, UltraProfilerOptions
415411
}
416412
}
417413

418-
private protected static ProcessState StartProcess(UltraProfilerOptions ultraProfilerOptions)
414+
private protected static async Task<ProcessState> StartProcess(ProfilerRunner runner, UltraProfilerOptions ultraProfilerOptions)
419415
{
420416
var mode = ultraProfilerOptions.ConsoleMode;
421417

@@ -437,6 +433,11 @@ private protected static ProcessState StartProcess(UltraProfilerOptions ultraPro
437433
startInfo.CreateNoWindow = true;
438434
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
439435

436+
if (runner.OnPrepareStartProcess != null)
437+
{
438+
await runner.OnPrepareStartProcess(startInfo);
439+
}
440+
440441
process.Start();
441442
}
442443
else
@@ -463,6 +464,11 @@ private protected static ProcessState StartProcess(UltraProfilerOptions ultraPro
463464
}
464465
};
465466

467+
if (runner.OnPrepareStartProcess != null)
468+
{
469+
await runner.OnPrepareStartProcess(startInfo);
470+
}
471+
466472
process.Start();
467473

468474
process.BeginOutputReadLine();
@@ -490,6 +496,11 @@ private protected static ProcessState StartProcess(UltraProfilerOptions ultraPro
490496
};
491497
thread.Start();
492498

499+
if (runner.OnProcessStarted != null)
500+
{
501+
await runner.OnProcessStarted(process);
502+
}
503+
493504
return state;
494505
}
495506

@@ -503,6 +514,31 @@ private void WaitForCleanCancel()
503514
}
504515
}
505516

517+
private protected class ProfilerRunner(string baseFileName)
518+
{
519+
public string BaseFileName { get; } = baseFileName;
520+
521+
public required Func<Task> OnStart;
522+
523+
public required Func<Task> OnEnablingProfiling;
524+
525+
public required Func<long> OnProfiling;
526+
527+
public required Func<Task> OnStop;
528+
529+
public Func<ProcessStartInfo, Task>? OnPrepareStartProcess;
530+
531+
public Func<Process, Task>? OnProcessStarted;
532+
533+
public required Func<Task> OnCatch;
534+
535+
public required Func<Task> OnFinally;
536+
537+
public required Func<Task<string>> FinishFileToConvert;
538+
539+
public required Func<Task> OnFinalCleanup;
540+
}
541+
506542
private protected class ProcessState
507543
{
508544
public ProcessState(Process process)

src/Ultra.Core/UltraProfilerEtw.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ private protected override ProfilerRunner CreateRunner(UltraProfilerOptions ultr
4646

4747
string? etlFinalFile = null;
4848

49-
var runner = new ProfilerRunner()
49+
var runner = new ProfilerRunner(baseName)
5050
{
5151
OnStart = () =>
5252
{

0 commit comments

Comments
 (0)