Skip to content

Commit

Permalink
Add EventPipe support
Browse files Browse the repository at this point in the history
  • Loading branch information
xoofx committed Dec 8, 2024
1 parent abd3e73 commit d31a701
Show file tree
Hide file tree
Showing 5 changed files with 345 additions and 31 deletions.
21 changes: 19 additions & 2 deletions src/Ultra.Core/DiagnosticsClientHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,29 @@ namespace Ultra.Core;
// - `ApplyStartupHook`: https://github.com/dotnet/diagnostics/pull/5086
internal static class DiagnosticsClientHelper
{
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ApplyStartupHook")]
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(ApplyStartupHook))]
public static extern void ApplyStartupHook(this DiagnosticsClient client, string assemblyPath);

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

/// <summary>
/// Wait for an available diagnostic endpoint to the runtime instance.
/// </summary>
/// <param name="timeout">The amount of time to wait before cancelling the wait for the connection.</param>
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(WaitForConnection))]
public static extern void WaitForConnection(this DiagnosticsClient client, TimeSpan timeout);

/// <summary>
/// Wait for an available diagnostic endpoint to the runtime instance.
/// </summary>
/// <param name="token">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task the completes when a diagnostic endpoint to the runtime instance becomes available.
/// </returns>
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(WaitForConnectionAsync))]
public static extern Task WaitForConnectionAsync(this DiagnosticsClient client, CancellationToken token);

public static DiagnosticsClient Create(IpcEndpointBridge endPoint)
{
var ctor = typeof(DiagnosticsClient).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance,
Expand Down
1 change: 1 addition & 0 deletions src/Ultra.Core/Ultra.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<ProjectReference Include="..\Ultra.ProcessHook\Ultra.ProcessHook.csproj" />
<ProjectReference Include="..\Ultra.Sampler\Ultra.Sampler.csproj" />
</ItemGroup>
</Project>
90 changes: 63 additions & 27 deletions src/Ultra.Core/UltraProfiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Diagnostics;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text.Json;
using ByteSizeLib;
using Microsoft.Diagnostics.Tracing.Session;
Expand All @@ -20,15 +21,19 @@ public abstract class UltraProfiler : IDisposable
private protected bool StopRequested;
private protected readonly Stopwatch ProfilerClock;
private protected TimeSpan LastTimeProgress;
private readonly CancellationTokenSource _cancellationTokenSource;

/// <summary>
/// Initializes a new instance of the <see cref="UltraProfiler"/> class.
/// </summary>
protected UltraProfiler()
{
ProfilerClock = new Stopwatch();
_cancellationTokenSource = new CancellationTokenSource();
}

protected CancellationToken CancellationToken => _cancellationTokenSource.Token;

/// <summary>
/// Creates a new instance of the <see cref="UltraProfiler"/> class.
/// </summary>
Expand All @@ -41,7 +46,12 @@ public static UltraProfiler Create()
return new UltraProfilerEtw();
}

throw new PlatformNotSupportedException("Only Windows is supported");
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
return new UltraProfilerEventPipe();
}

throw new PlatformNotSupportedException("Only Windows or macOS+ARM64 are supported");
}

/// <summary>
Expand Down Expand Up @@ -170,12 +180,12 @@ public async Task<string> Run(UltraProfilerOptions ultraProfilerOptions)
{
await runner.OnStart();
{
var startTheRequestedProgramIfRequired = () =>
var startTheRequestedProgramIfRequired = async () =>
{
// Start a command line process if needed
if (ultraProfilerOptions.ProgramPath is not null)
{
var processState = StartProcess(ultraProfilerOptions);
var processState = await StartProcess(runner, ultraProfilerOptions);
processList.Add(processState.Process);
// Append the pid for a single process that we are attaching to
if (singleProcess is null)
Expand All @@ -188,10 +198,11 @@ public async Task<string> Run(UltraProfilerOptions ultraProfilerOptions)
};

// If we have a delay, or we are asked to start paused, we start the process before the profiling starts
bool hasExplicitProgramHasStarted = ultraProfilerOptions.DelayInSeconds != 0.0 || ultraProfilerOptions.Paused;
// On macOS we always need to start the program before enabling profiling
bool hasExplicitProgramHasStarted = ultraProfilerOptions.DelayInSeconds != 0.0 || ultraProfilerOptions.Paused || OperatingSystem.IsMacOS();
if (hasExplicitProgramHasStarted)
{
startTheRequestedProgramIfRequired();
await startTheRequestedProgramIfRequired();
}

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

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

var fileToConvert = await runner.FinishFileToConvert();

var jsonFinalFile = await Convert(fileToConvert, processList.Select(x => x.Id).ToList(), ultraProfilerOptions);
string jsonFinalFile = string.Empty;
if (!string.IsNullOrEmpty(fileToConvert))
{
jsonFinalFile = await Convert(fileToConvert, processList.Select(x => x.Id).ToList(), ultraProfilerOptions);
}

await runner.OnFinalCleanup();

return jsonFinalFile;
}


private protected class ProfilerRunner
{
public required Func<Task> OnStart;

public required Func<Task> OnEnablingProfiling;

public required Func<long> OnProfiling;

public required Func<Task> OnStop;

public required Func<Task> OnCatch;

public required Func<Task> OnFinally;

public required Func<Task<string>> FinishFileToConvert;

public required Func<Task> OnFinalCleanup;
}

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

Expand Down Expand Up @@ -415,7 +411,7 @@ private protected async Task WaitForStaleFile(string file, UltraProfilerOptions
}
}

private protected static ProcessState StartProcess(UltraProfilerOptions ultraProfilerOptions)
private protected static async Task<ProcessState> StartProcess(ProfilerRunner runner, UltraProfilerOptions ultraProfilerOptions)
{
var mode = ultraProfilerOptions.ConsoleMode;

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

if (runner.OnPrepareStartProcess != null)
{
await runner.OnPrepareStartProcess(startInfo);
}

process.Start();
}
else
Expand All @@ -463,6 +464,11 @@ private protected static ProcessState StartProcess(UltraProfilerOptions ultraPro
}
};

if (runner.OnPrepareStartProcess != null)
{
await runner.OnPrepareStartProcess(startInfo);
}

process.Start();

process.BeginOutputReadLine();
Expand Down Expand Up @@ -490,6 +496,11 @@ private protected static ProcessState StartProcess(UltraProfilerOptions ultraPro
};
thread.Start();

if (runner.OnProcessStarted != null)
{
await runner.OnProcessStarted(process);
}

return state;
}

Expand All @@ -503,6 +514,31 @@ private void WaitForCleanCancel()
}
}

private protected class ProfilerRunner(string baseFileName)
{
public string BaseFileName { get; } = baseFileName;

public required Func<Task> OnStart;

public required Func<Task> OnEnablingProfiling;

public required Func<long> OnProfiling;

public required Func<Task> OnStop;

public Func<ProcessStartInfo, Task>? OnPrepareStartProcess;

public Func<Process, Task>? OnProcessStarted;

public required Func<Task> OnCatch;

public required Func<Task> OnFinally;

public required Func<Task<string>> FinishFileToConvert;

public required Func<Task> OnFinalCleanup;
}

private protected class ProcessState
{
public ProcessState(Process process)
Expand Down
2 changes: 1 addition & 1 deletion src/Ultra.Core/UltraProfilerEtw.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private protected override ProfilerRunner CreateRunner(UltraProfilerOptions ultr

string? etlFinalFile = null;

var runner = new ProfilerRunner()
var runner = new ProfilerRunner(baseName)
{
OnStart = () =>
{
Expand Down
Loading

0 comments on commit d31a701

Please sign in to comment.