From 96a2a20c1bfe89829cf0e7ed54eadc2159cfc8a3 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Wed, 27 Nov 2024 09:23:41 +0100 Subject: [PATCH] Add support for paused. Add that CTRL+C can be pressed (#12) --- src/Ultra.Core/EtwUltraProfiler.cs | 95 +++++++++++++++-------- src/Ultra.Core/EtwUltraProfilerOptions.cs | 4 + src/Ultra/Program.cs | 38 ++++++++- 3 files changed, 103 insertions(+), 34 deletions(-) diff --git a/src/Ultra.Core/EtwUltraProfiler.cs b/src/Ultra.Core/EtwUltraProfiler.cs index d20775c..1b89e36 100644 --- a/src/Ultra.Core/EtwUltraProfiler.cs +++ b/src/Ultra.Core/EtwUltraProfiler.cs @@ -57,6 +57,11 @@ public static bool IsElevated() public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions) { + if (ultraProfilerOptions.Paused && ultraProfilerOptions.ShouldStartProfiling is null) + { + throw new ArgumentException("ShouldStartProfiling is required when Paused is set to true"); + } + List processList = new List(); if (ultraProfilerOptions.ProcessIds.Count > 0) { @@ -142,39 +147,10 @@ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions) using (_userSession) using (_kernelSession) { - _kernelSession.StopOnDispose = true; - _kernelSession.CircularBufferMB = 0; - _kernelSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs; - _kernelSession.StackCompression = false; - - _userSession.StopOnDispose = true; - _userSession.CircularBufferMB = 0; - _userSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs; - _userSession.StackCompression = false; - - var kernelEvents = KernelTraceEventParser.Keywords.Profile - | KernelTraceEventParser.Keywords.ContextSwitch - | KernelTraceEventParser.Keywords.ImageLoad - | KernelTraceEventParser.Keywords.Process - | KernelTraceEventParser.Keywords.Thread; - _kernelSession.EnableKernelProvider(kernelEvents, KernelTraceEventParser.Keywords.Profile); - - var jitEvents = ClrTraceEventParser.Keywords.JITSymbols | - ClrTraceEventParser.Keywords.Exception | - ClrTraceEventParser.Keywords.GC | - ClrTraceEventParser.Keywords.GCHeapAndTypeNames | - ClrTraceEventParser.Keywords.Interop | - ClrTraceEventParser.Keywords.JITSymbols | - ClrTraceEventParser.Keywords.Jit | - ClrTraceEventParser.Keywords.JittedMethodILToNativeMap | - ClrTraceEventParser.Keywords.Loader | - ClrTraceEventParser.Keywords.Stack | - ClrTraceEventParser.Keywords.StartEnumeration; - - _userSession.EnableProvider( - ClrTraceEventParser.ProviderGuid, - TraceEventLevel.Verbose, // For call stacks. - (ulong)jitEvents, options); + if (!ultraProfilerOptions.Paused) + { + EnableProfiling(options, ultraProfilerOptions); + } HashSet exitedProcessList = new(); @@ -191,6 +167,22 @@ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions) singleProcess ??= processState.Process; } + // Wait for the process to start + if (ultraProfilerOptions.Paused) + { + while (!ultraProfilerOptions.ShouldStartProfiling!() && !_cancelRequested && !_stopRequested) + { + } + + // If we have a cancel request, we don't start the profiling + if (_cancelRequested || _stopRequested) + { + throw new InvalidOperationException("CTRL+C requested"); + } + + EnableProfiling(options, ultraProfilerOptions); + } + foreach (var process in processList) { ultraProfilerOptions.LogProgress?.Invoke($"Start Profiling Process {process.ProcessName} ({process.Id})"); @@ -322,6 +314,43 @@ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions) return jsonFinalFile; } + private void EnableProfiling(TraceEventProviderOptions options, EtwUltraProfilerOptions ultraProfilerOptions) + { + _kernelSession.StopOnDispose = true; + _kernelSession.CircularBufferMB = 0; + _kernelSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs; + _kernelSession.StackCompression = false; + + _userSession.StopOnDispose = true; + _userSession.CircularBufferMB = 0; + _userSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs; + _userSession.StackCompression = false; + + var kernelEvents = KernelTraceEventParser.Keywords.Profile + | KernelTraceEventParser.Keywords.ContextSwitch + | KernelTraceEventParser.Keywords.ImageLoad + | KernelTraceEventParser.Keywords.Process + | KernelTraceEventParser.Keywords.Thread; + _kernelSession.EnableKernelProvider(kernelEvents, KernelTraceEventParser.Keywords.Profile); + + var jitEvents = ClrTraceEventParser.Keywords.JITSymbols | + ClrTraceEventParser.Keywords.Exception | + ClrTraceEventParser.Keywords.GC | + ClrTraceEventParser.Keywords.GCHeapAndTypeNames | + ClrTraceEventParser.Keywords.Interop | + ClrTraceEventParser.Keywords.JITSymbols | + ClrTraceEventParser.Keywords.Jit | + ClrTraceEventParser.Keywords.JittedMethodILToNativeMap | + ClrTraceEventParser.Keywords.Loader | + ClrTraceEventParser.Keywords.Stack | + ClrTraceEventParser.Keywords.StartEnumeration; + + _userSession.EnableProvider( + ClrTraceEventParser.ProviderGuid, + TraceEventLevel.Verbose, // For call stacks. + (ulong)jitEvents, options); + } + public async Task Convert(string etlFile, List pIds, EtwUltraProfilerOptions ultraProfilerOptions) { var etlProcessor = new EtwConverterToFirefox(); diff --git a/src/Ultra.Core/EtwUltraProfilerOptions.cs b/src/Ultra.Core/EtwUltraProfilerOptions.cs index 45a4b29..65588af 100644 --- a/src/Ultra.Core/EtwUltraProfilerOptions.cs +++ b/src/Ultra.Core/EtwUltraProfilerOptions.cs @@ -30,6 +30,8 @@ public EtwUltraProfilerOptions() public int TimeOutAfterInMs { get; set; } + public bool Paused { get; set; } + public EtwUltraProfilerConsoleMode ConsoleMode { get; set; } public Action? LogProgress; @@ -44,6 +46,8 @@ public EtwUltraProfilerOptions() public Action? ProgramLogStderr; + public Func? ShouldStartProfiling { get; set; } + public bool KeepEtlIntermediateFiles { get; set; } public bool KeepMergedEtl { get; set; } diff --git a/src/Ultra/Program.cs b/src/Ultra/Program.cs index ced5f77..3349c9c 100644 --- a/src/Ultra/Program.cs +++ b/src/Ultra/Program.cs @@ -43,6 +43,7 @@ static async Task Main(string[] args) { "pid=", "The {PID} of the process to attach the profiler to.", (int pid) => { pidList.Add(pid); } }, { "sampling-interval=", $"The {{VALUE}} of the sample interval in ms. Default is 8190Hz = {options.CpuSamplingIntervalInMs:0.000}ms.", (float v) => options.CpuSamplingIntervalInMs = v }, { "symbol-path=", $"The {{VALUE}} of symbol path. The default value is `{options.GetCachedSymbolPath()}`.", v => options.SymbolPathText = v }, + { "paused", "Launch the profiler paused and wait for SPACE or ENTER keys to be pressed.", v => options.Paused = v is not null }, { "keep-merged-etl-file", "Keep the merged ETL file.", v => options.KeepMergedEtl = v is not null }, { "keep-intermediate-etl-files", "Keep the intermediate ETL files before merging.", v => options.KeepEtlIntermediateFiles = v is not null }, { "mode=", "Defines how the stdout/stderr of a program explicitly started by ultra should be integrated in its output. Default is `silent` which will not mix program's output. The other options are: `raw` is going to mix ultra and program output together in a raw output. `live` is going to mix ultra and program output within a live table.", v => @@ -92,9 +93,12 @@ static async Task Main(string[] args) options.Arguments.AddRange(arguments.AsSpan().Slice(1)); } + AnsiConsole.MarkupLine($"[green]You can press CTRL+C to stop profiling before the end of the process[/]"); + options.EnsureDirectoryForBaseOutputFileName(); var etwProfiler = new EtwUltraProfiler(); + Console.CancelKeyPress += (sender, eventArgs) => { AnsiConsole.WriteLine(); @@ -106,7 +110,39 @@ static async Task Main(string[] args) AnsiConsole.MarkupLine("[red]Stopped via CTRL+C[/]"); } }; - + + // Handle paused + if (options.Paused) + { + Console.TreatControlCAsInput = true; + + options.ShouldStartProfiling = () => + { + AnsiConsole.MarkupLine("[green]Press SPACE or ENTER to start profiling[/]"); + var key = Console.ReadKey(true); + bool startProfiling = key.Key == ConsoleKey.Spacebar || key.Key == ConsoleKey.Enter; + + bool isCtrlC = key.Modifiers == ConsoleModifiers.Control && key.Key == ConsoleKey.C; + if (startProfiling || isCtrlC) + { + // Restore the default behavior so that CancelKeyPress will be called later if CTRL+C is pressed + Console.TreatControlCAsInput = false; + } + + if (isCtrlC) + { + AnsiConsole.MarkupLine("[darkorange]Cancelled via CTRL+C[/]"); + etwProfiler.Cancel(); + } + else if (!startProfiling) + { + AnsiConsole.MarkupLine($"[darkorange]Key pressed {key.Modifiers} {key.Key}[/]"); + } + + return startProfiling; + }; + } + if (options.ConsoleMode == EtwUltraProfilerConsoleMode.Silent) { await AnsiConsole.Status()