diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 2384380..78d4285 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -8,9 +8,11 @@
-
+
+
+
\ No newline at end of file
diff --git a/src/Ultra.Core/EtwConverterToFirefox.cs b/src/Ultra.Core/EtwConverterToFirefox.cs
index e5af34b..92d87b6 100644
--- a/src/Ultra.Core/EtwConverterToFirefox.cs
+++ b/src/Ultra.Core/EtwConverterToFirefox.cs
@@ -14,7 +14,10 @@
namespace Ultra.Core;
-public class EtwConverterToFirefox : IDisposable
+///
+/// Converts an ETW trace file to a Firefox profile.
+///
+public sealed class EtwConverterToFirefox : IDisposable
{
private readonly Dictionary _mapModuleFileIndexToFirefox;
private readonly HashSet _setManagedModules;
@@ -23,39 +26,55 @@ public class EtwConverterToFirefox : IDisposable
private readonly Dictionary _mapCodeAddressIndexToMethodIndexFirefox;
private readonly Dictionary _mapMethodIndexToFirefox;
private readonly Dictionary _mapStringToFirefox;
- private SymbolReader? _symbolReader;
- private ETWTraceEventSource _etl;
+ private readonly SymbolReader _symbolReader;
+ private readonly ETWTraceEventSource _etl;
+ private readonly TraceLog _traceLog;
private ModuleFileIndex _clrJitModuleIndex = ModuleFileIndex.Invalid;
private ModuleFileIndex _coreClrModuleIndex = ModuleFileIndex.Invalid;
+ private int _profileThreadIndex;
+ private readonly EtwUltraProfilerOptions _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;
- public const int CategoryGC = 4;
+
+ ///
+ /// The GC category.
+ ///
+ public const int CategoryGc = 4;
+
+ ///
+ /// The JIT category.
+ ///
public const int CategoryJit = 5;
+
+ ///
+ /// The CLR category.
+ ///
public const int CategoryClr = 6;
- public EtwConverterToFirefox()
- {
- _mapModuleFileIndexToFirefox = new();
- _mapCallStackIndexToFirefox = new();
- _mapCodeAddressIndexToFirefox = new();
- _mapCodeAddressIndexToMethodIndexFirefox = new Dictionary();
- _mapMethodIndexToFirefox = new();
- _mapStringToFirefox = new Dictionary(StringComparer.Ordinal);
- _setManagedModules = new HashSet();
- }
-
- public FirefoxProfiler.Profile Convert(string traceFilePath, List processIds, EtwUltraProfilerOptions options)
+ private EtwConverterToFirefox(string traceFilePath, EtwUltraProfilerOptions options)
{
- const double MinimumCpuTimeBeforeThreadIsVisible = 10.0;
-
_etl = new ETWTraceEventSource(traceFilePath);
-
- using var log = TraceLog.OpenOrConvert(traceFilePath);
+ _traceLog = TraceLog.OpenOrConvert(traceFilePath);
- // Console.Out
var symbolPath = options.GetCachedSymbolPath();
var symbolPathText = symbolPath.ToString();
@@ -63,116 +82,49 @@ public FirefoxProfiler.Profile Convert(string traceFilePath, List processId
_symbolReader.Options = SymbolReaderOptions.None;
_symbolReader.SecurityCheck = (pdbPath) => true;
- var profile = new FirefoxProfiler.Profile
- {
- Meta =
- {
- StartTime = double.MaxValue,
- EndTime = 0.0f,
- ProfilingStartTime = double.MaxValue,
- ProfilingEndTime = 0.0f,
- Version = 29,
- PreprocessedProfileVersion = 51,
- Product = string.Empty,
- InitialSelectedThreads = [],
- Platform = $"{log.OSName} {log.OSVersion} {log.OSBuild}",
- Oscpu = $"{log.OSName} {log.OSVersion} {log.OSBuild}",
- LogicalCPUs = log.NumberOfProcessors,
- DoesNotUseFrameImplementation = true,
- Symbolicated = true,
- SampleUnits = new FirefoxProfiler.SampleUnits
- {
- Time = "ms",
- EventDelay = "ms",
- ThreadCPUDelta = "ns"
- },
- InitialVisibleThreads = [],
- Stackwalk = 1,
- Interval = log.SampleProfileInterval.TotalMilliseconds,
- Categories =
- [
- new FirefoxProfiler.Category()
- {
- Name = "Other",
- Color = FirefoxProfiler.ProfileColor.Grey,
- Subcategories =
- {
- "Other",
- }
- },
- new FirefoxProfiler.Category()
- {
- Name = "Kernel",
- Color = FirefoxProfiler.ProfileColor.Orange,
- Subcategories =
- {
- "Other",
- }
- },
- new FirefoxProfiler.Category()
- {
- Name = "Native",
- Color = FirefoxProfiler.ProfileColor.Blue,
- Subcategories =
- {
- "Other",
- }
- },
- new FirefoxProfiler.Category()
- {
- Name = ".NET",
- Color = FirefoxProfiler.ProfileColor.Green,
- Subcategories =
- {
- "Other",
- }
- },
- new FirefoxProfiler.Category()
- {
- Name = ".NET GC",
- Color = FirefoxProfiler.ProfileColor.Yellow,
- Subcategories =
- {
- "Other",
- }
- },
- new FirefoxProfiler.Category()
- {
- Name = ".NET JIT",
- Color = FirefoxProfiler.ProfileColor.Purple,
- Subcategories =
- {
- "Other",
- }
- },
- new FirefoxProfiler.Category()
- {
- Name = ".NET CLR",
- Color = FirefoxProfiler.ProfileColor.Grey,
- Subcategories =
- {
- "Other",
- }
- },
- ]
- }
- };
+ this._profile = CreateProfile();
+
+ this._options = options;
- profile.Meta.Abi = RuntimeInformation.RuntimeIdentifier;
- profile.Meta.MarkerSchema.Add(JitCompileEvent.Schema());
- profile.Meta.MarkerSchema.Add(GCEvent.Schema());
- profile.Meta.MarkerSchema.Add(GCHeapStatsEvent.Schema());
- profile.Meta.MarkerSchema.Add(GCAllocationTickEvent.Schema());
- profile.Meta.MarkerSchema.Add(GCSuspendExecutionEngineEvent.Schema());
- profile.Meta.MarkerSchema.Add(GCRestartExecutionEngineEvent.Schema());
+ _mapModuleFileIndexToFirefox = new();
+ _mapCallStackIndexToFirefox = new();
+ _mapCodeAddressIndexToFirefox = new();
+ _mapCodeAddressIndexToMethodIndexFirefox = new ();
+ _mapMethodIndexToFirefox = new();
+ _mapStringToFirefox = new(StringComparer.Ordinal);
+ _setManagedModules = new();
+ }
+ ///
+ public 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, EtwUltraProfilerOptions options, List processIds)
+ {
+ using var converter = new EtwConverterToFirefox(traceFilePath, options);
+ return converter.Convert(processIds);
+ }
+
+ private FirefoxProfiler.Profile Convert(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
- int profileThreadIndex = 0;
+ _profileThreadIndex = 0;
foreach (var processId in processIds)
{
@@ -182,569 +134,509 @@ public FirefoxProfiler.Profile Convert(string traceFilePath, List processId
_clrJitModuleIndex = ModuleFileIndex.Invalid;
_coreClrModuleIndex = ModuleFileIndex.Invalid;
- var process = log.Processes.LastProcessWithID(processId);
-
- if (profile.Meta.Product == string.Empty)
- {
- profile.Meta.Product = process.Name;
- }
+ var process = _traceLog.Processes.LastProcessWithID(processId);
- var processStartTime = new DateTimeOffset(process.StartTime.ToUniversalTime()).ToUnixTimeMilliseconds();
- var processEndTime = new DateTimeOffset(process.EndTime.ToUniversalTime()).ToUnixTimeMilliseconds();
- if (processStartTime < profile.Meta.StartTime)
- {
- profile.Meta.StartTime = processStartTime;
- }
- if (processEndTime > profile.Meta.EndTime)
- {
- profile.Meta.EndTime = processEndTime;
- }
+ ConvertProcess(process);
+ }
- var profilingStartTime = process.StartTimeRelativeMsec;
- if (profilingStartTime < profile.Meta.ProfilingStartTime)
- {
- profile.Meta.ProfilingStartTime = profilingStartTime;
- }
- var profilingEndTime = process.EndTimeRelativeMsec;
- if (profilingEndTime > profile.Meta.ProfilingEndTime)
- {
- profile.Meta.ProfilingEndTime = profilingEndTime;
- }
+ return _profile;
+ }
- options.LogProgress?.Invoke($"Loading Modules for process {process.Name}");
+ ///
+ /// Converts an ETW trace process to a Firefox profile.
+ ///
+ /// The process to convert.
+ private void ConvertProcess(TraceProcess process)
+ {
+ if (_profile.Meta.Product == string.Empty)
+ {
+ _profile.Meta.Product = process.Name;
+ }
- var allModules = process.LoadedModules.ToList();
- for (var i = 0; i < allModules.Count; i++)
- {
- var module = allModules[i];
- if (_mapModuleFileIndexToFirefox.ContainsKey(module.ModuleFile.ModuleFileIndex))
- {
- continue; // Skip in case
- }
+ var processStartTime = new DateTimeOffset(process.StartTime.ToUniversalTime()).ToUnixTimeMilliseconds();
+ var processEndTime = new DateTimeOffset(process.EndTime.ToUniversalTime()).ToUnixTimeMilliseconds();
+ if (processStartTime < _profile.Meta.StartTime)
+ {
+ _profile.Meta.StartTime = processStartTime;
+ }
+ if (processEndTime > _profile.Meta.EndTime)
+ {
+ _profile.Meta.EndTime = processEndTime;
+ }
- options.LogStepProgress?.Invoke($"Loading Symbols [{i}/{allModules.Count}] for Module `{module.Name}`, ImageSize: {ByteSize.FromBytes(module.ModuleFile.ImageSize)}");
+ var profilingStartTime = process.StartTimeRelativeMsec;
+ if (profilingStartTime < _profile.Meta.ProfilingStartTime)
+ {
+ _profile.Meta.ProfilingStartTime = profilingStartTime;
+ }
+ var profilingEndTime = process.EndTimeRelativeMsec;
+ if (profilingEndTime > _profile.Meta.ProfilingEndTime)
+ {
+ _profile.Meta.ProfilingEndTime = profilingEndTime;
+ }
- var lib = new FirefoxProfiler.Lib
- {
- Name = module.Name,
- AddressStart = module.ImageBase,
- AddressEnd = module.ModuleFile.ImageEnd,
- Path = module.ModuleFile.FilePath,
- DebugPath = module.ModuleFile.PdbName,
- DebugName = module.ModuleFile.PdbName,
- BreakpadId = $"0x{module.ModuleID:X16}",
- Arch = "x64" // TODO
- };
-
- log.CodeAddresses.LookupSymbolsForModule(_symbolReader, module.ModuleFile);
- _mapModuleFileIndexToFirefox.Add(module.ModuleFile.ModuleFileIndex, profile.Libs.Count);
- profile.Libs.Add(lib);
-
- var fileName = Path.GetFileName(module.FilePath);
- if (fileName.Equals("clrjit.dll", StringComparison.OrdinalIgnoreCase))
- {
- _clrJitModuleIndex = module.ModuleFile.ModuleFileIndex;
- }
- else if (fileName.Equals("coreclr.dll", StringComparison.OrdinalIgnoreCase))
- {
- _coreClrModuleIndex = module.ModuleFile.ModuleFileIndex;
- }
+ LoadModules(process);
- if (module is TraceManagedModule managedModule)
- {
- _setManagedModules.Add(managedModule.ModuleFile.ModuleFileIndex);
+ List<(double, GCHeapStatsEvent)> gcHeapStatsEvents = new();
+ Dictionary jitCompilePendingMethodId = new();
- foreach (var otherModule in allModules.Where(x => x is not TraceManagedModule))
- {
- if (string.Equals(managedModule.FilePath, otherModule.FilePath, StringComparison.OrdinalIgnoreCase))
- {
- _setManagedModules.Add(otherModule.ModuleFile.ModuleFileIndex);
- }
- }
- }
- }
+ // 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;
- Dictionary jitCompilePendingMethodId = new();
+ var threadVisited = new HashSet();
+ var processName = $"{process.Name} ({process.ProcessID})";
- List<(double, GCHeapStatsEvent)> gcHeapStatsEvents = new();
+ // Add threads
+ for (var threadIndex = 0; threadIndex < threads.Count; threadIndex++)
+ {
+ var thread = threads[threadIndex];
+ // Skip threads that have already been visited
+ // TODO: for some reasons we have some threads that are duplicated?
+ if (!threadVisited.Add(thread.ThreadID))
+ {
+ continue;
+ }
- // Sort threads by CPU time
- var threads = process.Threads.ToList();
- threads.Sort((a, b) => b.CPUMSec.CompareTo(a.CPUMSec));
+ _mapCallStackIndexToFirefox.Clear();
+ _mapCodeAddressIndexToFirefox.Clear();
+ _mapMethodIndexToFirefox.Clear();
+ _mapStringToFirefox.Clear();
+ _mapCodeAddressIndexToMethodIndexFirefox.Clear();
Stack<(double, GCSuspendExecutionEngineEvent)> gcSuspendEEEvents = new();
Stack gcRestartEEEvents = new();
Stack<(double, GCEvent)> gcStartStopEvents = new();
- double maxCpuTime = threads.Count > 0 ? threads[0].CPUMSec : 0;
- int threadIndexWithMaxCpuTime = threads.Count > 0 ? profileThreadIndex : -1;
-
- var threadVisited = new HashSet();
-
- var processName = $"{process.Name} ({process.ProcessID})";
-
- // Add threads
- for (var threadIndex = 0; threadIndex < threads.Count; threadIndex++)
+ var threadBaseName = thread.ThreadInfo is not null
+ ? $"{thread.ThreadInfo} ({thread.ThreadID})"
+ : $"Thread ({thread.ThreadID})";
+ var threadName = $"{threadIndex} - {threadBaseName}";
+
+ var profileThread = new FirefoxProfiler.Thread
+ {
+ Name = threadName,
+ ProcessName = processName,
+ ProcessStartupTime = thread.StartTimeRelativeMSec,
+ RegisterTime = thread.StartTimeRelativeMSec,
+ ProcessShutdownTime = thread.EndTimeRelativeMSec,
+ UnregisterTime = thread.EndTimeRelativeMSec,
+ ProcessType = "default",
+ Pid = $"{process.ProcessID}",
+ Tid = $"{thread.ThreadID}",
+ ShowMarkersInTimeline = true
+ };
+
+ _options.LogProgress?.Invoke($"Converting Events for Thread: {profileThread.Name}");
+
+ var samples = profileThread.Samples;
+ var markers = profileThread.Markers;
+
+ samples.ThreadCPUDelta = new List();
+ samples.TimeDeltas = new List();
+ samples.WeightType = "samples";
+
+ //const TraceEventID GCStartEventID = (TraceEventID) 1;
+ //const TraceEventID GCStopEventID = (TraceEventID) 2;
+ const TraceEventID GCRestartEEStopEventID = (TraceEventID) 3;
+ //const TraceEventID GCHeapStatsEventID = (TraceEventID) 4;
+ //const TraceEventID GCCreateSegmentEventID = (TraceEventID) 5;
+ //const TraceEventID GCFreeSegmentEventID = (TraceEventID) 6;
+ const TraceEventID GCRestartEEStartEventID = (TraceEventID) 7;
+ const TraceEventID GCSuspendEEStopEventID = (TraceEventID) 8;
+ //const TraceEventID GCSuspendEEStartEventID = (TraceEventID) 9;
+ //const TraceEventID GCAllocationTickEventID = (TraceEventID) 10;
+
+ double startTime = 0;
+ double switchTimeInMsec = 0.0;
+ //double switchTimeOutMsec = 0.0;
+ foreach (var evt in thread.EventsInThread)
{
- var thread = threads[threadIndex];
- // Skip threads that have already been visited
- // TODO: for some reasons we have some threads that are duplicated?
- if (!threadVisited.Add(thread.ThreadID))
+ if (evt.Opcode != (TraceEventOpcode) 46)
{
- continue;
- }
-
- _mapCallStackIndexToFirefox.Clear();
- _mapCodeAddressIndexToFirefox.Clear();
- _mapMethodIndexToFirefox.Clear();
- _mapStringToFirefox.Clear();
- _mapCodeAddressIndexToMethodIndexFirefox.Clear();
-
- gcSuspendEEEvents.Clear();
- gcRestartEEEvents.Clear();
+ if (evt.Opcode == (TraceEventOpcode) 0x24 && evt is CSwitchTraceData switchTraceData)
+ {
+ if (evt.ThreadID == thread.ThreadID && switchTraceData.OldThreadID != thread.ThreadID)
+ {
+ // Old Thread -> This Thread
+ // Switch-in
+ switchTimeInMsec = evt.TimeStampRelativeMSec;
+ }
+ //else if (evt.ThreadID != thread.ThreadID && switchTraceData.OldThreadID == thread.ThreadID)
+ //{
+ // // This Thread -> Other Thread
+ // // Switch-out
+ // switchTimeOutMsec = evt.TimeStampRelativeMSec;
+ //}
+ }
- var threadBaseName = thread.ThreadInfo is not null
- ? $"{thread.ThreadInfo} ({thread.ThreadID})"
- : $"Thread ({thread.ThreadID})";
- var threadName = $"{threadIndex} - {threadBaseName}";
-
- var profileThread = new FirefoxProfiler.Thread
- {
- Name = threadName,
- ProcessName = processName,
- ProcessStartupTime = thread.StartTimeRelativeMSec,
- RegisterTime = thread.StartTimeRelativeMSec,
- ProcessShutdownTime = thread.EndTimeRelativeMSec,
- UnregisterTime = thread.EndTimeRelativeMSec,
- ProcessType = "default",
- Pid = $"{process.ProcessID}",
- Tid = $"{thread.ThreadID}",
- ShowMarkersInTimeline = true
- };
-
- options.LogProgress?.Invoke($"Converting Events for Thread: {profileThread.Name}");
-
- var samples = profileThread.Samples;
- var markers = profileThread.Markers;
-
- samples.ThreadCPUDelta = new List();
- samples.TimeDeltas = new List();
- samples.WeightType = "samples";
-
- const TraceEventID GCStartEventID = (TraceEventID) 1;
- const TraceEventID GCStopEventID = (TraceEventID) 2;
- const TraceEventID GCRestartEEStopEventID = (TraceEventID) 3;
- const TraceEventID GCHeapStatsEventID = (TraceEventID) 4;
- const TraceEventID GCCreateSegmentEventID = (TraceEventID) 5;
- const TraceEventID GCFreeSegmentEventID = (TraceEventID) 6;
- const TraceEventID GCRestartEEStartEventID = (TraceEventID) 7;
- const TraceEventID GCSuspendEEStopEventID = (TraceEventID) 8;
- const TraceEventID GCSuspendEEStartEventID = (TraceEventID) 9;
- const TraceEventID GCAllocationTickEventID = (TraceEventID) 10;
-
- double startTime = 0;
- int currentThread = -1;
- double switchTimeInMsec = 0.0;
- //double switchTimeOutMsec = 0.0;
- foreach (var evt in thread.EventsInThread)
- {
- if (evt.Opcode != (TraceEventOpcode) 46)
+ if (evt.ThreadID == thread.ThreadID)
{
- if (evt.Opcode == (TraceEventOpcode) 0x24 && evt is CSwitchTraceData switchTraceData)
+ if (evt is MethodJittingStartedTraceData methodJittingStarted)
{
- if (evt.ThreadID == thread.ThreadID && switchTraceData.OldThreadID != thread.ThreadID)
+ var signature = methodJittingStarted.MethodSignature;
+ var indexOfParent = signature.IndexOf('(');
+ if (indexOfParent >= 0)
{
- // Old Thread -> This Thread
- // Switch-in
- switchTimeInMsec = evt.TimeStampRelativeMSec;
+ signature = signature.Substring(indexOfParent);
}
- //else if (evt.ThreadID != thread.ThreadID && switchTraceData.OldThreadID == thread.ThreadID)
- //{
- // // This Thread -> Other Thread
- // // Switch-out
- // switchTimeOutMsec = evt.TimeStampRelativeMSec;
- //}
+
+ var jitCompile = new JitCompileEvent
+ {
+ FullName =
+ $"{methodJittingStarted.MethodNamespace}.{methodJittingStarted.MethodName}{signature}",
+ MethodILSize = methodJittingStarted.MethodILSize
+ };
+
+ jitCompilePendingMethodId[methodJittingStarted.MethodID] =
+ (jitCompile, evt.TimeStampRelativeMSec);
}
+ else if (evt is MethodLoadUnloadTraceDataBase methodLoadUnloadVerbose)
+ {
+ if (jitCompilePendingMethodId.TryGetValue(methodLoadUnloadVerbose.MethodID,
+ out var jitCompilePair))
+ {
+ jitCompilePendingMethodId.Remove(methodLoadUnloadVerbose.MethodID);
- if (evt.ThreadID == thread.ThreadID)
+ markers.StartTime.Add(jitCompilePair.Item2);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryJit);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
+ markers.ThreadId.Add(_profileThreadIndex);
+ markers.Name.Add(GetOrCreateString("JitCompile", profileThread));
+ markers.Data.Add(jitCompilePair.Item1);
+ markers.Length++;
+ }
+ }
+ else if (evt is GCHeapStatsTraceData gcHeapStats)
+ {
+ markers.StartTime.Add(evt.TimeStampRelativeMSec);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGc);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Instance);
+ markers.ThreadId.Add(_profileThreadIndex);
+ markers.Name.Add(GetOrCreateString($"GCHeapStats", profileThread));
+
+ var heapStatEvent = new GCHeapStatsEvent
+ {
+ TotalHeapSize = gcHeapStats.TotalHeapSize,
+ TotalPromoted = gcHeapStats.TotalPromoted,
+ GenerationSize0 = gcHeapStats.GenerationSize0,
+ TotalPromotedSize0 = gcHeapStats.TotalPromotedSize0,
+ GenerationSize1 = gcHeapStats.GenerationSize1,
+ TotalPromotedSize1 = gcHeapStats.TotalPromotedSize1,
+ GenerationSize2 = gcHeapStats.GenerationSize2,
+ TotalPromotedSize2 = gcHeapStats.TotalPromotedSize2,
+ GenerationSize3 = gcHeapStats.GenerationSize3,
+ TotalPromotedSize3 = gcHeapStats.TotalPromotedSize3,
+ GenerationSize4 = gcHeapStats.GenerationSize4,
+ TotalPromotedSize4 = gcHeapStats.TotalPromotedSize4,
+ FinalizationPromotedSize = gcHeapStats.FinalizationPromotedSize,
+ FinalizationPromotedCount = gcHeapStats.FinalizationPromotedCount,
+ PinnedObjectCount = gcHeapStats.PinnedObjectCount,
+ SinkBlockCount = gcHeapStats.SinkBlockCount,
+ GCHandleCount = gcHeapStats.GCHandleCount
+ };
+
+ gcHeapStatsEvents.Add((evt.TimeStampRelativeMSec, heapStatEvent));
+
+ markers.Data.Add(heapStatEvent);
+ markers.Length++;
+ }
+ else if (evt is GCAllocationTickTraceData allocationTick)
{
- if (evt is MethodJittingStartedTraceData methodJittingStarted)
+ markers.StartTime.Add(evt.TimeStampRelativeMSec);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGc);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Instance);
+ markers.ThreadId.Add(_profileThreadIndex);
+ markers.Name.Add(GetOrCreateString($"{threadIndex} - GC Alloc ({thread.ThreadID})", profileThread));
+
+ var allocationTickEvent = new GCAllocationTickEvent
{
- var signature = methodJittingStarted.MethodSignature;
- var indexOfParent = signature.IndexOf('(');
- if (indexOfParent >= 0)
+ AllocationAmount = allocationTick.AllocationAmount,
+ AllocationKind = allocationTick.AllocationKind switch
{
- signature = signature.Substring(indexOfParent);
- }
-
- var jitCompile = new JitCompileEvent
+ GCAllocationKind.Small => "Small",
+ GCAllocationKind.Large => "Large",
+ GCAllocationKind.Pinned => "Pinned",
+ _ => "Unknown"
+ },
+ TypeName = allocationTick.TypeName,
+ HeapIndex = allocationTick.HeapIndex
+ };
+ markers.Data.Add(allocationTickEvent);
+ markers.Length++;
+ }
+ else if (evt.ProviderGuid == ClrTraceEventParser.ProviderGuid)
+ {
+ if (evt is GCStartTraceData gcStart)
+ {
+ var gcEvent = new GCEvent
{
- FullName =
- $"{methodJittingStarted.MethodNamespace}.{methodJittingStarted.MethodName}{signature}",
- MethodILSize = methodJittingStarted.MethodILSize
+ Reason = gcStart.Reason.ToString(),
+ Count = gcStart.Count,
+ Depth = gcStart.Depth,
+ GCType = gcStart.Type.ToString()
};
- jitCompilePendingMethodId[methodJittingStarted.MethodID] =
- (jitCompile, evt.TimeStampRelativeMSec);
+ gcStartStopEvents.Push((evt.TimeStampRelativeMSec, gcEvent));
}
- else if (evt is MethodLoadUnloadTraceDataBase methodLoadUnloadVerbose)
+ else if (evt is GCEndTraceData gcEnd && gcStartStopEvents.Count > 0)
{
- if (jitCompilePendingMethodId.TryGetValue(methodLoadUnloadVerbose.MethodID,
- out var jitCompilePair))
- {
- jitCompilePendingMethodId.Remove(methodLoadUnloadVerbose.MethodID);
-
- markers.StartTime.Add(jitCompilePair.Item2);
- markers.EndTime.Add(evt.TimeStampRelativeMSec);
- markers.Category.Add(CategoryJit);
- markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
- markers.ThreadId.Add(profileThreadIndex);
- markers.Name.Add(GetFirefoxString("JitCompile", profileThread));
- markers.Data.Add(jitCompilePair.Item1);
- markers.Length++;
- }
+ var (gcEventStartTime, gcEvent) = gcStartStopEvents.Pop();
+
+ markers.StartTime.Add(gcEventStartTime);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGc);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
+ markers.ThreadId.Add(_profileThreadIndex);
+ markers.Name.Add(GetOrCreateString($"GC Event", profileThread));
+ markers.Data.Add(gcEvent);
+ markers.Length++;
}
- else if (evt is GCHeapStatsTraceData gcHeapStats)
+ else if (evt is GCSuspendEETraceData gcSuspendEE)
{
- markers.StartTime.Add(evt.TimeStampRelativeMSec);
- markers.EndTime.Add(evt.TimeStampRelativeMSec);
- markers.Category.Add(CategoryGC);
- markers.Phase.Add(FirefoxProfiler.MarkerPhase.Instance);
- markers.ThreadId.Add(profileThreadIndex);
- markers.Name.Add(GetFirefoxString($"GCHeapStats", profileThread));
-
- var heapStatEvent = new GCHeapStatsEvent
+ var gcSuspendEEEvent = new GCSuspendExecutionEngineEvent
{
- TotalHeapSize = gcHeapStats.TotalHeapSize,
- TotalPromoted = gcHeapStats.TotalPromoted,
- GenerationSize0 = gcHeapStats.GenerationSize0,
- TotalPromotedSize0 = gcHeapStats.TotalPromotedSize0,
- GenerationSize1 = gcHeapStats.GenerationSize1,
- TotalPromotedSize1 = gcHeapStats.TotalPromotedSize1,
- GenerationSize2 = gcHeapStats.GenerationSize2,
- TotalPromotedSize2 = gcHeapStats.TotalPromotedSize2,
- GenerationSize3 = gcHeapStats.GenerationSize3,
- TotalPromotedSize3 = gcHeapStats.TotalPromotedSize3,
- GenerationSize4 = gcHeapStats.GenerationSize4,
- TotalPromotedSize4 = gcHeapStats.TotalPromotedSize4,
- FinalizationPromotedSize = gcHeapStats.FinalizationPromotedSize,
- FinalizationPromotedCount = gcHeapStats.FinalizationPromotedCount,
- PinnedObjectCount = gcHeapStats.PinnedObjectCount,
- SinkBlockCount = gcHeapStats.SinkBlockCount,
- GCHandleCount = gcHeapStats.GCHandleCount
+ Reason = gcSuspendEE.Reason.ToString(),
+ Count = gcSuspendEE.Count
};
- gcHeapStatsEvents.Add((evt.TimeStampRelativeMSec, heapStatEvent));
-
- markers.Data.Add(heapStatEvent);
- markers.Length++;
+ gcSuspendEEEvents.Push((evt.TimeStampRelativeMSec, gcSuspendEEEvent));
}
- else if (evt is GCAllocationTickTraceData allocationTick)
+ else if (evt.ID == GCSuspendEEStopEventID && evt is GCNoUserDataTraceData &&
+ gcSuspendEEEvents.Count > 0)
{
- markers.StartTime.Add(evt.TimeStampRelativeMSec);
- markers.EndTime.Add(evt.TimeStampRelativeMSec);
- markers.Category.Add(CategoryGC);
- markers.Phase.Add(FirefoxProfiler.MarkerPhase.Instance);
- markers.ThreadId.Add(profileThreadIndex);
- markers.Name.Add(GetFirefoxString($"{threadIndex} - GC Alloc ({thread.ThreadID})", profileThread));
+ var (gcSuspendEEEventStartTime, gcSuspendEEEvent) = gcSuspendEEEvents.Pop();
- var allocationTickEvent = new GCAllocationTickEvent
- {
- AllocationAmount = allocationTick.AllocationAmount,
- AllocationKind = allocationTick.AllocationKind switch
- {
- GCAllocationKind.Small => "Small",
- GCAllocationKind.Large => "Large",
- GCAllocationKind.Pinned => "Pinned",
- _ => "Unknown"
- },
- TypeName = allocationTick.TypeName,
- HeapIndex = allocationTick.HeapIndex
- };
- markers.Data.Add(allocationTickEvent);
+ markers.StartTime.Add(gcSuspendEEEventStartTime);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGc);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
+ markers.ThreadId.Add(_profileThreadIndex);
+ markers.Name.Add(GetOrCreateString($"GC Suspend EE", profileThread));
+ markers.Data.Add(gcSuspendEEEvent);
markers.Length++;
}
- else if (evt.ProviderGuid == ClrTraceEventParser.ProviderGuid)
+ else if (evt.ID == GCRestartEEStartEventID && evt is GCNoUserDataTraceData)
{
- if (evt is GCStartTraceData gcStart)
- {
- var gcEvent = new GCEvent
- {
- Reason = gcStart.Reason.ToString(),
- Count = gcStart.Count,
- Depth = gcStart.Depth,
- GCType = gcStart.Type.ToString()
- };
-
- gcStartStopEvents.Push((evt.TimeStampRelativeMSec, gcEvent));
- }
- else if (evt is GCEndTraceData gcEnd && gcStartStopEvents.Count > 0)
- {
- var (gcEventStartTime, gcEvent) = gcStartStopEvents.Pop();
-
- markers.StartTime.Add(gcEventStartTime);
- markers.EndTime.Add(evt.TimeStampRelativeMSec);
- markers.Category.Add(CategoryGC);
- markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
- markers.ThreadId.Add(profileThreadIndex);
- markers.Name.Add(GetFirefoxString($"GC Event", profileThread));
- markers.Data.Add(gcEvent);
- markers.Length++;
- }
- else if (evt is GCSuspendEETraceData gcSuspendEE)
- {
- var gcSuspendEEEvent = new GCSuspendExecutionEngineEvent
- {
- Reason = gcSuspendEE.Reason.ToString(),
- Count = gcSuspendEE.Count
- };
-
- gcSuspendEEEvents.Push((evt.TimeStampRelativeMSec, gcSuspendEEEvent));
- }
- else if (evt.ID == GCSuspendEEStopEventID && evt is GCNoUserDataTraceData &&
- gcSuspendEEEvents.Count > 0)
- {
- var (gcSuspendEEEventStartTime, gcSuspendEEEvent) = gcSuspendEEEvents.Pop();
-
- markers.StartTime.Add(gcSuspendEEEventStartTime);
- markers.EndTime.Add(evt.TimeStampRelativeMSec);
- markers.Category.Add(CategoryGC);
- markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
- markers.ThreadId.Add(profileThreadIndex);
- markers.Name.Add(GetFirefoxString($"GC Suspend EE", profileThread));
- markers.Data.Add(gcSuspendEEEvent);
- markers.Length++;
- }
- else if (evt.ID == GCRestartEEStartEventID && evt is GCNoUserDataTraceData)
- {
- gcRestartEEEvents.Push(evt.TimeStampRelativeMSec);
- }
- else if (evt.ID == GCRestartEEStopEventID && evt is GCNoUserDataTraceData &&
- gcRestartEEEvents.Count > 0)
- {
- var gcRestartEEEventStartTime = gcRestartEEEvents.Pop();
-
- markers.StartTime.Add(gcRestartEEEventStartTime);
- markers.EndTime.Add(evt.TimeStampRelativeMSec);
- markers.Category.Add(CategoryGC);
- markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
- markers.ThreadId.Add(profileThreadIndex);
- markers.Name.Add(GetFirefoxString($"GC Restart EE", profileThread));
- markers.Data.Add(null);
- markers.Length++;
- }
+ gcRestartEEEvents.Push(evt.TimeStampRelativeMSec);
}
- }
-
- continue;
- }
-
- if (evt.ProcessID != processId || evt.ThreadID != thread.ThreadID)
- {
- continue;
- }
-
- //Console.WriteLine($"PERF {evt}");
-
- var callStackIndex = evt.CallStackIndex();
- if (callStackIndex == CallStackIndex.Invalid)
- {
- continue;
- }
-
- // Add sample
- var firefoxCallStackIndex = ProcessCallStack(callStackIndex, log, profileThread);
+ else if (evt.ID == GCRestartEEStopEventID && evt is GCNoUserDataTraceData &&
+ gcRestartEEEvents.Count > 0)
+ {
+ var gcRestartEEEventStartTime = gcRestartEEEvents.Pop();
- var deltaTime = evt.TimeStampRelativeMSec - startTime;
- samples.TimeDeltas.Add(deltaTime);
- samples.Stack.Add(firefoxCallStackIndex);
- var cpuDeltaMs = (long) ((evt.TimeStampRelativeMSec - switchTimeInMsec) * 1_000_000.0);
- if (cpuDeltaMs > 0)
- {
- samples.ThreadCPUDelta.Add((int) cpuDeltaMs);
- }
- else
- {
- samples.ThreadCPUDelta.Add(0);
+ markers.StartTime.Add(gcRestartEEEventStartTime);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGc);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
+ markers.ThreadId.Add(_profileThreadIndex);
+ markers.Name.Add(GetOrCreateString($"GC Restart EE", profileThread));
+ markers.Data.Add(null);
+ markers.Length++;
+ }
+ }
}
- switchTimeInMsec = evt.TimeStampRelativeMSec;
- samples.Length++;
- startTime = evt.TimeStampRelativeMSec;
+ continue;
}
- profile.Threads.Add(profileThread);
-
- // Make visible threads in the UI that consume a minimum amount of CPU time
- if (thread.CPUMSec > MinimumCpuTimeBeforeThreadIsVisible)
+ if (evt.ProcessID != process.ProcessID || evt.ThreadID != thread.ThreadID)
{
- profile.Meta.InitialVisibleThreads.Add(profileThreadIndex);
+ continue;
}
- // We will select by default the thread that has the maximum activity
- if (thread.CPUMSec > maxCpuTime)
+ //Console.WriteLine($"PERF {evt}");
+
+ var callStackIndex = evt.CallStackIndex();
+ if (callStackIndex == CallStackIndex.Invalid)
{
- maxCpuTime = thread.CPUMSec;
- threadIndexWithMaxCpuTime = profileThreadIndex;
+ continue;
}
- profileThreadIndex++;
- }
-
- if (gcHeapStatsEvents.Count > 0)
- {
- gcHeapStatsEvents.Sort((a, b) => a.Item1.CompareTo(b.Item1));
+ // Add sample
+ var firefoxCallStackIndex = ConvertCallStack(callStackIndex, profileThread);
- var gcHeapStatsCounter = new FirefoxProfiler.Counter()
- {
- Name = "GCHeapStats",
- Category = "Memory", // Category must be Memory otherwise it won't be displayed
- Description = "GC Heap Stats",
- Color = FirefoxProfiler.ProfileColor.Orange, // Doesn't look like it is used
- Pid = $"{process.ProcessID}",
- MainThreadIndex = threadIndexWithMaxCpuTime,
- };
-
- //gcHeapStatsCounter.Samples.Number = new();
- gcHeapStatsCounter.Samples.Time = new();
-
- profile.Counters ??= new();
- profile.Counters.Add(gcHeapStatsCounter);
-
- long previousTotalHeapSize = 0;
-
- // Bug in Memory, they discard the first sample
- // and it is then not recording the first TotalHeapSize which is the initial value
- // So we force to create a dummy empty entry
- // https://github.com/firefox-devtools/profiler/blob/e9fe870f2a85b1c8771b1d671eb316bd1f5723ec/src/profile-logic/profile-data.js#L1732-L1753
- gcHeapStatsCounter.Samples.Time!.Add(0);
- gcHeapStatsCounter.Samples.Count.Add(0);
- gcHeapStatsCounter.Samples.Length++;
-
- foreach (var evt in gcHeapStatsEvents)
+ var deltaTime = evt.TimeStampRelativeMSec - startTime;
+ samples.TimeDeltas.Add(deltaTime);
+ samples.Stack.Add(firefoxCallStackIndex);
+ var cpuDeltaMs = (long) ((evt.TimeStampRelativeMSec - switchTimeInMsec) * 1_000_000.0);
+ if (cpuDeltaMs > 0)
{
- gcHeapStatsCounter.Samples.Time!.Add(evt.Item1);
- // The memory track is special and is assuming a delta
- var deltaMemory = evt.Item2.TotalHeapSize - previousTotalHeapSize;
- gcHeapStatsCounter.Samples.Count.Add(deltaMemory);
- gcHeapStatsCounter.Samples.Length++;
- previousTotalHeapSize = evt.Item2.TotalHeapSize;
+ samples.ThreadCPUDelta.Add((int) cpuDeltaMs);
}
- }
-
- 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))
+ else
{
- profile.Meta.InitialVisibleThreads.Add(threadIndexWithMaxCpuTime);
+ samples.ThreadCPUDelta.Add(0);
}
-
- profile.Meta.InitialSelectedThreads.Add(threadIndexWithMaxCpuTime);
- }
- }
-
- return profile;
- }
-
- private int ProcessCallStack(CallStackIndex callStackIndex, TraceLog log, FirefoxProfiler.Thread profileThread)
- {
- if (callStackIndex == CallStackIndex.Invalid) return -1;
-
- var parentCallStackIndex = log.CallStacks.Caller(callStackIndex);
- var fireFoxParentCallStackIndex = ProcessCallStack(parentCallStackIndex, log, profileThread);
-
- return GetFirefoxCallStackIndex(callStackIndex, fireFoxParentCallStackIndex, log, profileThread);
- }
-
- private int GetFirefoxString(string text, FirefoxProfiler.Thread profileThread)
- {
- if (_mapStringToFirefox.TryGetValue(text, out var index))
- {
- return index;
- }
- var firefoxStringIndex = profileThread.StringArray.Count;
- _mapStringToFirefox.Add(text, firefoxStringIndex);
-
- profileThread.StringArray.Add(text);
- return firefoxStringIndex;
- }
-
- private int GetFirefoxMethodIndex(CodeAddressIndex codeAddressIndex, MethodIndex methodIndex, TraceLog log, FirefoxProfiler.Thread profileThread)
- {
- var funcTable = profileThread.FuncTable;
- int firefoxMethodIndex;
- if (methodIndex == MethodIndex.Invalid)
- {
- if (_mapCodeAddressIndexToMethodIndexFirefox.TryGetValue(codeAddressIndex, out var index))
+
+ switchTimeInMsec = evt.TimeStampRelativeMSec;
+ samples.Length++;
+ startTime = evt.TimeStampRelativeMSec;
+ }
+
+ _profile.Threads.Add(profileThread);
+
+ // Make visible threads in the UI that consume a minimum amount of CPU time
+ if (thread.CPUMSec > _options.MinimumCpuTimeBeforeThreadIsVisibleInMs)
{
- return index;
+ _profile.Meta.InitialVisibleThreads!.Add(_profileThreadIndex);
}
- firefoxMethodIndex = funcTable.Length;
- _mapCodeAddressIndexToMethodIndexFirefox[codeAddressIndex] = firefoxMethodIndex;
- }
- else if (_mapMethodIndexToFirefox.TryGetValue(methodIndex, out var index))
- {
- return index;
+
+ // We will select by default the thread that has the maximum activity
+ if (thread.CPUMSec > maxCpuTime)
+ {
+ maxCpuTime = thread.CPUMSec;
+ threadIndexWithMaxCpuTime = _profileThreadIndex;
+ }
+
+ _profileThreadIndex++;
}
- else
+
+ // If we have GCHeapStatsEvents, we can create a Memory track
+ if (gcHeapStatsEvents.Count > 0)
{
- firefoxMethodIndex = funcTable.Length;
- _mapMethodIndexToFirefox.Add(methodIndex, firefoxMethodIndex);
+ gcHeapStatsEvents.Sort((a, b) => a.Item1.CompareTo(b.Item1));
+
+ var gcHeapStatsCounter = new FirefoxProfiler.Counter()
+ {
+ Name = "GCHeapStats",
+ Category = "Memory", // Category must be Memory otherwise it won't be displayed
+ Description = "GC Heap Stats",
+ Color = FirefoxProfiler.ProfileColor.Orange, // Doesn't look like it is used
+ Pid = $"{process.ProcessID}",
+ MainThreadIndex = threadIndexWithMaxCpuTime,
+ };
+
+ //gcHeapStatsCounter.Samples.Number = new();
+ gcHeapStatsCounter.Samples.Time = new();
+
+ _profile.Counters ??= new();
+ _profile.Counters.Add(gcHeapStatsCounter);
+
+ long previousTotalHeapSize = 0;
+
+ // Bug in Memory, they discard the first sample
+ // and it is then not recording the first TotalHeapSize which is the initial value
+ // So we force to create a dummy empty entry
+ // https://github.com/firefox-devtools/profiler/blob/e9fe870f2a85b1c8771b1d671eb316bd1f5723ec/src/profile-logic/profile-data.js#L1732-L1753
+ gcHeapStatsCounter.Samples.Time!.Add(0);
+ gcHeapStatsCounter.Samples.Count.Add(0);
+ gcHeapStatsCounter.Samples.Length++;
+
+ foreach (var evt in gcHeapStatsEvents)
+ {
+ gcHeapStatsCounter.Samples.Time!.Add(evt.Item1);
+ // The memory track is special and is assuming a delta
+ var deltaMemory = evt.Item2.TotalHeapSize - previousTotalHeapSize;
+ gcHeapStatsCounter.Samples.Count.Add(deltaMemory);
+ gcHeapStatsCounter.Samples.Length++;
+ previousTotalHeapSize = evt.Item2.TotalHeapSize;
+ }
}
-
- //public List Name { get; }
- //public List IsJS { get; }
- //public List RelevantForJS { get; }
- //public List Resource { get; }
- //public List FileName { get; }
- //public List LineNumber { get; }
- //public List ColumnNumber { get; }
- if (methodIndex == MethodIndex.Invalid)
+ if (threads.Count > 0)
{
- funcTable.Name.Add(GetFirefoxString($"0x{log.CodeAddresses.Address(codeAddressIndex):X16}", profileThread));
- funcTable.IsJS.Add(false);
- funcTable.RelevantForJS.Add(false);
- funcTable.Resource.Add(-1);
- funcTable.FileName.Add(null);
- funcTable.LineNumber.Add(null);
- funcTable.ColumnNumber.Add(null);
+ // Always make at least the first thread visible (that is taking most of the CPU time)
+ if (!_profile.Meta.InitialVisibleThreads!.Contains(threadIndexWithMaxCpuTime))
+ {
+ _profile.Meta.InitialVisibleThreads.Add(threadIndexWithMaxCpuTime);
+ }
+
+ _profile.Meta.InitialSelectedThreads!.Add(threadIndexWithMaxCpuTime);
}
- else
+ }
+
+ ///
+ /// Loads the modules - and symbols for a given process.
+ ///
+ /// The process to load the modules.
+ private void LoadModules(TraceProcess process)
+ {
+ _options.LogProgress?.Invoke($"Loading Modules for process {process.Name}");
+
+ var allModules = process.LoadedModules.ToList();
+ for (var i = 0; i < allModules.Count; i++)
{
- var fullMethodName = log.CodeAddresses.Methods.FullMethodName(methodIndex) ?? $"0x{log.CodeAddresses.Address(codeAddressIndex):X16}";
+ var module = allModules[i];
+ if (_mapModuleFileIndexToFirefox.ContainsKey(module.ModuleFile.ModuleFileIndex))
+ {
+ continue; // Skip in case
+ }
- var firefoxMethodNameIndex = GetFirefoxString(fullMethodName, profileThread);
- funcTable.Name.Add(firefoxMethodNameIndex);
- funcTable.IsJS.Add(false);
- funcTable.RelevantForJS.Add(false);
- funcTable.FileName.Add(null); // TODO
- funcTable.LineNumber.Add(null);
- funcTable.ColumnNumber.Add(null);
+ _options.LogStepProgress?.Invoke($"Loading Symbols [{i}/{allModules.Count}] for Module `{module.Name}`, ImageSize: {ByteSize.FromBytes(module.ModuleFile.ImageSize)}");
- var moduleIndex = log.CodeAddresses.ModuleFileIndex(codeAddressIndex);
- if (moduleIndex != ModuleFileIndex.Invalid)
+ var lib = new FirefoxProfiler.Lib
{
- funcTable.Resource.Add(profileThread.ResourceTable.Length); // TODO
- var moduleName = Path.GetFileName(log.ModuleFiles[moduleIndex].FilePath);
- profileThread.ResourceTable.Name.Add(GetFirefoxString(moduleName, profileThread));
- profileThread.ResourceTable.Lib.Add(_mapModuleFileIndexToFirefox[moduleIndex]);
- profileThread.ResourceTable.Length++;
+ Name = module.Name,
+ AddressStart = module.ImageBase,
+ AddressEnd = module.ModuleFile.ImageEnd,
+ Path = module.ModuleFile.FilePath,
+ DebugPath = module.ModuleFile.PdbName,
+ DebugName = module.ModuleFile.PdbName,
+ BreakpadId = $"0x{module.ModuleID:X16}",
+ Arch = "x64" // TODO
+ };
+
+ _traceLog!.CodeAddresses.LookupSymbolsForModule(_symbolReader, module.ModuleFile);
+ _mapModuleFileIndexToFirefox.Add(module.ModuleFile.ModuleFileIndex, _profile.Libs.Count);
+ _profile.Libs.Add(lib);
+
+ var fileName = Path.GetFileName(module.FilePath);
+ if (fileName.Equals("clrjit.dll", StringComparison.OrdinalIgnoreCase))
+ {
+ _clrJitModuleIndex = module.ModuleFile.ModuleFileIndex;
}
- else
+ else if (fileName.Equals("coreclr.dll", StringComparison.OrdinalIgnoreCase))
{
- funcTable.Resource.Add(-1);
+ _coreClrModuleIndex = module.ModuleFile.ModuleFileIndex;
+ }
+
+ if (module is TraceManagedModule managedModule)
+ {
+ _setManagedModules.Add(managedModule.ModuleFile.ModuleFileIndex);
+
+ foreach (var otherModule in allModules.Where(x => x is not TraceManagedModule))
+ {
+ if (string.Equals(managedModule.FilePath, otherModule.FilePath, StringComparison.OrdinalIgnoreCase))
+ {
+ _setManagedModules.Add(otherModule.ModuleFile.ModuleFileIndex);
+ }
+ }
}
}
+ }
- funcTable.Length++;
+ ///
+ /// Converts an ETW call stack to a Firefox call stack.
+ ///
+ /// The ETW callstack index to convert.
+ /// The current Firefox thread.
+ /// The converted Firefox call stack index.
+ private int ConvertCallStack(CallStackIndex callStackIndex, FirefoxProfiler.Thread profileThread)
+ {
+ if (callStackIndex == CallStackIndex.Invalid) return -1;
- return firefoxMethodIndex;
+ var parentCallStackIndex = _traceLog.CallStacks.Caller(callStackIndex);
+ var fireFoxParentCallStackIndex = ConvertCallStack(parentCallStackIndex, profileThread);
+
+ return ConvertCallStack(callStackIndex, fireFoxParentCallStackIndex, profileThread);
}
- private int GetFirefoxCallStackIndex(CallStackIndex callStackIndex, int firefoxParentCallStackIndex, TraceLog log, FirefoxProfiler.Thread profileThread)
+ ///
+ /// Converts an ETW call stack to a Firefox call stack.
+ ///
+ /// The ETW callstack index to convert.
+ /// The parent Firefox callstack index.
+ /// The current Firefox thread.
+ /// The converted Firefox call stack index.
+ private int ConvertCallStack(CallStackIndex callStackIndex, int firefoxParentCallStackIndex, FirefoxProfiler.Thread profileThread)
{
if (_mapCallStackIndexToFirefox.TryGetValue(callStackIndex, out var index))
{
@@ -754,20 +646,28 @@ private int GetFirefoxCallStackIndex(CallStackIndex callStackIndex, int firefoxP
var firefoxCallStackIndex = stackTable.Length;
_mapCallStackIndexToFirefox.Add(callStackIndex, firefoxCallStackIndex);
-
- var codeAddressIndex = log.CallStacks.CodeAddressIndex(callStackIndex);
- var frameTableIndex = GetFirefoxFrameTableIndex(codeAddressIndex, log, profileThread, out var category, out var subCategory);
-
+
+ var codeAddressIndex = _traceLog.CallStacks.CodeAddressIndex(callStackIndex);
+ var frameTableIndex = ConvertFrame(codeAddressIndex, profileThread, out var category, out var subCategory);
+
stackTable.Frame.Add(frameTableIndex);
stackTable.Category.Add(category);
stackTable.Subcategory.Add(subCategory);
stackTable.Prefix.Add(firefoxParentCallStackIndex < 0 ? null : (int)firefoxParentCallStackIndex);
stackTable.Length++;
-
+
return firefoxCallStackIndex;
}
- private int GetFirefoxFrameTableIndex(CodeAddressIndex codeAddressIndex, TraceLog log, FirefoxProfiler.Thread profileThread, out int category, out int subCategory)
+ ///
+ /// Converts an ETW code address to a Firefox frame.
+ ///
+ /// The ETW code address index.
+ /// The current Firefox thread.
+ /// The category of the frame.
+ /// The subcategory of the frame.
+ ///
+ private int ConvertFrame(CodeAddressIndex codeAddressIndex, FirefoxProfiler.Thread profileThread, out int category, out int subCategory)
{
var frameTable = profileThread.FrameTable;
@@ -781,8 +681,8 @@ private int GetFirefoxFrameTableIndex(CodeAddressIndex codeAddressIndex, TraceLo
firefoxFrameTableIndex = frameTable.Length;
_mapCodeAddressIndexToFirefox.Add(codeAddressIndex, firefoxFrameTableIndex);
- var module = log.CodeAddresses.ModuleFile(codeAddressIndex);
- var absoluteAddress = log.CodeAddresses.Address(codeAddressIndex);
+ var module = _traceLog.CodeAddresses.ModuleFile(codeAddressIndex);
+ var absoluteAddress = _traceLog.CodeAddresses.Address(codeAddressIndex);
var offsetIntoModule = module is not null ? (int)(absoluteAddress - module.ImageBase) : 0;
// Address
@@ -829,18 +729,19 @@ private int GetFirefoxFrameTableIndex(CodeAddressIndex codeAddressIndex, TraceLo
}
}
- var methodIndex = log.CodeAddresses.MethodIndex(codeAddressIndex);
- var firefoxMethodIndex = GetFirefoxMethodIndex(codeAddressIndex, methodIndex, log, profileThread);
+ var methodIndex = _traceLog.CodeAddresses.MethodIndex(codeAddressIndex);
+ var firefoxMethodIndex = ConvertMethod(codeAddressIndex, methodIndex, profileThread);
if (methodIndex != MethodIndex.Invalid)
{
var nameIndex = profileThread.FuncTable.Name[firefoxMethodIndex];
var fullMethodName = profileThread.StringArray[nameIndex];
// Hack to distinguish GC methods
+ // https://github.com/dotnet/runtime/blob/af3393d3991b7aab608e514e4a4be3ae2bbafbf8/src/coreclr/gc/gc.cpp#L49-L53
var isGC = fullMethodName.StartsWith("WKS::gc", StringComparison.OrdinalIgnoreCase) || fullMethodName.StartsWith("SVR::gc", StringComparison.OrdinalIgnoreCase);
if (isGC)
{
- category = CategoryGC;
+ category = CategoryGc;
}
}
@@ -870,9 +771,216 @@ private int GetFirefoxFrameTableIndex(CodeAddressIndex codeAddressIndex, TraceLo
return firefoxFrameTableIndex;
}
- public void Dispose()
+ ///
+ /// Converts an ETW method to a Firefox method.
+ ///
+ /// The original code address.
+ /// The method index. Can be invalid.
+ /// The current Firefox thread.
+ /// The converted Firefox method index.
+ private int ConvertMethod(CodeAddressIndex codeAddressIndex, MethodIndex methodIndex, FirefoxProfiler.Thread profileThread)
{
- _symbolReader?.Dispose();
- _etl.Dispose();
+ var funcTable = profileThread.FuncTable;
+ int firefoxMethodIndex;
+ if (methodIndex == MethodIndex.Invalid)
+ {
+ if (_mapCodeAddressIndexToMethodIndexFirefox.TryGetValue(codeAddressIndex, out var index))
+ {
+ return index;
+ }
+ firefoxMethodIndex = funcTable.Length;
+ _mapCodeAddressIndexToMethodIndexFirefox[codeAddressIndex] = firefoxMethodIndex;
+ }
+ else if (_mapMethodIndexToFirefox.TryGetValue(methodIndex, out var index))
+ {
+ return index;
+ }
+ else
+ {
+ firefoxMethodIndex = funcTable.Length;
+ _mapMethodIndexToFirefox.Add(methodIndex, firefoxMethodIndex);
+ }
+
+ //public List Name { get; }
+ //public List IsJS { get; }
+ //public List RelevantForJS { get; }
+ //public List Resource { get; }
+ //public List FileName { get; }
+ //public List LineNumber { get; }
+ //public List ColumnNumber { get; }
+
+ if (methodIndex == MethodIndex.Invalid)
+ {
+ funcTable.Name.Add(GetOrCreateString($"0x{_traceLog.CodeAddresses.Address(codeAddressIndex):X16}", profileThread));
+ funcTable.IsJS.Add(false);
+ funcTable.RelevantForJS.Add(false);
+ funcTable.Resource.Add(-1);
+ funcTable.FileName.Add(null);
+ funcTable.LineNumber.Add(null);
+ funcTable.ColumnNumber.Add(null);
+ }
+ else
+ {
+ var fullMethodName = _traceLog.CodeAddresses.Methods.FullMethodName(methodIndex) ?? $"0x{_traceLog.CodeAddresses.Address(codeAddressIndex):X16}";
+
+ var firefoxMethodNameIndex = GetOrCreateString(fullMethodName, profileThread);
+ funcTable.Name.Add(firefoxMethodNameIndex);
+ funcTable.IsJS.Add(false);
+ funcTable.RelevantForJS.Add(false);
+ funcTable.FileName.Add(null); // TODO
+ funcTable.LineNumber.Add(null);
+ funcTable.ColumnNumber.Add(null);
+
+ var moduleIndex = _traceLog.CodeAddresses.ModuleFileIndex(codeAddressIndex);
+ if (moduleIndex != ModuleFileIndex.Invalid)
+ {
+ funcTable.Resource.Add(profileThread.ResourceTable.Length); // TODO
+ var moduleName = Path.GetFileName(_traceLog.ModuleFiles[moduleIndex].FilePath);
+ profileThread.ResourceTable.Name.Add(GetOrCreateString(moduleName, profileThread));
+ profileThread.ResourceTable.Lib.Add(_mapModuleFileIndexToFirefox[moduleIndex]);
+ profileThread.ResourceTable.Length++;
+ }
+ else
+ {
+ funcTable.Resource.Add(-1);
+ }
+ }
+
+ funcTable.Length++;
+
+ return firefoxMethodIndex;
+ }
+
+ ///
+ /// Gets or creates a string for the specified Firefox profile thread.
+ ///
+ /// The string to create.
+ /// The current Firefox thread to create the string in.
+ /// The index of the string in the Firefox profile thread.
+ private int GetOrCreateString(string text, FirefoxProfiler.Thread profileThread)
+ {
+ if (_mapStringToFirefox.TryGetValue(text, out var index))
+ {
+ return index;
+ }
+ var firefoxStringIndex = profileThread.StringArray.Count;
+ _mapStringToFirefox.Add(text, firefoxStringIndex);
+
+ profileThread.StringArray.Add(text);
+ return firefoxStringIndex;
+ }
+
+ ///
+ /// Creates a new Firefox profile.
+ ///
+ /// A new Firefox profile.
+ private FirefoxProfiler.Profile CreateProfile()
+ {
+ var profile = new FirefoxProfiler.Profile
+ {
+ Meta =
+ {
+ StartTime = double.MaxValue,
+ EndTime = 0.0f,
+ ProfilingStartTime = double.MaxValue,
+ ProfilingEndTime = 0.0f,
+ Version = 29,
+ PreprocessedProfileVersion = 51,
+ Product = string.Empty,
+ InitialSelectedThreads = [],
+ Platform = $"{_traceLog.OSName} {_traceLog.OSVersion} {_traceLog.OSBuild}",
+ Oscpu = $"{_traceLog.OSName} {_traceLog.OSVersion} {_traceLog.OSBuild}",
+ LogicalCPUs = _traceLog.NumberOfProcessors,
+ DoesNotUseFrameImplementation = true,
+ Symbolicated = true,
+ SampleUnits = new FirefoxProfiler.SampleUnits
+ {
+ Time = "ms",
+ EventDelay = "ms",
+ ThreadCPUDelta = "ns"
+ },
+ InitialVisibleThreads = [],
+ Stackwalk = 1,
+ Interval = _traceLog.SampleProfileInterval.TotalMilliseconds,
+ Categories =
+ [
+ new FirefoxProfiler.Category()
+ {
+ Name = "Other",
+ Color = FirefoxProfiler.ProfileColor.Grey,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = "Kernel",
+ Color = FirefoxProfiler.ProfileColor.Orange,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = "Native",
+ Color = FirefoxProfiler.ProfileColor.Blue,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = ".NET",
+ Color = FirefoxProfiler.ProfileColor.Green,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = ".NET GC",
+ Color = FirefoxProfiler.ProfileColor.Yellow,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = ".NET JIT",
+ Color = FirefoxProfiler.ProfileColor.Purple,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = ".NET CLR",
+ Color = FirefoxProfiler.ProfileColor.Grey,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ ],
+ Abi = RuntimeInformation.RuntimeIdentifier,
+ MarkerSchema =
+ {
+ JitCompileEvent.Schema(),
+ GCEvent.Schema(),
+ GCHeapStatsEvent.Schema(),
+ GCAllocationTickEvent.Schema(),
+ GCSuspendExecutionEngineEvent.Schema(),
+ GCRestartExecutionEngineEvent.Schema(),
+ }
+ }
+ };
+
+ return profile;
}
}
\ No newline at end of file
diff --git a/src/Ultra.Core/EtwUltraProfiler.cs b/src/Ultra.Core/EtwUltraProfiler.cs
index c22b047..7265033 100644
--- a/src/Ultra.Core/EtwUltraProfiler.cs
+++ b/src/Ultra.Core/EtwUltraProfiler.cs
@@ -113,7 +113,7 @@ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions)
var process = System.Diagnostics.Process.GetProcessById(pidToAttach);
processList.Add(process);
}
- catch (ArgumentException ex)
+ catch (ArgumentException)
{
throw new ArgumentException($"Unable to find Process with pid {pidToAttach}");
}
@@ -383,8 +383,7 @@ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions)
/// Thrown when a stop request is received.
public async Task Convert(string etlFile, List pIds, EtwUltraProfilerOptions ultraProfilerOptions)
{
- var etlProcessor = new EtwConverterToFirefox();
- var profile = etlProcessor.Convert(etlFile, pIds, ultraProfilerOptions);
+ var profile = EtwConverterToFirefox.Convert(etlFile, ultraProfilerOptions, pIds);
if (_stopRequested)
{
@@ -435,12 +434,12 @@ private async Task EnableProfiling(TraceEventProviderOptions options, EtwUltraPr
throw new InvalidOperationException("CTRL+C requested");
}
- _kernelSession.StopOnDispose = true;
+ _kernelSession!.StopOnDispose = true;
_kernelSession.CircularBufferMB = 0;
_kernelSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs;
_kernelSession.StackCompression = false;
- _userSession.StopOnDispose = true;
+ _userSession!.StopOnDispose = true;
_userSession.CircularBufferMB = 0;
_userSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs;
_userSession.StackCompression = false;
diff --git a/src/Ultra.Core/EtwUltraProfilerOptions.cs b/src/Ultra.Core/EtwUltraProfilerOptions.cs
index bbcda1a..b6ca76d 100644
--- a/src/Ultra.Core/EtwUltraProfilerOptions.cs
+++ b/src/Ultra.Core/EtwUltraProfilerOptions.cs
@@ -24,6 +24,7 @@ public EtwUltraProfilerOptions()
KeepEtlIntermediateFiles = false;
DelayInSeconds = 0.0; // 0 seconds
DurationInSeconds = 120.0; // 120 seconds
+ MinimumCpuTimeBeforeThreadIsVisibleInMs = 10.0;
}
///
@@ -136,6 +137,11 @@ public EtwUltraProfilerOptions()
///
public string? BaseOutputFileName { get; set; }
+ ///
+ /// Gets or sets the minimum CPU time before a thread is visible in milliseconds.
+ ///
+ public double MinimumCpuTimeBeforeThreadIsVisibleInMs { get; set; }
+
///
/// Ensures that the directory for the base output file name exists.
///
diff --git a/src/Ultra.Core/FirefoxProfiler.cs b/src/Ultra.Core/FirefoxProfiler.cs
index cd772c3..f16d2d1 100644
--- a/src/Ultra.Core/FirefoxProfiler.cs
+++ b/src/Ultra.Core/FirefoxProfiler.cs
@@ -6,17 +6,18 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
// ReSharper disable InconsistentNaming
namespace Ultra.Core;
-// https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js
-
-// Ideas: https://github.com/parttimenerd/jfrtofp/blob/main/src/main/kotlin/me/bechberger/jfrtofp/types/Marker.kt
-
-// Profile code source loading:
-// https://github.com/firefox-devtools/profiler/blob/e51f64485f85091e5c3f5fc692e69068b3324fbd/src/utils/special-paths.js#L52-L90
-
+///
+/// The Firefox profiler JSON format.
+///
+///
+/// This file was manually converted from https://github.com/xoofx/firefox-profiler/blob/main/src/types/profile.js
+///
public static partial class FirefoxProfiler
{
[JsonSourceGenerationOptions(
@@ -1158,6 +1159,12 @@ public class MarkerPayload
public Dictionary? ExtensionData { get; set; }
+ ///
+ /// Writes the JSON representation of the current object.
+ ///
+ /// The JSON writer.
+ /// The marker payload.
+ /// The JSON serializer options.
protected internal virtual void WriteJson(Utf8JsonWriter writer, MarkerPayload payload, JsonSerializerOptions options)
{
}
@@ -1218,7 +1225,11 @@ private class MarkerPayloadConverter : JsonConverter
payload.ExtensionData[propertyName] = null;
break;
default:
+#pragma warning disable IL3050
+#pragma warning disable IL2026
payload.ExtensionData[propertyName] = JsonSerializer.Deserialize