Skip to content

Commit b3eb108

Browse files
committed
Allow to have a specific implementation of the profiler per OS
1 parent d9add41 commit b3eb108

File tree

3 files changed

+338
-274
lines changed

3 files changed

+338
-274
lines changed

src/Ultra.Core/EtwUltraProfiler.cs

+35-272
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,44 @@
55
using System.Diagnostics;
66
using System.IO.Compression;
77
using System.Text.Json;
8-
using ByteSizeLib;
9-
using Microsoft.Diagnostics.Tracing;
10-
using Microsoft.Diagnostics.Tracing.Parsers;
11-
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
128
using Microsoft.Diagnostics.Tracing.Session;
139

1410
namespace Ultra.Core;
1511

1612
/// <summary>
1713
/// A profiler that uses Event Tracing for Windows (ETW) to collect performance data.
1814
/// </summary>
19-
public class EtwUltraProfiler : IDisposable
15+
public abstract class EtwUltraProfiler : IDisposable
2016
{
21-
private TraceEventSession? _userSession;
22-
private TraceEventSession? _kernelSession;
23-
private bool _cancelRequested;
24-
private ManualResetEvent? _cleanCancel;
25-
private bool _stopRequested;
26-
private readonly Stopwatch _profilerClock;
27-
private TimeSpan _lastTimeProgress;
17+
private protected bool _cancelRequested;
18+
private protected ManualResetEvent? _cleanCancel;
19+
private protected bool _stopRequested;
20+
private protected readonly Stopwatch _profilerClock;
21+
private protected TimeSpan _lastTimeProgress;
2822

2923
/// <summary>
3024
/// Initializes a new instance of the <see cref="EtwUltraProfiler"/> class.
3125
/// </summary>
32-
public EtwUltraProfiler()
26+
protected EtwUltraProfiler()
3327
{
3428
_profilerClock = new Stopwatch();
3529
}
3630

31+
/// <summary>
32+
/// Creates a new instance of the <see cref="EtwUltraProfiler"/> class.
33+
/// </summary>
34+
/// <returns>A new instance of the <see cref="EtwUltraProfiler"/> class.</returns>
35+
/// <exception cref="PlatformNotSupportedException">Thrown when the current platform is not supported.</exception>
36+
public static EtwUltraProfiler Create()
37+
{
38+
if (OperatingSystem.IsWindows())
39+
{
40+
return new EtwUltraProfilerWindows();
41+
}
42+
43+
throw new PlatformNotSupportedException("Only Windows is supported");
44+
}
45+
3746
/// <summary>
3847
/// Requests to cancel the profiling session.
3948
/// </summary>
@@ -61,14 +70,13 @@ public bool Cancel()
6170
/// </summary>
6271
public void Dispose()
6372
{
64-
_userSession?.Dispose();
65-
_userSession = null;
66-
_kernelSession?.Dispose();
67-
_kernelSession = null;
73+
DisposeImpl();
6874
_cleanCancel?.Dispose();
6975
_cleanCancel = null;
7076
}
7177

78+
private protected abstract void DisposeImpl();
79+
7280
/// <summary>
7381
/// Determines whether the current process is running with elevated privileges.
7482
/// </summary>
@@ -153,226 +161,12 @@ public async Task<string> Run(EtwUltraProfilerOptions ultraProfilerOptions)
153161
baseName = $"{baseName}_pid_{singleProcess.Id}";
154162
}
155163

156-
var options = new TraceEventProviderOptions()
157-
{
158-
StacksEnabled = true,
159-
};
160-
161-
// Filter the requested process ids
162-
if (processList.Count > 0)
163-
{
164-
options.ProcessIDFilter = new List<int>();
165-
foreach (var process in processList)
166-
{
167-
options.ProcessIDFilter.Add(process.Id);
168-
}
169-
}
170-
171-
// Make sure to filter the process name if we have a single process
172-
if (ultraProfilerOptions.ProgramPath != null)
173-
{
174-
options.ProcessNameFilter = [Path.GetFileName(ultraProfilerOptions.ProgramPath)];
175-
}
176-
177-
var kernelFileName = $"{baseName}.kernel.etl";
178-
var userFileName = $"{baseName}.user.etl";
179-
180-
_profilerClock.Restart();
181-
_lastTimeProgress = _profilerClock.Elapsed;
182-
183-
_userSession = new TraceEventSession($"{baseName}-user", userFileName);
184-
_kernelSession = new TraceEventSession($"{baseName}-kernel", kernelFileName);
185-
186-
try
187-
{
188-
using (_userSession)
189-
using (_kernelSession)
190-
{
191-
var startTheRequestedProgramIfRequired = () =>
192-
{
193-
// Start a command line process if needed
194-
if (ultraProfilerOptions.ProgramPath is not null)
195-
{
196-
var processState = StartProcess(ultraProfilerOptions);
197-
processList.Add(processState.Process);
198-
// Append the pid for a single process that we are attaching to
199-
if (singleProcess is null)
200-
{
201-
baseName = $"{baseName}_pid_{processState.Process.Id}";
202-
}
203-
204-
singleProcess ??= processState.Process;
205-
}
206-
};
207-
208-
// If we have a delay, or we are asked to start paused, we start the process before the profiling starts
209-
bool hasExplicitProgramHasStarted = ultraProfilerOptions.DelayInSeconds != 0.0 || ultraProfilerOptions.Paused;
210-
if (hasExplicitProgramHasStarted)
211-
{
212-
startTheRequestedProgramIfRequired();
213-
}
214-
215-
// Wait for the process to start
216-
if (ultraProfilerOptions.Paused)
217-
{
218-
while (!ultraProfilerOptions.ShouldStartProfiling!() && !_cancelRequested && !_stopRequested)
219-
{
220-
}
221-
222-
// If we have a cancel request, we don't start the profiling
223-
if (_cancelRequested || _stopRequested)
224-
{
225-
throw new InvalidOperationException("CTRL+C requested");
226-
}
227-
}
228-
229-
await EnableProfiling(options, ultraProfilerOptions);
230-
231-
// If we haven't started the program yet, we start it now (for explicit program path)
232-
if (!hasExplicitProgramHasStarted)
233-
{
234-
startTheRequestedProgramIfRequired();
235-
}
236-
237-
foreach (var process in processList)
238-
{
239-
ultraProfilerOptions.LogProgress?.Invoke($"Start Profiling Process {process.ProcessName} ({process.Id})");
240-
}
241-
242-
// Collect the data until all processes have exited or there is a cancel request
243-
HashSet<Process> exitedProcessList = new();
244-
while (!_cancelRequested)
245-
{
246-
// Exit if we have reached the duration
247-
if (_profilerClock.Elapsed.TotalSeconds > ultraProfilerOptions.DurationInSeconds)
248-
{
249-
ultraProfilerOptions.LogProgress?.Invoke($"Stopping profiling, max duration reached at {ultraProfilerOptions.DurationInSeconds}s");
250-
break;
251-
}
252-
253-
if (_profilerClock.Elapsed.TotalMilliseconds - _lastTimeProgress.TotalMilliseconds > ultraProfilerOptions.UpdateLogAfterInMs)
254-
{
255-
var userFileNameLength = new FileInfo(userFileName).Length;
256-
var kernelFileNameLength = new FileInfo(kernelFileName).Length;
257-
var totalFileNameLength = userFileNameLength + kernelFileNameLength;
258-
259-
ultraProfilerOptions.LogStepProgress?.Invoke(singleProcess is not null
260-
? $"Profiling Process {singleProcess.ProcessName} ({singleProcess.Id}) - {(int)_profilerClock.Elapsed.TotalSeconds}s - {ByteSize.FromBytes(totalFileNameLength)}"
261-
: $"Profiling {processList.Count} Processes - {(int)_profilerClock.Elapsed.TotalSeconds}s - {ByteSize.FromBytes(totalFileNameLength)}");
262-
_lastTimeProgress = _profilerClock.Elapsed;
263-
}
264-
265-
await Task.Delay(ultraProfilerOptions.CheckDeltaTimeInMs);
266-
267-
foreach (var process in processList)
268-
{
269-
if (process.HasExited && exitedProcessList.Add(process))
270-
{
271-
ultraProfilerOptions.LogProgress?.Invoke($"Process {process.ProcessName} ({process.Id}) has exited");
272-
}
273-
}
274-
275-
if (exitedProcessList.Count == processList.Count)
276-
{
277-
break;
278-
}
279-
280-
} // Needed for JIT Compile code that was already compiled.
281-
282-
_kernelSession.Stop();
283-
_userSession.Stop();
284-
285-
ultraProfilerOptions.LogProgress?.Invoke(singleProcess is not null ? $"End Profiling Process" : $"End Profiling {processList.Count} Processes");
286-
287-
await WaitForStaleFile(userFileName, ultraProfilerOptions);
288-
await WaitForStaleFile(kernelFileName, ultraProfilerOptions);
289-
}
290-
}
291-
catch
292-
{
293-
// Delete intermediate files if we have an exception
294-
File.Delete(kernelFileName);
295-
File.Delete(userFileName);
296-
throw;
297-
}
298-
finally
299-
{
300-
_userSession = null;
301-
_kernelSession = null;
302-
_cleanCancel?.Set();
303-
}
304-
305-
if (_stopRequested)
306-
{
307-
throw new InvalidOperationException("CTRL+C requested");
308-
}
309-
310-
var rundownSession = $"{baseName}.rundown.etl";
311-
using (TraceEventSession clrRundownSession = new TraceEventSession($"{baseName}-rundown", rundownSession))
312-
{
313-
clrRundownSession.StopOnDispose = true;
314-
clrRundownSession.CircularBufferMB = 0;
315-
316-
ultraProfilerOptions.LogProgress?.Invoke($"Running CLR Rundown");
317-
318-
// The runtime does method rundown first then the module rundown. This means if you have a large
319-
// number of methods and method rundown does not complete you don't get ANYTHING. To avoid this
320-
// we first trigger all module (loader) rundown and then trigger the method rundown
321-
clrRundownSession.EnableProvider(
322-
ClrRundownTraceEventParser.ProviderGuid,
323-
TraceEventLevel.Verbose,
324-
(ulong)(ClrRundownTraceEventParser.Keywords.Loader | ClrRundownTraceEventParser.Keywords.ForceEndRundown), options);
325-
326-
await Task.Delay(500);
327-
328-
clrRundownSession.EnableProvider(
329-
ClrRundownTraceEventParser.ProviderGuid,
330-
TraceEventLevel.Verbose,
331-
(ulong)(ClrRundownTraceEventParser.Keywords.Default & ~ClrRundownTraceEventParser.Keywords.Loader), options);
332-
333-
await WaitForStaleFile(rundownSession, ultraProfilerOptions);
334-
}
335-
336-
if (_stopRequested)
337-
{
338-
throw new InvalidOperationException("CTRL+C requested");
339-
}
340-
341-
ultraProfilerOptions.LogProgress?.Invoke($"Merging ETL Files");
342-
// Merge file (and to force Volume mapping)
343-
var etlFinalFile = $"{ultraProfilerOptions.BaseOutputFileName ?? baseName}.etl";
344-
TraceEventSession.Merge([kernelFileName, userFileName, rundownSession], etlFinalFile);
345-
//TraceEventSession.Merge([kernelFileName, userFileName], $"{baseName}.etl");
346-
347-
if (_stopRequested)
348-
{
349-
throw new InvalidOperationException("CTRL+C requested");
350-
}
351-
352-
if (!ultraProfilerOptions.KeepEtlIntermediateFiles)
353-
{
354-
File.Delete(kernelFileName);
355-
File.Delete(userFileName);
356-
File.Delete(rundownSession);
357-
}
358-
359-
if (_stopRequested)
360-
{
361-
throw new InvalidOperationException("CTRL+C requested");
362-
}
363-
364-
var jsonFinalFile = await Convert(etlFinalFile, processList.Select(x => x.Id).ToList(), ultraProfilerOptions);
365-
366-
if (!ultraProfilerOptions.KeepMergedEtl)
367-
{
368-
File.Delete(etlFinalFile);
369-
var etlxFinalFile = Path.ChangeExtension(etlFinalFile, ".etlx");
370-
File.Delete(etlxFinalFile);
371-
}
372-
164+
var jsonFinalFile = await RunImpl(ultraProfilerOptions, processList, baseName, singleProcess);
373165
return jsonFinalFile;
374166
}
375167

168+
private protected abstract Task<string> RunImpl(EtwUltraProfilerOptions ultraProfilerOptions, List<Process> processList, string baseName, Process? singleProcess);
169+
376170
/// <summary>
377171
/// Converts the ETL file to a compressed JSON file in the Firefox Profiler format.
378172
/// </summary>
@@ -402,7 +196,9 @@ public async Task<string> Convert(string etlFile, List<int> pIds, EtwUltraProfil
402196
return jsonFinalFile;
403197
}
404198

405-
private async Task EnableProfiling(TraceEventProviderOptions options, EtwUltraProfilerOptions ultraProfilerOptions)
199+
private protected abstract Task EnableProfilingImpl(TraceEventProviderOptions options, EtwUltraProfilerOptions ultraProfilerOptions);
200+
201+
private protected async Task EnableProfiling(TraceEventProviderOptions options, EtwUltraProfilerOptions ultraProfilerOptions)
406202
{
407203
_profilerClock.Restart();
408204
while (!_cancelRequested)
@@ -434,47 +230,14 @@ private async Task EnableProfiling(TraceEventProviderOptions options, EtwUltraPr
434230
throw new InvalidOperationException("CTRL+C requested");
435231
}
436232

437-
_kernelSession!.StopOnDispose = true;
438-
_kernelSession.CircularBufferMB = 0;
439-
_kernelSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs;
440-
_kernelSession.StackCompression = false;
441-
442-
_userSession!.StopOnDispose = true;
443-
_userSession.CircularBufferMB = 0;
444-
_userSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs;
445-
_userSession.StackCompression = false;
446-
447-
var kernelEvents = KernelTraceEventParser.Keywords.Profile
448-
| KernelTraceEventParser.Keywords.ContextSwitch
449-
| KernelTraceEventParser.Keywords.ImageLoad
450-
| KernelTraceEventParser.Keywords.Process
451-
| KernelTraceEventParser.Keywords.Thread;
452-
_kernelSession.EnableKernelProvider(kernelEvents, KernelTraceEventParser.Keywords.Profile);
453-
454-
var jitEvents = ClrTraceEventParser.Keywords.JITSymbols |
455-
ClrTraceEventParser.Keywords.Exception |
456-
ClrTraceEventParser.Keywords.GC |
457-
ClrTraceEventParser.Keywords.GCHeapAndTypeNames |
458-
ClrTraceEventParser.Keywords.Interop |
459-
ClrTraceEventParser.Keywords.JITSymbols |
460-
ClrTraceEventParser.Keywords.Jit |
461-
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap |
462-
ClrTraceEventParser.Keywords.Loader |
463-
ClrTraceEventParser.Keywords.Stack |
464-
ClrTraceEventParser.Keywords.StartEnumeration;
465-
466-
_userSession.EnableProvider(
467-
ClrTraceEventParser.ProviderGuid,
468-
TraceEventLevel.Verbose, // For call stacks.
469-
(ulong)jitEvents, options);
470-
233+
await EnableProfilingImpl(options, ultraProfilerOptions);
471234

472235
// Reset the clock to account for the duration of the profiler
473236
_profilerClock.Restart();
474237
}
475238

476239

477-
private async Task WaitForStaleFile(string file, EtwUltraProfilerOptions options)
240+
private protected async Task WaitForStaleFile(string file, EtwUltraProfilerOptions options)
478241
{
479242
var clock = Stopwatch.StartNew();
480243
var startTime = clock.ElapsedMilliseconds;
@@ -511,7 +274,7 @@ private async Task WaitForStaleFile(string file, EtwUltraProfilerOptions options
511274
}
512275
}
513276

514-
private static ProcessState StartProcess(EtwUltraProfilerOptions ultraProfilerOptions)
277+
private protected static ProcessState StartProcess(EtwUltraProfilerOptions ultraProfilerOptions)
515278
{
516279
var mode = ultraProfilerOptions.ConsoleMode;
517280

@@ -599,7 +362,7 @@ private void WaitForCleanCancel()
599362
}
600363
}
601364

602-
private class ProcessState
365+
private protected class ProcessState
603366
{
604367
public ProcessState(Process process)
605368
{

0 commit comments

Comments
 (0)