From 40d1aa2bdd5ad5f4c73988a9cbf9cbbd04bfa50d Mon Sep 17 00:00:00 2001 From: En3Tho- Date: Mon, 3 Feb 2025 13:57:13 +0300 Subject: [PATCH] Add Inspect functionality to analyze ETW trace files and return information about processes --- src/Ultra.Core/CommandInputsOutputs.cs | 39 +++++++++++++++ src/Ultra.Core/EtwConverterToFirefox.cs | 65 ++++++++++++++++++++----- src/Ultra.Core/FirefoxProfiler.cs | 8 +-- src/Ultra/Program.cs | 40 ++++++++++++--- 4 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 src/Ultra.Core/CommandInputsOutputs.cs diff --git a/src/Ultra.Core/CommandInputsOutputs.cs b/src/Ultra.Core/CommandInputsOutputs.cs new file mode 100644 index 0000000..9a0da57 --- /dev/null +++ b/src/Ultra.Core/CommandInputsOutputs.cs @@ -0,0 +1,39 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Ultra.Core +{ + internal readonly struct InspectProcessInfo(string name, int pid, string commandLine, int events) + { + public string Name => name; + public int Pid => pid; + public string CommandLine => commandLine; + public int Events => events; + } + + internal class InspectOutput(string operatingSystem, int totalEvents, TimeSpan duration) + { + public string OperatingSystem => operatingSystem; + public int TotalEvents => totalEvents; + public TimeSpan Duration => duration; + public List Processes { get; } = [ ]; + } + + [JsonSerializable(typeof(InspectOutput))] + internal partial class CommandInputsOutputsJsonSerializerContext : JsonSerializerContext + { + static CommandInputsOutputsJsonSerializerContext() + { + // Replace context with new options which are more human-friendly for text output + var options = new JsonSerializerOptions() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true + }; + + var context = new CommandInputsOutputsJsonSerializerContext(options); + Default = context; + } + } +} \ No newline at end of file diff --git a/src/Ultra.Core/EtwConverterToFirefox.cs b/src/Ultra.Core/EtwConverterToFirefox.cs index d9767e6..bf32c31 100644 --- a/src/Ultra.Core/EtwConverterToFirefox.cs +++ b/src/Ultra.Core/EtwConverterToFirefox.cs @@ -3,6 +3,7 @@ // See license.txt file in the project root for full license information. using System.Runtime.InteropServices; +using System.Text.Json; using ByteSizeLib; using Microsoft.Diagnostics.Symbols; using Microsoft.Diagnostics.Tracing; @@ -83,7 +84,7 @@ private EtwConverterToFirefox(string traceFilePath, EtwUltraProfilerOptions opti _symbolReader.SecurityCheck = (pdbPath) => true; this._profile = CreateProfile(); - + this._options = options; _mapModuleFileIndexToFirefox = new(); @@ -103,6 +104,50 @@ public void Dispose() _etl.Dispose(); } + /// + /// Inspects the ETW trace file and returns a JSON string with the inspection results. + /// + /// The path to the ETW trace file. + /// An optional filter to apply to the process names and command lines. + /// A JSON string containing the inspection results. + public static string Inspect(string traceFilePath, string? filter) + { + var options = new EtwUltraProfilerOptions(); + using var converter = new EtwConverterToFirefox(traceFilePath, options); + return converter.Inspect(filter); + } + + /// + /// Inspects the ETW trace file and returns a JSON string with the inspection results. + /// + /// An optional filter to apply to the process names and command lines. + /// A JSON string containing the inspection results. + public string Inspect(string? filter) + { + var inspectProfile = new InspectOutput($"{_traceLog.OSName} {_traceLog.OSVersion} {_traceLog.OSBuild}", _traceLog.EventCount, _traceLog.SessionDuration); + + Dictionary processEvents = new(); + + foreach (var evt in _traceLog.Events) + { + CollectionsMarshal.GetValueRefOrAddDefault(processEvents, evt.ProcessID, out _)++; + } + + foreach (var process in _traceLog.Processes) + { + if (filter != null && !(process.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) + || process.CommandLine.Contains(filter, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + var processInfo = new InspectProcessInfo(process.Name, process.ProcessID, process.CommandLine, processEvents[process.ProcessID]); + inspectProfile.Processes.Add(processInfo); + } + + inspectProfile.Processes.Sort((x, y) => y.Events - x.Events); + return JsonSerializer.Serialize(inspectProfile, CommandInputsOutputsJsonSerializerContext.Default.InspectOutput); + } + /// /// Converts an ETW trace file to a Firefox profile. /// @@ -118,8 +163,8 @@ public static FirefoxProfiler.Profile Convert(string traceFilePath, EtwUltraProf private FirefoxProfiler.Profile Convert(List processIds) { - // MSNT_SystemTrace/Image/KernelBase - ThreadID="-1" ProcessorNumber="9" ImageBase="0xfffff80074000000" - + // 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 @@ -177,7 +222,7 @@ private void ConvertProcess(TraceProcess process) // Sort threads by CPU time var threads = process.Threads.ToList(); threads.Sort((a, b) => b.CPUMSec.CompareTo(a.CPUMSec)); - + double maxCpuTime = threads.Count > 0 ? threads[0].CPUMSec : 0; int threadIndexWithMaxCpuTime = threads.Count > 0 ? _profileThreadIndex : -1; @@ -210,7 +255,7 @@ private void ConvertProcess(TraceProcess process) ? $"{thread.ThreadInfo} ({thread.ThreadID})" : $"Thread ({thread.ThreadID})"; var threadName = $"{threadIndex} - {threadBaseName}"; - + var profileThread = new FirefoxProfiler.Thread { Name = threadName, @@ -291,11 +336,9 @@ private void ConvertProcess(TraceProcess process) } else if (evt is MethodLoadUnloadTraceDataBase methodLoadUnloadVerbose) { - if (jitCompilePendingMethodId.TryGetValue(methodLoadUnloadVerbose.MethodID, + if (jitCompilePendingMethodId.Remove(methodLoadUnloadVerbose.MethodID, out var jitCompilePair)) { - jitCompilePendingMethodId.Remove(methodLoadUnloadVerbose.MethodID); - markers.StartTime.Add(jitCompilePair.Item2); markers.EndTime.Add(evt.TimeStampRelativeMSec); markers.Category.Add(CategoryJit); @@ -523,7 +566,7 @@ private void ConvertProcess(TraceProcess process) gcHeapStatsCounter.Samples.Time!.Add(0); gcHeapStatsCounter.Samples.Count.Add(0); gcHeapStatsCounter.Samples.Length++; - + foreach (var evt in gcHeapStatsEvents) { gcHeapStatsCounter.Samples.Time!.Add(evt.Item1); @@ -797,7 +840,7 @@ private int ConvertMethod(CodeAddressIndex codeAddressIndex, MethodIndex methodI firefoxMethodIndex = funcTable.Length; _mapMethodIndexToFirefox.Add(methodIndex, firefoxMethodIndex); } - + //public List Name { get; } //public List IsJS { get; } //public List RelevantForJS { get; } @@ -848,7 +891,7 @@ private int ConvertMethod(CodeAddressIndex codeAddressIndex, MethodIndex methodI return firefoxMethodIndex; } - + /// /// Gets or creates a string for the specified Firefox profile thread. /// diff --git a/src/Ultra.Core/FirefoxProfiler.cs b/src/Ultra.Core/FirefoxProfiler.cs index f16d2d1..740b6b5 100644 --- a/src/Ultra.Core/FirefoxProfiler.cs +++ b/src/Ultra.Core/FirefoxProfiler.cs @@ -40,7 +40,7 @@ public static partial class FirefoxProfiler public partial class JsonProfilerContext : JsonSerializerContext { } - + public class StackTable { public StackTable() @@ -276,13 +276,13 @@ public Lib() DebugPath = string.Empty; BreakpadId = string.Empty; } - + public ulong? AddressStart { get; set; } public ulong? AddressEnd { get; set; } public ulong? AddressOffset { get; set; } - + public string Arch { get; set; } public string Name { get; set; } @@ -1032,7 +1032,7 @@ public override void Write(Utf8JsonWriter writer, MarkerFormatType value, JsonSe } } } - + public class MarkerTableFormatType : MarkerFormatType { diff --git a/src/Ultra/Program.cs b/src/Ultra/Program.cs index f8d96a7..d466ac1 100644 --- a/src/Ultra/Program.cs +++ b/src/Ultra/Program.cs @@ -23,6 +23,7 @@ static async Task Main(string[] args) bool verbose = false; var options = new EtwUltraProfilerOptions(); + string? inspectFilter = null; const string _ = ""; @@ -82,7 +83,7 @@ static async Task Main(string[] args) string? fileOutput = null; - + // Add the pid passed as options options.ProcessIds.AddRange(pidList); @@ -141,7 +142,7 @@ static async Task Main(string[] args) { AnsiConsole.MarkupLine($"[darkorange]Key pressed {key.Modifiers} {key.Key}[/]"); } - + return startProfiling; }; } @@ -202,7 +203,7 @@ await AnsiConsole.Status() 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); @@ -277,7 +278,7 @@ await AnsiConsole.Live(statusTable.Table) statusTable.UpdateTable(); liveCtx.Refresh(); }; - + try { fileOutput = await etwProfiler.Run(options); @@ -396,12 +397,35 @@ await AnsiConsole.Status() return 0; } + }, + new Command("inspect", "Inspects an existing ETL file") + { + new CommandUsage("Usage: {NAME} "), + _, + new HelpOption(), + { "filter=", "Filter for the process name or command line", (string? filter) => { inspectFilter = filter; } }, + (_, arguments) => + { + if (arguments.Length == 0) + { + AnsiConsole.MarkupLine("[red]Missing ETL file name[/]"); + return ValueTask.FromResult(1); + } + + var etlFile = arguments[0]; + + var result = EtwConverterToFirefox.Inspect(etlFile, inspectFilter); + var report = new Text(result, Style.Plain); + AnsiConsole.Write(report); + + return ValueTask.FromResult(0); + } } }; var width = Console.IsOutputRedirected ? 80 : Math.Max(80, Console.WindowWidth); var optionWidth = Console.IsOutputRedirected || width == 80 ? 29 : 36; - + try { return await commandApp.RunAsync(args, new CommandRunConfig(width, optionWidth)); @@ -418,7 +442,7 @@ await AnsiConsole.Status() return 1; } } - + private class StatusTable { private string? _statusText; @@ -435,7 +459,7 @@ public StatusTable() } public Table Table { get; } - + public void LogText(string text) { if (_logLines.Count > MaxLogLines) @@ -445,7 +469,7 @@ public void LogText(string text) _logLines.Enqueue(text); } - + public void Status(string text) { _statusText = text;