Skip to content

Commit

Permalink
Add Inspect functionality to analyze ETW trace files and return infor…
Browse files Browse the repository at this point in the history
…mation about processes
  • Loading branch information
En3Tho committed Feb 3, 2025
1 parent 439b76d commit 40d1aa2
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 23 deletions.
39 changes: 39 additions & 0 deletions src/Ultra.Core/CommandInputsOutputs.cs
Original file line number Diff line number Diff line change
@@ -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<InspectProcessInfo> 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;
}
}
}
65 changes: 54 additions & 11 deletions src/Ultra.Core/EtwConverterToFirefox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,7 +84,7 @@ private EtwConverterToFirefox(string traceFilePath, EtwUltraProfilerOptions opti
_symbolReader.SecurityCheck = (pdbPath) => true;

this._profile = CreateProfile();

this._options = options;

_mapModuleFileIndexToFirefox = new();
Expand All @@ -103,6 +104,50 @@ public void Dispose()
_etl.Dispose();
}

/// <summary>
/// Inspects the ETW trace file and returns a JSON string with the inspection results.
/// </summary>
/// <param name="traceFilePath">The path to the ETW trace file.</param>
/// <param name="filter">An optional filter to apply to the process names and command lines.</param>
/// <returns>A JSON string containing the inspection results.</returns>
public static string Inspect(string traceFilePath, string? filter)
{
var options = new EtwUltraProfilerOptions();
using var converter = new EtwConverterToFirefox(traceFilePath, options);
return converter.Inspect(filter);
}

/// <summary>
/// Inspects the ETW trace file and returns a JSON string with the inspection results.
/// </summary>
/// <param name="filter">An optional filter to apply to the process names and command lines.</param>
/// <returns>A JSON string containing the inspection results.</returns>
public string Inspect(string? filter)
{
var inspectProfile = new InspectOutput($"{_traceLog.OSName} {_traceLog.OSVersion} {_traceLog.OSBuild}", _traceLog.EventCount, _traceLog.SessionDuration);

Dictionary<int, int> 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);
}

/// <summary>
/// Converts an ETW trace file to a Firefox profile.
/// </summary>
Expand All @@ -118,8 +163,8 @@ public static FirefoxProfiler.Profile Convert(string traceFilePath, EtwUltraProf

private FirefoxProfiler.Profile Convert(List<int> 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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -797,7 +840,7 @@ private int ConvertMethod(CodeAddressIndex codeAddressIndex, MethodIndex methodI
firefoxMethodIndex = funcTable.Length;
_mapMethodIndexToFirefox.Add(methodIndex, firefoxMethodIndex);
}

//public List<int> Name { get; }
//public List<bool> IsJS { get; }
//public List<bool> RelevantForJS { get; }
Expand Down Expand Up @@ -848,7 +891,7 @@ private int ConvertMethod(CodeAddressIndex codeAddressIndex, MethodIndex methodI

return firefoxMethodIndex;
}

/// <summary>
/// Gets or creates a string for the specified Firefox profile thread.
/// </summary>
Expand Down
8 changes: 4 additions & 4 deletions src/Ultra.Core/FirefoxProfiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static partial class FirefoxProfiler
public partial class JsonProfilerContext : JsonSerializerContext
{
}

public class StackTable
{
public StackTable()
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -1032,7 +1032,7 @@ public override void Write(Utf8JsonWriter writer, MarkerFormatType value, JsonSe
}
}
}


public class MarkerTableFormatType : MarkerFormatType
{
Expand Down
40 changes: 32 additions & 8 deletions src/Ultra/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ static async Task<int> Main(string[] args)

bool verbose = false;
var options = new EtwUltraProfilerOptions();
string? inspectFilter = null;

const string _ = "";

Expand Down Expand Up @@ -82,7 +83,7 @@ static async Task<int> Main(string[] args)

string? fileOutput = null;


// Add the pid passed as options
options.ProcessIds.AddRange(pidList);

Expand Down Expand Up @@ -141,7 +142,7 @@ static async Task<int> Main(string[] args)
{
AnsiConsole.MarkupLine($"[darkorange]Key pressed {key.Modifiers} {key.Key}[/]");
}

return startProfiling;
};
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -277,7 +278,7 @@ await AnsiConsole.Live(statusTable.Table)
statusTable.UpdateTable();
liveCtx.Refresh();
};

try
{
fileOutput = await etwProfiler.Run(options);
Expand Down Expand Up @@ -396,12 +397,35 @@ await AnsiConsole.Status()

return 0;
}
},
new Command("inspect", "Inspects an existing ETL file")
{
new CommandUsage("Usage: {NAME} <etl_file_name.etl>"),
_,
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));
Expand All @@ -418,7 +442,7 @@ await AnsiConsole.Status()
return 1;
}
}

private class StatusTable
{
private string? _statusText;
Expand All @@ -435,7 +459,7 @@ public StatusTable()
}

public Table Table { get; }

public void LogText(string text)
{
if (_logLines.Count > MaxLogLines)
Expand All @@ -445,7 +469,7 @@ public void LogText(string text)

_logLines.Enqueue(text);
}

public void Status(string text)
{
_statusText = text;
Expand Down

0 comments on commit 40d1aa2

Please sign in to comment.