From 03e8a6bfafdc41ced52c02c65ce6180894bca548 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Fri, 13 Dec 2024 09:53:11 +0100 Subject: [PATCH] Refactor code to support decoding nettrace files --- src/Ultra.Core/DiagnosticPortSession.cs | 19 ++- src/Ultra.Core/Ultra.Core.csproj | 4 +- src/Ultra.Core/UltraConverterToFirefox.cs | 105 +++++++++++++++ ...refox.cs => UltraConverterToFirefoxEtw.cs} | 124 +++++------------- src/Ultra.Core/UltraProfiler.cs | 29 ++-- src/Ultra.Core/UltraProfilerEtw.cs | 2 +- src/Ultra.Core/UltraProfilerEventPipe.cs | 25 ++-- src/Ultra.Core/UltraProfilerTraceFile.cs | 22 ++++ src/Ultra/Program.cs | 35 +++-- 9 files changed, 238 insertions(+), 127 deletions(-) create mode 100644 src/Ultra.Core/UltraConverterToFirefox.cs rename src/Ultra.Core/{ConverterToFirefox.cs => UltraConverterToFirefoxEtw.cs} (90%) create mode 100644 src/Ultra.Core/UltraProfilerTraceFile.cs diff --git a/src/Ultra.Core/DiagnosticPortSession.cs b/src/Ultra.Core/DiagnosticPortSession.cs index 864a204..5fe54e4 100644 --- a/src/Ultra.Core/DiagnosticPortSession.cs +++ b/src/Ultra.Core/DiagnosticPortSession.cs @@ -3,6 +3,7 @@ // See license.txt file in the project root for full license information. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; using System.Net.Sockets; using Microsoft.Diagnostics.NETCore.Client; @@ -29,7 +30,7 @@ internal class DiagnosticPortSession private FileStream? _nettraceFileStream; private Task? _eventStreamCopyTask; private bool _disposed; - + public DiagnosticPortSession(int pid, bool sampler, string baseName, CancellationToken token) { _pid = pid; @@ -40,6 +41,18 @@ public DiagnosticPortSession(int pid, bool sampler, string baseName, Cancellatio _connectTask = ConnectAndStartProfilingImpl(pid, sampler, baseName, token); } + public bool TryGetNettraceFilePathIfExists([NotNullWhen(true)] out string? nettraceFilePath) + { + if (_nettraceFilePath is null || !File.Exists(_nettraceFilePath)) + { + nettraceFilePath = null; + return false; + } + + nettraceFilePath = _nettraceFilePath; + return nettraceFilePath is not null; + } + private async Task ConnectAndStartProfilingImpl(int pid, bool sampler, string baseName, CancellationToken token) { CancellationTokenSource linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, _cancelConnectSource.Token); @@ -91,7 +104,7 @@ public async Task StartProfiling(CancellationToken token) { - _nettraceFilePath = Path.Combine(Environment.CurrentDirectory, $"{_baseName}_{(_sampler ? "sampler" : "main")}_{_pid}.nettrace"); + _nettraceFilePath = Path.Combine(Environment.CurrentDirectory, $"{_baseName}_{(_sampler ? "sampler" : "clr")}_{_pid}.nettrace"); _nettraceFileStream = new FileStream(_nettraceFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, 65536, FileOptions.Asynchronous); long keywords = -1; @@ -100,7 +113,7 @@ public async Task StartProfiling(CancellationToken token) if (!_sampler) { - providerName = "Microsoft-Windows-DotNETRuntime"; + providerName = ClrTraceEventParser.ProviderName; keywords = (long)( ClrTraceEventParser.Keywords.JITSymbols | ClrTraceEventParser.Keywords.Exception | diff --git a/src/Ultra.Core/Ultra.Core.csproj b/src/Ultra.Core/Ultra.Core.csproj index 217b010..54bae3a 100644 --- a/src/Ultra.Core/Ultra.Core.csproj +++ b/src/Ultra.Core/Ultra.Core.csproj @@ -44,7 +44,7 @@ - + diff --git a/src/Ultra.Core/UltraConverterToFirefox.cs b/src/Ultra.Core/UltraConverterToFirefox.cs new file mode 100644 index 0000000..4bdd0b7 --- /dev/null +++ b/src/Ultra.Core/UltraConverterToFirefox.cs @@ -0,0 +1,105 @@ +// 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; + +/// +/// Converts a list of trace files (one ETL file or a list of nettrace files) to a Firefox profile. +/// +public abstract class UltraConverterToFirefox : IDisposable +{ + private protected readonly List TraceFiles; + private protected readonly UltraProfilerOptions Options; + private protected FirefoxProfiler.Profile ProfilerResult; + + /// + /// A generic other category. + /// + public const int CategoryOther = 0; + + /// + /// The kernel category. + /// + public const int CategoryKernel = 1; + + /// + /// The native category. + /// + public const int CategoryNative = 2; + + /// + /// The managed category. + /// + public const int CategoryManaged = 3; + + /// + /// The GC category. + /// + public const int CategoryGc = 4; + + /// + /// The JIT category. + /// + public const int CategoryJit = 5; + + /// + /// The CLR category. + /// + public const int CategoryClr = 6; + + private protected UltraConverterToFirefox(List traceFiles, UltraProfilerOptions options) + { + TraceFiles = traceFiles; + this.Options = options; + ProfilerResult = new FirefoxProfiler.Profile(); // Create an empty profile (not used and override by the derived class) + } + + /// + public abstract void Dispose(); + + /// + /// Converts a list of trace files (one ETL file, or a list of nettrace files) to a Firefox profile. + /// + /// The list of trace files to convert. + /// The options used for converting. + /// The list of process ids to extract from the trace files. + /// The converted Firefox profile. + public static FirefoxProfiler.Profile Convert(List traceFiles, UltraProfilerOptions options, List processIds) + { + var extensions = traceFiles.Select(x => Path.GetExtension(x.FileName)).ToHashSet(StringComparer.OrdinalIgnoreCase); + + if (extensions.Count != 1) + { + throw new ArgumentException($"All trace files must have the same extension. Instead got [{string.Join(", ", extensions)}]"); + } + + var extension = extensions.First(); + if (extension == ".etl") + { + if (traceFiles.Count > 1) + { + throw new ArgumentException("Only one ETL file is supported"); + } + + using var converter = new UltraConverterToFirefoxEtw(traceFiles, options); + return converter.Convert(processIds); + } + else if (extension == ".nettrace") + { + throw new NotImplementedException(); + } + else + { + throw new ArgumentException($"Unsupported trace file extension [{extension}]"); + } + } + + private FirefoxProfiler.Profile Convert(List processIds) + { + ConvertImpl(processIds); + return ProfilerResult; + } + + private protected abstract void ConvertImpl(List processIds); +} diff --git a/src/Ultra.Core/ConverterToFirefox.cs b/src/Ultra.Core/UltraConverterToFirefoxEtw.cs similarity index 90% rename from src/Ultra.Core/ConverterToFirefox.cs rename to src/Ultra.Core/UltraConverterToFirefoxEtw.cs index c830e5e..00c7a1a 100644 --- a/src/Ultra.Core/ConverterToFirefox.cs +++ b/src/Ultra.Core/UltraConverterToFirefoxEtw.cs @@ -17,7 +17,7 @@ namespace Ultra.Core; /// /// Converts an ETW trace file to a Firefox profile. /// -public sealed class ConverterToFirefox : IDisposable +internal class UltraConverterToFirefoxEtw : UltraConverterToFirefox { private readonly Dictionary _mapModuleFileIndexToFirefox; private readonly HashSet _setManagedModules; @@ -32,48 +32,13 @@ public sealed class ConverterToFirefox : IDisposable private ModuleFileIndex _clrJitModuleIndex = ModuleFileIndex.Invalid; private ModuleFileIndex _coreClrModuleIndex = ModuleFileIndex.Invalid; private int _profileThreadIndex; - private readonly UltraProfilerOptions _options; - private readonly FirefoxProfiler.Profile _profile; - - /// - /// A generic other category. - /// - public const int CategoryOther = 0; - - /// - /// The kernel category. - /// - public const int CategoryKernel = 1; - - /// - /// The native category. - /// - public const int CategoryNative = 2; - - /// - /// The managed category. - /// - public const int CategoryManaged = 3; - - /// - /// The GC category. - /// - public const int CategoryGc = 4; - - /// - /// The JIT category. - /// - public const int CategoryJit = 5; - - /// - /// The CLR category. - /// - public const int CategoryClr = 6; - - private ConverterToFirefox(string traceFilePath, UltraProfilerOptions options) + + public UltraConverterToFirefoxEtw(List traceFiles, UltraProfilerOptions options) : base(traceFiles, options) { - _etl = new ETWTraceEventSource(traceFilePath); - _traceLog = TraceLog.OpenOrConvert(traceFilePath); + _etl = new ETWTraceEventSource(traceFiles[0].FileName); + _traceLog = TraceLog.OpenOrConvert(traceFiles[0].FileName); + + ProfilerResult = CreateProfile(); var symbolPath = options.GetCachedSymbolPath(); var symbolPathText = symbolPath.ToString(); @@ -82,10 +47,6 @@ private ConverterToFirefox(string traceFilePath, UltraProfilerOptions options) _symbolReader.Options = SymbolReaderOptions.None; _symbolReader.SecurityCheck = (pdbPath) => true; - this._profile = CreateProfile(); - - this._options = options; - _mapModuleFileIndexToFirefox = new(); _mapCallStackIndexToFirefox = new(); _mapCodeAddressIndexToFirefox = new(); @@ -96,34 +57,15 @@ private ConverterToFirefox(string traceFilePath, UltraProfilerOptions options) } /// - public void Dispose() + public override void Dispose() { _symbolReader.Dispose(); _traceLog.Dispose(); _etl.Dispose(); } - /// - /// Converts an ETW trace file to a Firefox profile. - /// - /// The ETW trace file to convert. - /// The options used for converting. - /// The list of process ids to extract from the ETL file. - /// The converted Firefox profile. - public static FirefoxProfiler.Profile Convert(string traceFilePath, UltraProfilerOptions options, List processIds) - { - using var converter = new ConverterToFirefox(traceFilePath, options); - return converter.Convert(processIds); - } - - private FirefoxProfiler.Profile Convert(List processIds) + private protected override void ConvertImpl(List processIds) { - // MSNT_SystemTrace/Image/KernelBase - ThreadID="-1" ProcessorNumber="9" ImageBase="0xfffff80074000000" - - // We don't have access to physical CPUs - //profile.Meta.PhysicalCPUs = Environment.ProcessorCount / 2; - //profile.Meta.CPUName = ""; // TBD - _profileThreadIndex = 0; foreach (var processId in processIds) @@ -132,8 +74,6 @@ private FirefoxProfiler.Profile Convert(List processIds) ConvertProcess(process); } - - return _profile; } /// @@ -142,31 +82,31 @@ private FirefoxProfiler.Profile Convert(List processIds) /// The process to convert. private void ConvertProcess(TraceProcess process) { - if (_profile.Meta.Product == string.Empty) + if (ProfilerResult.Meta.Product == string.Empty) { - _profile.Meta.Product = process.Name; + ProfilerResult.Meta.Product = process.Name; } var processStartTime = new DateTimeOffset(process.StartTime.ToUniversalTime()).ToUnixTimeMilliseconds(); var processEndTime = new DateTimeOffset(process.EndTime.ToUniversalTime()).ToUnixTimeMilliseconds(); - if (processStartTime < _profile.Meta.StartTime) + if (processStartTime < ProfilerResult.Meta.StartTime) { - _profile.Meta.StartTime = processStartTime; + ProfilerResult.Meta.StartTime = processStartTime; } - if (processEndTime > _profile.Meta.EndTime) + if (processEndTime > ProfilerResult.Meta.EndTime) { - _profile.Meta.EndTime = processEndTime; + ProfilerResult.Meta.EndTime = processEndTime; } var profilingStartTime = process.StartTimeRelativeMsec; - if (profilingStartTime < _profile.Meta.ProfilingStartTime) + if (profilingStartTime < ProfilerResult.Meta.ProfilingStartTime) { - _profile.Meta.ProfilingStartTime = profilingStartTime; + ProfilerResult.Meta.ProfilingStartTime = profilingStartTime; } var profilingEndTime = process.EndTimeRelativeMsec; - if (profilingEndTime > _profile.Meta.ProfilingEndTime) + if (profilingEndTime > ProfilerResult.Meta.ProfilingEndTime) { - _profile.Meta.ProfilingEndTime = profilingEndTime; + ProfilerResult.Meta.ProfilingEndTime = profilingEndTime; } LoadModules(process); @@ -225,7 +165,7 @@ private void ConvertProcess(TraceProcess process) ShowMarkersInTimeline = true }; - _options.LogProgress?.Invoke($"Converting Events for Thread: {profileThread.Name}"); + Options.LogProgress?.Invoke($"Converting Events for Thread: {profileThread.Name}"); var samples = profileThread.Samples; var markers = profileThread.Markers; @@ -475,12 +415,12 @@ private void ConvertProcess(TraceProcess process) startTime = evt.TimeStampRelativeMSec; } - _profile.Threads.Add(profileThread); + ProfilerResult.Threads.Add(profileThread); // Make visible threads in the UI that consume a minimum amount of CPU time - if (thread.CPUMSec > _options.MinimumCpuTimeBeforeThreadIsVisibleInMs) + if (thread.CPUMSec > Options.MinimumCpuTimeBeforeThreadIsVisibleInMs) { - _profile.Meta.InitialVisibleThreads!.Add(_profileThreadIndex); + ProfilerResult.Meta.InitialVisibleThreads!.Add(_profileThreadIndex); } // We will select by default the thread that has the maximum activity @@ -511,8 +451,8 @@ private void ConvertProcess(TraceProcess process) //gcHeapStatsCounter.Samples.Number = new(); gcHeapStatsCounter.Samples.Time = new(); - _profile.Counters ??= new(); - _profile.Counters.Add(gcHeapStatsCounter); + ProfilerResult.Counters ??= new(); + ProfilerResult.Counters.Add(gcHeapStatsCounter); long previousTotalHeapSize = 0; @@ -538,12 +478,12 @@ private void ConvertProcess(TraceProcess process) if (threads.Count > 0) { // Always make at least the first thread visible (that is taking most of the CPU time) - if (!_profile.Meta.InitialVisibleThreads!.Contains(threadIndexWithMaxCpuTime)) + if (!ProfilerResult.Meta.InitialVisibleThreads!.Contains(threadIndexWithMaxCpuTime)) { - _profile.Meta.InitialVisibleThreads.Add(threadIndexWithMaxCpuTime); + ProfilerResult.Meta.InitialVisibleThreads.Add(threadIndexWithMaxCpuTime); } - _profile.Meta.InitialSelectedThreads!.Add(threadIndexWithMaxCpuTime); + ProfilerResult.Meta.InitialSelectedThreads!.Add(threadIndexWithMaxCpuTime); } } @@ -553,7 +493,7 @@ private void ConvertProcess(TraceProcess process) /// The process to load the modules. private void LoadModules(TraceProcess process) { - _options.LogProgress?.Invoke($"Loading Modules for process {process.Name} ({process.ProcessID})"); + Options.LogProgress?.Invoke($"Loading Modules for process {process.Name} ({process.ProcessID})"); _setManagedModules.Clear(); _clrJitModuleIndex = ModuleFileIndex.Invalid; @@ -565,7 +505,7 @@ private void LoadModules(TraceProcess process) var module = allModules[i]; if (!_mapModuleFileIndexToFirefox.ContainsKey(module.ModuleFile.ModuleFileIndex)) { - _options.LogStepProgress?.Invoke($"Loading Symbols [{i}/{allModules.Count}] for Module `{module.Name}`, ImageSize: {ByteSize.FromBytes(module.ModuleFile.ImageSize)}"); + Options.LogStepProgress?.Invoke($"Loading Symbols [{i}/{allModules.Count}] for Module `{module.Name}`, ImageSize: {ByteSize.FromBytes(module.ModuleFile.ImageSize)}"); var lib = new FirefoxProfiler.Lib { @@ -581,8 +521,8 @@ private void LoadModules(TraceProcess process) _traceLog!.CodeAddresses.LookupSymbolsForModule(_symbolReader, module.ModuleFile); - _mapModuleFileIndexToFirefox.Add(module.ModuleFile.ModuleFileIndex, _profile.Libs.Count); - _profile.Libs.Add(lib); + _mapModuleFileIndexToFirefox.Add(module.ModuleFile.ModuleFileIndex, ProfilerResult.Libs.Count); + ProfilerResult.Libs.Add(lib); } var fileName = Path.GetFileName(module.FilePath); diff --git a/src/Ultra.Core/UltraProfiler.cs b/src/Ultra.Core/UltraProfiler.cs index bc2eb9a..61a61c5 100644 --- a/src/Ultra.Core/UltraProfiler.cs +++ b/src/Ultra.Core/UltraProfiler.cs @@ -24,6 +24,16 @@ public abstract class UltraProfiler : IDisposable private readonly CancellationTokenSource _cancellationTokenSource; private bool _disposed; + /// + /// The postfix name for the sampler events for a nettrace file. + /// + public const string NettracePostfixNameSampler = "_sampler"; + + /// + /// The postfix name for the CLR events for a nettrace file. + /// + public const string NettracePostfixNameClr = "_clr"; + /// /// Initializes a new instance of the class. /// @@ -306,12 +316,12 @@ public async Task Run(UltraProfilerOptions ultraProfilerOptions) throw new InvalidOperationException("CTRL+C requested"); } - var fileToConvert = await runner.FinishFileToConvert(); + var traceFiles = await runner.FinishFileToConvert(); string jsonFinalFile = string.Empty; - if (!string.IsNullOrEmpty(fileToConvert)) + if (traceFiles.Count > 9) { - jsonFinalFile = await Convert(fileToConvert, processList.Select(x => x.Id).ToList(), ultraProfilerOptions); + jsonFinalFile = await Convert(baseName, traceFiles, processList.Select(x => x.Id).ToList(), ultraProfilerOptions); } await runner.OnFinalCleanup(); @@ -325,23 +335,22 @@ public async Task Run(UltraProfilerOptions ultraProfilerOptions) /// /// Converts the ETL file to a compressed JSON file in the Firefox Profiler format. /// - /// The path to the ETL file. + /// The base name of the output file. + /// The list of trace files to include in the conversion. /// The list of process IDs to include in the conversion. /// The options for the profiler. /// A task that represents the asynchronous operation. The task result contains the path to the generated JSON file. /// Thrown when a stop request is received. - public async Task Convert(string etlFile, List pIds, UltraProfilerOptions ultraProfilerOptions) + public async Task Convert(string baseFileNameOutput, List traceFiles, List pIds, UltraProfilerOptions ultraProfilerOptions) { - var profile = ConverterToFirefox.Convert(etlFile, ultraProfilerOptions, pIds); + var profile = UltraConverterToFirefox.Convert(traceFiles, ultraProfilerOptions, pIds); if (StopRequested) { throw new InvalidOperationException("CTRL+C requested"); } - var directory = Path.GetDirectoryName(etlFile); - var etlFileNameWithoutExtension = Path.GetFileNameWithoutExtension(etlFile); - var jsonFinalFile = $"{ultraProfilerOptions.BaseOutputFileName ?? etlFileNameWithoutExtension}.json.gz"; + var jsonFinalFile = $"{ultraProfilerOptions.BaseOutputFileName ?? baseFileNameOutput}.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); @@ -549,7 +558,7 @@ private protected class ProfilerRunner(string baseFileName) public required Func OnFinally; - public required Func> FinishFileToConvert; + public required Func>> FinishFileToConvert; public required Func OnFinalCleanup; } diff --git a/src/Ultra.Core/UltraProfilerEtw.cs b/src/Ultra.Core/UltraProfilerEtw.cs index 5248053..09d3bae 100644 --- a/src/Ultra.Core/UltraProfilerEtw.cs +++ b/src/Ultra.Core/UltraProfilerEtw.cs @@ -159,7 +159,7 @@ private protected override ProfilerRunner CreateRunner(UltraProfilerOptions ultr File.Delete(rundownSession); } - return etlFinalFile; + return [new(etlFinalFile)]; }, OnFinalCleanup = () => diff --git a/src/Ultra.Core/UltraProfilerEventPipe.cs b/src/Ultra.Core/UltraProfilerEventPipe.cs index 3527d46..92f3a32 100644 --- a/src/Ultra.Core/UltraProfilerEventPipe.cs +++ b/src/Ultra.Core/UltraProfilerEventPipe.cs @@ -58,15 +58,9 @@ private protected override ProfilerRunner CreateRunner(UltraProfilerOptions ultr OnFinally = () => Task.CompletedTask, - FinishFileToConvert = () => - { - return Task.FromResult(string.Empty); - }, + FinishFileToConvert = () => Task.FromResult(profilerState?.GetGeneratedTraceFiles() ?? []), - OnFinalCleanup = () => - { - return Task.CompletedTask; - }, + OnFinalCleanup = () => Task.CompletedTask, OnEnablingProfiling = async () => { @@ -112,6 +106,21 @@ public UltraSamplerProfilerState(string baseName, int pid, CancellationToken tok _clrSession = new(pid, false, baseName, token); } + public List GetGeneratedTraceFiles() + { + var files = new List(); + if (_samplerSession.TryGetNettraceFilePathIfExists(out var nettraceFilePath)) + { + files.Add(new UltraProfilerTraceFile(nettraceFilePath)); + } + + if (_clrSession.TryGetNettraceFilePathIfExists(out nettraceFilePath)) + { + files.Add(new UltraProfilerTraceFile(nettraceFilePath)); + } + return files; + } + public long TotalFileLength() { return _samplerSession.GetNettraceFileLength() + _clrSession.GetNettraceFileLength(); diff --git a/src/Ultra.Core/UltraProfilerTraceFile.cs b/src/Ultra.Core/UltraProfilerTraceFile.cs new file mode 100644 index 0000000..6ec74a6 --- /dev/null +++ b/src/Ultra.Core/UltraProfilerTraceFile.cs @@ -0,0 +1,22 @@ +// 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; + +/// +/// Represents a trace file used by UltraProfiler. +/// +/// The name of the trace file. +public readonly record struct UltraProfilerTraceFile(string FileName) +{ + /// + /// Gets a value indicating whether the trace file is an ETL file. + /// + public bool IsEtl => FileName.EndsWith(".etl", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets a value indicating whether the trace file is a Nettrace file. + /// + public bool IsNettrace => FileName.EndsWith(".nettrace", StringComparison.OrdinalIgnoreCase); +} \ No newline at end of file diff --git a/src/Ultra/Program.cs b/src/Ultra/Program.cs index b779239..397d9dc 100644 --- a/src/Ultra/Program.cs +++ b/src/Ultra/Program.cs @@ -80,7 +80,7 @@ static async Task Main(string[] args) return 1; } - string? fileOutput = null; + string? jsonGzOutput = null; // Add the pid passed as options @@ -180,7 +180,7 @@ await AnsiConsole.Status() try { - fileOutput = await etwProfiler.Run(options); + jsonGzOutput = await etwProfiler.Run(options); } finally { @@ -205,7 +205,7 @@ await AnsiConsole.Status() try { - fileOutput = await etwProfiler.Run(options); + jsonGzOutput = await etwProfiler.Run(options); } finally { @@ -280,7 +280,7 @@ await AnsiConsole.Live(statusTable.Table) try { - fileOutput = await etwProfiler.Run(options); + jsonGzOutput = await etwProfiler.Run(options); } finally { @@ -290,18 +290,18 @@ await AnsiConsole.Live(statusTable.Table) ); } - if (fileOutput != null) + if (jsonGzOutput != null) { - AnsiConsole.MarkupLine($"Generated Firefox Profiler JSON file -> [green]{fileOutput}[/] - {ByteSize.FromBytes(new FileInfo(fileOutput).Length)}"); + AnsiConsole.MarkupLine($"Generated Firefox Profiler JSON file -> [green]{jsonGzOutput}[/] - {ByteSize.FromBytes(new FileInfo(jsonGzOutput).Length)}"); AnsiConsole.MarkupLine($"Go to [blue]https://profiler.firefox.com/ [/]"); } return 0; } }, - new Command("convert", "Convert an existing ETL file to a Firefox Profiler json file") + new Command("convert", "Convert an existing trace file (one ETL file or a list of nettrace files) to a Firefox Profiler json file") { - new CommandUsage("Usage: {NAME} --pid xxx "), + 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 }, @@ -313,7 +313,7 @@ await AnsiConsole.Live(statusTable.Table) if (arguments.Length == 0) { - AnsiConsole.MarkupLine("[red]Missing ETL file name[/]"); + AnsiConsole.MarkupLine("[red]Missing trace file name[/]"); return 1; } @@ -323,7 +323,7 @@ await AnsiConsole.Live(statusTable.Table) return 1; } - var etlFile = arguments[0]; + string? fileOutput = null; @@ -373,7 +373,20 @@ await AnsiConsole.Status() options.EnsureDirectoryForBaseOutputFileName(); - fileOutput = await etwProfiler.Convert(etlFile, pidList, options); + var traceFiles = arguments.Select(x => new UltraProfilerTraceFile(x)).ToList(); + + // Try to recover the base name from the first trace file + string baseName = Path.GetFileNameWithoutExtension(traceFiles[0].FileName); + if (baseName.EndsWith(UltraProfiler.NettracePostfixNameSampler)) // nettrace + { + baseName = baseName.Substring(0, baseName.Length - UltraProfiler.NettracePostfixNameSampler.Length); + } + else if (baseName.EndsWith(UltraProfiler.NettracePostfixNameClr)) // nettrace + { + baseName = baseName.Substring(0, baseName.Length - UltraProfiler.NettracePostfixNameClr.Length); + } + + fileOutput = await etwProfiler.Convert(baseName, traceFiles, pidList, options); } finally {