|
5 | 5 | using System.Diagnostics;
|
6 | 6 | using System.IO.Compression;
|
7 | 7 | 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; |
12 | 8 | using Microsoft.Diagnostics.Tracing.Session;
|
13 | 9 |
|
14 | 10 | namespace Ultra.Core;
|
15 | 11 |
|
16 | 12 | /// <summary>
|
17 | 13 | /// A profiler that uses Event Tracing for Windows (ETW) to collect performance data.
|
18 | 14 | /// </summary>
|
19 |
| -public class EtwUltraProfiler : IDisposable |
| 15 | +public abstract class EtwUltraProfiler : IDisposable |
20 | 16 | {
|
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; |
28 | 22 |
|
29 | 23 | /// <summary>
|
30 | 24 | /// Initializes a new instance of the <see cref="EtwUltraProfiler"/> class.
|
31 | 25 | /// </summary>
|
32 |
| - public EtwUltraProfiler() |
| 26 | + protected EtwUltraProfiler() |
33 | 27 | {
|
34 | 28 | _profilerClock = new Stopwatch();
|
35 | 29 | }
|
36 | 30 |
|
| 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 | + |
37 | 46 | /// <summary>
|
38 | 47 | /// Requests to cancel the profiling session.
|
39 | 48 | /// </summary>
|
@@ -61,14 +70,13 @@ public bool Cancel()
|
61 | 70 | /// </summary>
|
62 | 71 | public void Dispose()
|
63 | 72 | {
|
64 |
| - _userSession?.Dispose(); |
65 |
| - _userSession = null; |
66 |
| - _kernelSession?.Dispose(); |
67 |
| - _kernelSession = null; |
| 73 | + DisposeImpl(); |
68 | 74 | _cleanCancel?.Dispose();
|
69 | 75 | _cleanCancel = null;
|
70 | 76 | }
|
71 | 77 |
|
| 78 | + private protected abstract void DisposeImpl(); |
| 79 | + |
72 | 80 | /// <summary>
|
73 | 81 | /// Determines whether the current process is running with elevated privileges.
|
74 | 82 | /// </summary>
|
@@ -153,226 +161,12 @@ public async Task<string> Run(EtwUltraProfilerOptions ultraProfilerOptions)
|
153 | 161 | baseName = $"{baseName}_pid_{singleProcess.Id}";
|
154 | 162 | }
|
155 | 163 |
|
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); |
373 | 165 | return jsonFinalFile;
|
374 | 166 | }
|
375 | 167 |
|
| 168 | + private protected abstract Task<string> RunImpl(EtwUltraProfilerOptions ultraProfilerOptions, List<Process> processList, string baseName, Process? singleProcess); |
| 169 | + |
376 | 170 | /// <summary>
|
377 | 171 | /// Converts the ETL file to a compressed JSON file in the Firefox Profiler format.
|
378 | 172 | /// </summary>
|
@@ -402,7 +196,9 @@ public async Task<string> Convert(string etlFile, List<int> pIds, EtwUltraProfil
|
402 | 196 | return jsonFinalFile;
|
403 | 197 | }
|
404 | 198 |
|
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) |
406 | 202 | {
|
407 | 203 | _profilerClock.Restart();
|
408 | 204 | while (!_cancelRequested)
|
@@ -434,47 +230,14 @@ private async Task EnableProfiling(TraceEventProviderOptions options, EtwUltraPr
|
434 | 230 | throw new InvalidOperationException("CTRL+C requested");
|
435 | 231 | }
|
436 | 232 |
|
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); |
471 | 234 |
|
472 | 235 | // Reset the clock to account for the duration of the profiler
|
473 | 236 | _profilerClock.Restart();
|
474 | 237 | }
|
475 | 238 |
|
476 | 239 |
|
477 |
| - private async Task WaitForStaleFile(string file, EtwUltraProfilerOptions options) |
| 240 | + private protected async Task WaitForStaleFile(string file, EtwUltraProfilerOptions options) |
478 | 241 | {
|
479 | 242 | var clock = Stopwatch.StartNew();
|
480 | 243 | var startTime = clock.ElapsedMilliseconds;
|
@@ -511,7 +274,7 @@ private async Task WaitForStaleFile(string file, EtwUltraProfilerOptions options
|
511 | 274 | }
|
512 | 275 | }
|
513 | 276 |
|
514 |
| - private static ProcessState StartProcess(EtwUltraProfilerOptions ultraProfilerOptions) |
| 277 | + private protected static ProcessState StartProcess(EtwUltraProfilerOptions ultraProfilerOptions) |
515 | 278 | {
|
516 | 279 | var mode = ultraProfilerOptions.ConsoleMode;
|
517 | 280 |
|
@@ -599,7 +362,7 @@ private void WaitForCleanCancel()
|
599 | 362 | }
|
600 | 363 | }
|
601 | 364 |
|
602 |
| - private class ProcessState |
| 365 | + private protected class ProcessState |
603 | 366 | {
|
604 | 367 | public ProcessState(Process process)
|
605 | 368 | {
|
|
0 commit comments