From 31e7fae7d98dc97f87a1886878b95a4fe3caa7e2 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Sun, 24 Nov 2024 17:37:07 +0100 Subject: [PATCH] Add support for raw and live output (#7) --- doc/profile_mode_live.png | Bin 0 -> 59500 bytes doc/readme.md | 43 ++- src/Ultra.Core/EtwUltraProfiler.cs | 114 ++++++-- src/Ultra.Core/EtwUltraProfilerConsoleMode.cs | 26 ++ src/Ultra.Core/EtwUltraProfilerOptions.cs | 6 + src/Ultra.Example/Program.cs | 9 + src/Ultra/Program.cs | 253 +++++++++++++++--- 7 files changed, 391 insertions(+), 60 deletions(-) create mode 100644 doc/profile_mode_live.png create mode 100644 src/Ultra.Core/EtwUltraProfilerConsoleMode.cs diff --git a/doc/profile_mode_live.png b/doc/profile_mode_live.png new file mode 100644 index 0000000000000000000000000000000000000000..ed7441e319d2c6e03cd592175c16d4c5d44671c7 GIT binary patch literal 59500 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 Ouyi!^)yvhMhy4dQ60FJq literal 0 HcmV?d00001 diff --git a/doc/readme.md b/doc/readme.md index b2133d7..d45f621 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -16,6 +16,22 @@ $ ultra.exe profile -- my_command.exe arg0 arg1 arg2... This will create a `ultra_my_command_..._.json.gz` trace file in the current directory. +By default, ultra won't show the stdout/stderr of the program launched. You can change this behavior by specifying the `--mode` option: + +- `silent` (default): won't mix program's output +- `raw`: will mix ultra and program output together in a raw output. Ultra output will be prefixed at the start of a line with `>>ultra::` +- `live`: will mix ultra and program output within a live table + +For example, a profile with `live` mode: + +```console +$ ultra.exe profile --mode live -- my_command.exe arg0 arg1 arg2... +``` + +will display the following live table when running your process: + +![Live ultra mode](profile_mode_live.png) + When attaching an existing process, you can pass directly a PID to ultra.exe: ```console @@ -116,11 +132,24 @@ Usage: ultra profile [Options] -h, -?, --help Show this message and exit --pid=PID The PID of the process to attach the profiler to. - --sampling-interval=VALUE The VALUE of the sample interval in ms. Default is 8190Hz = 0.122ms. - --symbol-path=VALUE The VALUE of symbol path. The default value is `;SRV*C:\Users\xoofx\AppData\Local\Temp\SymbolCache*https://msdl.microsoft.com/download/symbols;SRV*C:\Users\xoofx\AppData\Local\Temp\SymbolCache*https:// - symbols.nuget.org/download/symbols`. + --sampling-interval=VALUE The VALUE of the sample interval in ms. Default + is 8190Hz = 0.122ms. + --symbol-path=VALUE The VALUE of symbol path. The default value is `; + SRV*C:\Users\alexa\AppData\Local\Temp\ + SymbolCache*https://msdl.microsoft.com/download/ + symbols;SRV*C:\Users\alexa\AppData\Local\Temp\ + SymbolCache*https://symbols.nuget.org/download/ + symbols`. --keep-merged-etl-file Keep the merged ETL file. --keep-intermediate-etl-files Keep the intermediate ETL files before merging. + --mode=VALUE 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. ``` ## Convert @@ -134,6 +163,10 @@ Usage: ultra convert --pid xxx -h, -?, --help Show this message and exit --pid=PID The PID of the process - --symbol-path=VALUE The VALUE of symbol path. The default value is `;SRV*C:\Users\xoofx\AppData\Local\Temp\SymbolCache*https://msdl.microsoft.com/download/symbols;SRV*C:\Users\xoofx\AppData\Local\Temp\SymbolCache*https:// - symbols.nuget.org/download/symbols`. + --symbol-path=VALUE The VALUE of symbol path. The default value is `; + SRV*C:\Users\alexa\AppData\Local\Temp\ + SymbolCache*https://msdl.microsoft.com/download/ + symbols;SRV*C:\Users\alexa\AppData\Local\Temp\ + SymbolCache*https://symbols.nuget.org/download/ + symbols`. ``` diff --git a/src/Ultra.Core/EtwUltraProfiler.cs b/src/Ultra.Core/EtwUltraProfiler.cs index 5b72be1..b5f07f5 100644 --- a/src/Ultra.Core/EtwUltraProfiler.cs +++ b/src/Ultra.Core/EtwUltraProfiler.cs @@ -104,7 +104,7 @@ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions) // Append the pid for a single process that we are attaching to if (singleProcess is not null) { - baseName = $"{baseName}_{singleProcess.Id}"; + baseName = $"{baseName}_pid_{singleProcess.Id}"; } var options = new TraceEventProviderOptions() @@ -181,23 +181,14 @@ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions) // Start a command line process if needed if (ultraProfilerOptions.ProgramPath is not null) { - var startInfo = new ProcessStartInfo + var processState = StartProcess(ultraProfilerOptions); + processList.Add(processState.Process); + // Append the pid for a single process that we are attaching to + if (singleProcess is null) { - FileName = ultraProfilerOptions.ProgramPath, - UseShellExecute = true, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden - }; - - foreach (var arg in ultraProfilerOptions.Arguments) - { - startInfo.ArgumentList.Add(arg); + baseName = $"{baseName}_pid_{processState.Process.Id}"; } - - ultraProfilerOptions.LogProgress?.Invoke($"Starting Process {startInfo.FileName} {string.Join(" ", startInfo.ArgumentList)}"); - var process = System.Diagnostics.Process.Start(startInfo)!; - processList.Add(process); - singleProcess ??= process; + singleProcess ??= processState.Process; } foreach (var process in processList) @@ -391,6 +382,97 @@ private async Task WaitForStaleFile(string file, EtwUltraProfilerOptions options } } + + private static ProcessState StartProcess(EtwUltraProfilerOptions ultraProfilerOptions) + { + var mode = ultraProfilerOptions.ConsoleMode; + + var process = new Process(); + + var startInfo = process.StartInfo; + startInfo.FileName = ultraProfilerOptions.ProgramPath; + + foreach (var arg in ultraProfilerOptions.Arguments) + { + startInfo.ArgumentList.Add(arg); + } + + ultraProfilerOptions.LogProgress?.Invoke($"Starting Process {startInfo.FileName} {string.Join(" ", startInfo.ArgumentList)}"); + + if (mode == EtwUltraProfilerConsoleMode.Silent) + { + startInfo.UseShellExecute = true; + startInfo.CreateNoWindow = true; + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + + process.Start(); + } + else + { + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.RedirectStandardInput = true; + + process.OutputDataReceived += (sender, args) => + { + if (args.Data != null) + { + ultraProfilerOptions.ProgramLogStdout?.Invoke(args.Data); + } + }; + + process.ErrorDataReceived += (sender, args) => + { + if (args.Data != null) + { + ultraProfilerOptions.ProgramLogStderr?.Invoke(args.Data); + } + }; + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + } + + var state = new ProcessState(process); + + // Make sure to call WaitForExit + var thread = new Thread(() => + { + try + { + process.WaitForExit(); + state.HasExited = true; + } + catch + { + // ignore + } + }) + { + Name = "Ultra-ProcessWaitForExit", + IsBackground = true + }; + thread.Start(); + + return state; + } + + private class ProcessState + { + public ProcessState(Process process) + { + Process = process; + } + + public readonly Process Process; + + public bool HasExited; + } + public void Dispose() { _userSession?.Dispose(); diff --git a/src/Ultra.Core/EtwUltraProfilerConsoleMode.cs b/src/Ultra.Core/EtwUltraProfilerConsoleMode.cs new file mode 100644 index 0000000..922ca75 --- /dev/null +++ b/src/Ultra.Core/EtwUltraProfilerConsoleMode.cs @@ -0,0 +1,26 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// Licensed under the BSD-Clause 2 license. +// See license.txt file in the project root for full license information. + +namespace Ultra.Core; + +/// +/// The mode of the console output. +/// +public enum EtwUltraProfilerConsoleMode +{ + /// + /// No console output from the program started. + /// + Silent, + + /// + /// Redirect the console output from the program started to the current console, but live progress using Spectre.Console is disabled. + /// + Raw, + + /// + /// Redirect the last lines of the console output from the program started to the live progress using Spectre.Console. + /// + Live, +} \ No newline at end of file diff --git a/src/Ultra.Core/EtwUltraProfilerOptions.cs b/src/Ultra.Core/EtwUltraProfilerOptions.cs index acd5383..d7547f1 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 EtwUltraProfilerConsoleMode ConsoleMode { get; set; } + public Action? LogProgress; public Action? LogStepProgress; @@ -38,6 +40,10 @@ public EtwUltraProfilerOptions() public Action? WaitingFileToCompleteTimeOut; + public Action? ProgramLogStdout; + + public Action? ProgramLogStderr; + public bool KeepEtlIntermediateFiles { get; set; } public bool KeepMergedEtl { get; set; } diff --git a/src/Ultra.Example/Program.cs b/src/Ultra.Example/Program.cs index 0129a0c..ec3ed27 100644 --- a/src/Ultra.Example/Program.cs +++ b/src/Ultra.Example/Program.cs @@ -9,6 +9,10 @@ for (int i = 0; i < countBenchMarkdig; i++) { var html = Markdig.Markdown.ToHtml(md); + if (i % 100 == 0 && i > 0) + { + Console.WriteLine($"Markdig {i} conversions done"); + } } }; @@ -24,6 +28,11 @@ for (int i = 0; i < countBenchScriban; i++) { var text = template.Render(new { values = values }); + + if (i % 1000 == 0 && i > 0) + { + Console.WriteLine($"Scriban {i} conversions done"); + } } }; diff --git a/src/Ultra/Program.cs b/src/Ultra/Program.cs index 399bbda..20c29d0 100644 --- a/src/Ultra/Program.cs +++ b/src/Ultra/Program.cs @@ -3,9 +3,11 @@ using System.Text; using ByteSizeLib; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Spectre.Console; using Ultra.Core; using XenoAtom.CommandLine; +using static System.Net.Mime.MediaTypeNames; namespace Ultra; @@ -42,7 +44,23 @@ static async Task Main(string[] args) { "symbol-path=", $"The {{VALUE}} of symbol path. The default value is `{options.GetCachedSymbolPath()}`.", v => options.SymbolPathText = v }, { "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 }, - // Action for the commit commandd + { "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 => + { + if ("raw".Equals(v, StringComparison.OrdinalIgnoreCase)) + { + options.ConsoleMode = EtwUltraProfilerConsoleMode.Raw; + } + else if ("live".Equals(v, StringComparison.OrdinalIgnoreCase)) + { + options.ConsoleMode = EtwUltraProfilerConsoleMode.Live; + } + else + { + options.ConsoleMode = EtwUltraProfilerConsoleMode.Silent; + } + } + }, + // Action for the commit command async (ctx, arguments) => { if (arguments.Length == 0 && pidList.Count == 0) @@ -59,76 +77,176 @@ static async Task Main(string[] args) string? fileOutput = null; - await AnsiConsole.Status() - .Spinner(Spinner.Known.Default) - .SpinnerStyle(Style.Parse("red")) - .StartAsync("Profiling", async statusCtx => + + // Add the pid passed as options + options.ProcessIds.AddRange(pidList); + + if (arguments.Length == 1 && int.TryParse(arguments[0], out var pid)) + { + options.ProcessIds.Add(pid); + } + else if (arguments.Length > 0) + { + options.ProgramPath = arguments[0]; + options.Arguments.AddRange(arguments.AsSpan().Slice(1)); + } + + var etwProfiler = new EtwUltraProfiler(); + Console.CancelKeyPress += (sender, eventArgs) => + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("[darkorange]Cancelled via CTRL+C[/]"); + eventArgs.Cancel = true; + // ReSharper disable once AccessToDisposedClosure + if (etwProfiler.Cancel()) + { + AnsiConsole.MarkupLine("[red]Stopped via CTRL+C[/]"); + } + }; + + if (options.ConsoleMode == EtwUltraProfilerConsoleMode.Silent) + { + await AnsiConsole.Status() + .Spinner(Spinner.Known.Default) + .SpinnerStyle(Style.Parse("red")) + .StartAsync("Profiling", async statusCtx => + { + string? previousText = null; + + options.LogStepProgress = (text) => + { + if (verbose && previousText is not null && previousText != text) + { + AnsiConsole.MarkupLine($"{Markup.Escape(previousText)} [green]\u2713[/]"); + previousText = text; + } + + statusCtx.Status(Markup.Escape(text)); + }; + options.LogProgress = (text) => + { + if (verbose && previousText != null && previousText != text) + { + AnsiConsole.MarkupLine($"{Markup.Escape(previousText)} [green]\u2713[/]"); + } + + statusCtx.Status(Markup.Escape(text)); + previousText = text; + }; + options.WaitingFileToComplete = (file) => { statusCtx.Status($"Waiting for {Markup.Escape(file)} to complete"); }; + options.WaitingFileToCompleteTimeOut = (file) => { statusCtx.Status($"Timeout waiting for {Markup.Escape(file)} to complete"); }; + + try + { + fileOutput = await etwProfiler.Run(options); + } + finally + { + etwProfiler.Dispose(); + } + + if (verbose) + { + options.LogProgress.Invoke("Profiling Done"); + } + } + ); + } + else if (options.ConsoleMode == EtwUltraProfilerConsoleMode.Raw) + { + options.LogStepProgress = s => AnsiConsole.WriteLine($">>ultra::{s}"); + options.LogProgress = s => AnsiConsole.WriteLine($">>ultra::{s}"); + options.WaitingFileToComplete = (file) => { AnsiConsole.WriteLine($">>ultra::Waiting for {Markup.Escape(file)} to complete"); }; + options.WaitingFileToCompleteTimeOut = (file) => { AnsiConsole.WriteLine($">>ultra::Timeout waiting for {Markup.Escape(file)} to complete"); }; + options.ProgramLogStdout = AnsiConsole.WriteLine; + options.ProgramLogStderr = AnsiConsole.WriteLine; + + try + { + fileOutput = await etwProfiler.Run(options); + } + finally + { + etwProfiler.Dispose(); + } + } + else if (options.ConsoleMode == EtwUltraProfilerConsoleMode.Live) + { + var statusTable = new StatusTable(); + + await AnsiConsole.Live(statusTable.Table) + // .AutoClear(true) // No auto clear to keep the output (e.g. in case the program shows errors in its stdout/stderr) + .StartAsync(async liveCtx => { string? previousText = null; - + options.LogStepProgress = (text) => { if (verbose && previousText is not null && previousText != text) { - AnsiConsole.MarkupLine($"{Markup.Escape(previousText)} [green]\u2713[/]"); + statusTable.LogText($"{Markup.Escape(previousText)} [green]\u2713[/]"); previousText = text; } - statusCtx.Status(Markup.Escape(text)); + statusTable.Status(Markup.Escape(text)); + + statusTable.UpdateTable(); + liveCtx.Refresh(); }; + options.LogProgress = (text) => { if (verbose && previousText != null && previousText != text) { - AnsiConsole.MarkupLine($"{Markup.Escape(previousText)} [green]\u2713[/]"); + statusTable.LogText($"{Markup.Escape(previousText)} [green]\u2713[/]"); } - statusCtx.Status(Markup.Escape(text)); + statusTable.Status(Markup.Escape(text)); previousText = text; + + statusTable.UpdateTable(); + liveCtx.Refresh(); }; - options.WaitingFileToComplete = (file) => { statusCtx.Status($"Waiting for {Markup.Escape(file)} to complete"); }; - options.WaitingFileToCompleteTimeOut = (file) => { statusCtx.Status($"Timeout waiting for {Markup.Escape(file)} to complete"); }; - // Add the pid passed as options - options.ProcessIds.AddRange(pidList); - - if (arguments.Length == 1 && int.TryParse(arguments[0], out var pid)) + options.WaitingFileToComplete = (file) => { - options.ProcessIds.Add(pid); - } - else if (arguments.Length > 0) + statusTable.Status($"Waiting for {Markup.Escape(file)} to complete"); + statusTable.UpdateTable(); + liveCtx.Refresh(); + }; + + options.WaitingFileToCompleteTimeOut = (file) => { - options.ProgramPath = arguments[0]; - options.Arguments.AddRange(arguments.AsSpan().Slice(1)); - } + statusTable.Status($"Timeout waiting for {Markup.Escape(file)} to complete"); + statusTable.UpdateTable(); + liveCtx.Refresh(); + }; + + options.ProgramLogStdout = (text) => + { + statusTable.LogText(text); + statusTable.UpdateTable(); + liveCtx.Refresh(); + }; + + options.ProgramLogStderr = (text) => + { + statusTable.LogText(text); + statusTable.UpdateTable(); + liveCtx.Refresh(); + }; - var etwProfiler = new EtwUltraProfiler(); try { - Console.CancelKeyPress += (sender, eventArgs) => - { - AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine("[darkorange]Cancelled via CTRL+C[/]"); - eventArgs.Cancel = true; - if (etwProfiler.Cancel()) - { - AnsiConsole.MarkupLine("[red]Stopped via CTRL+C[/]"); - } - }; - fileOutput = await etwProfiler.Run(options); } finally { etwProfiler.Dispose(); } - - if (verbose) - { - options.LogProgress.Invoke("Profiling Done"); - } } ); + } if (fileOutput != null) { @@ -255,4 +373,61 @@ await AnsiConsole.Status() return 1; } } + + private class StatusTable + { + private string? _statusText; + private readonly Queue _logLines = new(); + private const int MaxLogLines = 10; + private int _spinnerStep; + private readonly Style _spinnerStyle; + + public StatusTable() + { + Table = new Table(); + Table.AddColumn(new TableColumn("Status")); + _spinnerStyle = Style.Parse("red"); + } + + public Table Table { get; } + + public void LogText(string text) + { + if (_logLines.Count > MaxLogLines) + { + _logLines.Dequeue(); + } + + _logLines.Enqueue(text); + } + + public void Status(string text) + { + _statusText = text; + } + + public void UpdateTable() + { + Table.Rows.Clear(); + + if (_logLines.Count > 0) + { + var rows = new Rows(_logLines.Select(x => new Markup(x))); + Table.AddRow(rows); + } + + if (_statusText != null) + { + var tableColumn = Table.Columns[0]; + + var spinnerStep = _spinnerStep; + var spinnerText = Spinner.Known.Default.Frames[spinnerStep]; + _spinnerStep = (_spinnerStep + 1) % Spinner.Known.Default.Frames.Count; + + tableColumn.Header = new Markup($"[red]{spinnerText}[/] Status"); + tableColumn.Footer = new Markup(_statusText); + Table.ShowFooters = true; + } + } + } } \ No newline at end of file