From 67cba0453c5fd4923b239cafdc3f9952fde9f54f Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Tue, 26 Nov 2024 07:56:51 +0100 Subject: [PATCH] Add support for specifying an output base filename (#8) --- doc/readme.md | 34 ++++++++++------------- src/Ultra.Core/EtwUltraProfiler.cs | 7 +++-- src/Ultra.Core/EtwUltraProfilerOptions.cs | 13 +++++++++ src/Ultra/Program.cs | 6 ++++ 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/doc/readme.md b/doc/readme.md index a8d2a97..f183ced 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -137,25 +137,20 @@ This is the main command to profile an application - Only working within an elev Usage: ultra profile [Options] -h, -?, --help Show this message and exit + -o, --output=FILE The base output FILE name. Default is ultra__yyyy-MM-dd_HH_mm_ss. --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\alexa\AppData\Local\Temp\ - SymbolCache*https://msdl.microsoft.com/download/ - symbols;SRV*C:\Users\alexa\AppData\Local\Temp\ - SymbolCache*https://symbols.nuget.org/download/ + --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. + --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 @@ -168,11 +163,10 @@ It requires a list of PID in order to only produce results for these processes. Usage: ultra convert --pid xxx -h, -?, --help Show this message and exit + -o, --output=FILE The base output FILE name. Default is the input file name without the extension. --pid=PID The PID of the process - --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/ + --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 ce168f5..d20775c 100644 --- a/src/Ultra.Core/EtwUltraProfiler.cs +++ b/src/Ultra.Core/EtwUltraProfiler.cs @@ -289,7 +289,7 @@ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions) ultraProfilerOptions.LogProgress?.Invoke($"Merging ETL Files"); // Merge file (and to force Volume mapping) - var etlFinalFile = $"{baseName}.etl"; + var etlFinalFile = $"{ultraProfilerOptions.BaseOutputFileName ?? baseName}.etl"; TraceEventSession.Merge([kernelFileName, userFileName, rundownSession], etlFinalFile); //TraceEventSession.Merge([kernelFileName, userFileName], $"{baseName}.etl"); @@ -315,7 +315,8 @@ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions) if (!ultraProfilerOptions.KeepMergedEtl) { File.Delete(etlFinalFile); - File.Delete($"{baseName}.etlx"); + var etlxFinalFile = Path.ChangeExtension(etlFinalFile, ".etlx"); + File.Delete(etlxFinalFile); } return jsonFinalFile; @@ -333,7 +334,7 @@ public async Task Convert(string etlFile, List pIds, EtwUltraProfil var directory = Path.GetDirectoryName(etlFile); var etlFileNameWithoutExtension = Path.GetFileNameWithoutExtension(etlFile); - var jsonFinalFile = $"{etlFileNameWithoutExtension}.json.gz"; + var jsonFinalFile = $"{ultraProfilerOptions.BaseOutputFileName ?? etlFileNameWithoutExtension}.json.gz"; ultraProfilerOptions.LogProgress?.Invoke($"Converting to Firefox Profiler JSON"); await using var stream = File.Create(jsonFinalFile); await using var gzipStream = new GZipStream(stream, CompressionLevel.Optimal); diff --git a/src/Ultra.Core/EtwUltraProfilerOptions.cs b/src/Ultra.Core/EtwUltraProfilerOptions.cs index 0a0cf23..45a4b29 100644 --- a/src/Ultra.Core/EtwUltraProfilerOptions.cs +++ b/src/Ultra.Core/EtwUltraProfilerOptions.cs @@ -52,6 +52,19 @@ public EtwUltraProfilerOptions() public string? SymbolPathText { get; set; } + public string? BaseOutputFileName { get; set; } + + public void EnsureDirectoryForBaseOutputFileName() + { + if (BaseOutputFileName == null) return; + + var directory = Path.GetDirectoryName(BaseOutputFileName); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + } + public SymbolPath GetCachedSymbolPath() { var symbolPath = new SymbolPath(); diff --git a/src/Ultra/Program.cs b/src/Ultra/Program.cs index 20c29d0..ced5f77 100644 --- a/src/Ultra/Program.cs +++ b/src/Ultra/Program.cs @@ -39,6 +39,7 @@ static async Task Main(string[] args) new CommandUsage("Usage: {NAME} [Options] "), _, new HelpOption(), + { "o|output=", "The base output {FILE} name. Default is ultra__yyyy-MM-dd_HH_mm_ss.", v => options.BaseOutputFileName = v }, { "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 }, @@ -91,6 +92,8 @@ static async Task Main(string[] args) options.Arguments.AddRange(arguments.AsSpan().Slice(1)); } + options.EnsureDirectoryForBaseOutputFileName(); + var etwProfiler = new EtwUltraProfiler(); Console.CancelKeyPress += (sender, eventArgs) => { @@ -262,6 +265,7 @@ await AnsiConsole.Live(statusTable.Table) new CommandUsage("Usage: {NAME} --pid xxx "), _, new HelpOption(), + { "o|output=", "The base output {FILE} name. Default is the input file name without the extension.", v => options.BaseOutputFileName = v }, { "pid=", "The {PID} of the process", (int pid) => { pidList.Add(pid); } }, { "symbol-path=", $"The {{VALUE}} of symbol path. The default value is `{options.GetCachedSymbolPath()}`.", v => options.SymbolPathText = v }, async (ctx, arguments) => @@ -328,6 +332,8 @@ await AnsiConsole.Status() } }; + options.EnsureDirectoryForBaseOutputFileName(); + fileOutput = await etwProfiler.Convert(etlFile, pidList, options); } finally