From aa50acf9d18511c1abcf9cd29f7a879c97b38585 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Thu, 28 Nov 2024 09:10:57 +0100 Subject: [PATCH] Break API to cleanup, add more documentation and JSON tests (#3) --- src/Directory.Packages.props | 4 +- src/Ultra.Core/EtwConverterToFirefox.cs | 1344 +++++++++-------- src/Ultra.Core/EtwUltraProfiler.cs | 9 +- src/Ultra.Core/EtwUltraProfilerOptions.cs | 6 + src/Ultra.Core/FirefoxProfiler.cs | 29 +- .../Markers/GCAllocationTickEvent.cs | 101 +- src/Ultra.Core/Markers/GCEvent.cs | 37 +- src/Ultra.Core/Markers/GCHeapStatsEvent.cs | 67 +- .../Markers/GCRestartExecutionEngineEvent.cs | 15 +- .../Markers/GCSuspendExecutionEngineEvent.cs | 26 +- src/Ultra.Core/Markers/JitCompileEvent.cs | 55 +- src/Ultra.Core/Ultra.Core.csproj | 1 + src/Ultra.Example/Program.cs | 4 + src/Ultra.Tests/EtwUltraProfilerTests.cs | 14 + src/Ultra.Tests/FirefoxProfilerTests.cs | 213 +-- src/Ultra.Tests/TestInitializer.cs | 22 + src/Ultra.Tests/Ultra.Tests.csproj | 4 +- ...oxProfilerTests.TestSerialize.verified.txt | 468 ++++++ src/Ultra/Program.cs | 5 +- 19 files changed, 1543 insertions(+), 881 deletions(-) create mode 100644 src/Ultra.Tests/EtwUltraProfilerTests.cs create mode 100644 src/Ultra.Tests/TestInitializer.cs create mode 100644 src/Ultra.Tests/Verified/FirefoxProfilerTests.TestSerialize.verified.txt 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(ref reader); +#pragma warning restore IL2026 +#pragma warning restore IL3050 break; } } @@ -1283,7 +1294,11 @@ public override void Write(Utf8JsonWriter writer, MarkerPayload payload, JsonSer writer.WriteNullValue(); break; default: +#pragma warning disable IL3050 +#pragma warning disable IL2026 JsonSerializer.Serialize(writer, value); +#pragma warning restore IL2026 +#pragma warning restore IL3050 break; } } diff --git a/src/Ultra.Core/Markers/GCAllocationTickEvent.cs b/src/Ultra.Core/Markers/GCAllocationTickEvent.cs index 079500c..44f981f 100644 --- a/src/Ultra.Core/Markers/GCAllocationTickEvent.cs +++ b/src/Ultra.Core/Markers/GCAllocationTickEvent.cs @@ -6,23 +6,46 @@ namespace Ultra.Core.Markers; +/// +/// Represents a garbage collection allocation tick event marker payload for Firefox Profiler. +/// public class GCAllocationTickEvent : FirefoxProfiler.MarkerPayload { + /// + /// The type identifier for GC allocation tick events. + /// public const string TypeId = "GCMinor"; // dotnet.gc.allocation_tick + /// + /// Initializes a new instance of the class. + /// public GCAllocationTickEvent() { Type = TypeId; + AllocationKind = string.Empty; } + /// + /// Gets or sets the amount of memory allocated. + /// public long AllocationAmount { get; set; } - public string AllocationKind { get; set; } + /// + /// Gets or sets the kind of allocation. + /// + public string? AllocationKind { get; set; } - public string TypeName { get; set; } + /// + /// Gets or sets the type name of the allocated object. + /// + public string? TypeName { get; set; } + /// + /// Gets or sets the index of the heap where the allocation occurred. + /// public int HeapIndex { get; set; } - + + /// protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfiler.MarkerPayload payload, JsonSerializerOptions options) { writer.WriteNumber("allocationAmount", AllocationAmount); @@ -34,6 +57,10 @@ protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfile // https://github.com/xoofx/firefox-profiler/blob/56ac64c17b79b964c1263e8022dd2db3399f230f/src/components/tooltip/GCMarker.js#L28-L32 } + /// + /// Gets the schema for the GC allocation tick event. + /// + /// The marker schema. public static FirefoxProfiler.MarkerSchema Schema() => new() { @@ -42,44 +69,44 @@ public static FirefoxProfiler.MarkerSchema Schema() TableLabel = "GC Allocation: {marker.data.typeName}, Amount: {marker.data.allocationAmount}", Display = { - FirefoxProfiler.MarkerDisplayLocation.TimelineOverview, - FirefoxProfiler.MarkerDisplayLocation.MarkerChart, - FirefoxProfiler.MarkerDisplayLocation.MarkerTable + FirefoxProfiler.MarkerDisplayLocation.TimelineOverview, + FirefoxProfiler.MarkerDisplayLocation.MarkerChart, + FirefoxProfiler.MarkerDisplayLocation.MarkerTable }, Graphs = [ new() - { - Key = "allocationAmount", - Color = FirefoxProfiler.ProfileColor.Red, - Type = FirefoxProfiler.MarkerGraphType.Bar, - } - ], + { + Key = "allocationAmount", + Color = FirefoxProfiler.ProfileColor.Red, + Type = FirefoxProfiler.MarkerGraphType.Bar, + } + ], Data = { - new FirefoxProfiler.MarkerDataItem() - { - Format = FirefoxProfiler.MarkerFormatType.Bytes, - Key = "allocationAmount", - Label = "Allocation Amount", - }, - new FirefoxProfiler.MarkerDataItem() - { - Format = FirefoxProfiler.MarkerFormatType.String, - Key = "allocationKind", - Label = "Allocation Kind", - }, - new FirefoxProfiler.MarkerDataItem() - { - Format = FirefoxProfiler.MarkerFormatType.String, - Key = "typeName", - Label = "Type Name", - }, - new FirefoxProfiler.MarkerDataItem() - { - Format = FirefoxProfiler.MarkerFormatType.Integer, - Key = "heapIndex", - Label = "Heap Index", - } + new FirefoxProfiler.MarkerDataItem() + { + Format = FirefoxProfiler.MarkerFormatType.Bytes, + Key = "allocationAmount", + Label = "Allocation Amount", + }, + new FirefoxProfiler.MarkerDataItem() + { + Format = FirefoxProfiler.MarkerFormatType.String, + Key = "allocationKind", + Label = "Allocation Kind", + }, + new FirefoxProfiler.MarkerDataItem() + { + Format = FirefoxProfiler.MarkerFormatType.String, + Key = "typeName", + Label = "Type Name", + }, + new FirefoxProfiler.MarkerDataItem() + { + Format = FirefoxProfiler.MarkerFormatType.Integer, + Key = "heapIndex", + Label = "Heap Index", + } } }; -} \ No newline at end of file +} diff --git a/src/Ultra.Core/Markers/GCEvent.cs b/src/Ultra.Core/Markers/GCEvent.cs index 575ca03..a62e3d2 100644 --- a/src/Ultra.Core/Markers/GCEvent.cs +++ b/src/Ultra.Core/Markers/GCEvent.cs @@ -6,23 +6,45 @@ namespace Ultra.Core.Markers; +/// +/// Represents a garbage collection event marker payload for Firefox Profiler. +/// public class GCEvent : FirefoxProfiler.MarkerPayload { + /// + /// The type identifier for the GC event. + /// public const string TypeId = "GCMajor"; // Use a predefined type to have a different marker style in the timeline + /// + /// Initializes a new instance of the class. + /// public GCEvent() { Type = TypeId; } - public string Reason { get; set; } + /// + /// Gets or sets the reason for the garbage collection. + /// + public string? Reason { get; set; } + /// + /// Gets or sets the count of garbage collections. + /// public int Count { get; set; } + /// + /// Gets or sets the depth of the garbage collection. + /// public int Depth { get; set; } - public string GCType { get; set; } - + /// + /// Gets or sets the type of garbage collection. + /// + public string? GCType { get; set; } + + /// protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfiler.MarkerPayload payload, JsonSerializerOptions options) { writer.WriteString("reason", Reason); @@ -30,15 +52,18 @@ protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfile writer.WriteNumber("depth", Depth); writer.WriteString("gcType", GCType); - // This is a dummy field to make it compatible with firefox-profiler - ugly hack, but there is no way to use our own marker styles, so we are reusing GCMajor here. // But we need to workaround the following code that expect some fields to be present: // https://github.com/xoofx/firefox-profiler/blob/56ac64c17b79b964c1263e8022dd2db3399f230f/src/components/tooltip/GCMarker.js#L218-L224 - writer.WriteStartObject("timings"); + writer.WriteStartObject("timings"); writer.WriteString("status", string.Empty); writer.WriteEndObject(); } + /// + /// Returns the schema for the GC event marker. + /// + /// The schema for the GC event marker. public static FirefoxProfiler.MarkerSchema Schema() => new() { @@ -79,4 +104,4 @@ public static FirefoxProfiler.MarkerSchema Schema() } }, }; -} \ No newline at end of file +} diff --git a/src/Ultra.Core/Markers/GCHeapStatsEvent.cs b/src/Ultra.Core/Markers/GCHeapStatsEvent.cs index 9b7b0d3..919d014 100644 --- a/src/Ultra.Core/Markers/GCHeapStatsEvent.cs +++ b/src/Ultra.Core/Markers/GCHeapStatsEvent.cs @@ -7,49 +7,110 @@ namespace Ultra.Core.Markers; +/// +/// Represents a garbage collection heap stats event marker payload for Firefox Profiler. +/// public class GCHeapStatsEvent : FirefoxProfiler.MarkerPayload { + /// + /// The type identifier for the GCHeapStatsEvent. + /// public const string TypeId = "dotnet.gc.heap_stats"; + /// + /// Initializes a new instance of the class. + /// public GCHeapStatsEvent() { Type = TypeId; } + /// + /// Gets or sets the total heap size. + /// public long TotalHeapSize { get; set; } + /// + /// Gets or sets the total promoted size. + /// public long TotalPromoted { get; set; } + /// + /// Gets or sets the size of generation 0. + /// public long GenerationSize0 { get; set; } + /// + /// Gets or sets the total promoted size of generation 0. + /// public long TotalPromotedSize0 { get; set; } + /// + /// Gets or sets the size of generation 1. + /// public long GenerationSize1 { get; set; } + /// + /// Gets or sets the total promoted size of generation 1. + /// public long TotalPromotedSize1 { get; set; } + /// + /// Gets or sets the size of generation 2. + /// public long GenerationSize2 { get; set; } + /// + /// Gets or sets the total promoted size of generation 2. + /// public long TotalPromotedSize2 { get; set; } + /// + /// Gets or sets the size of generation 3. + /// public long GenerationSize3 { get; set; } + /// + /// Gets or sets the total promoted size of generation 3. + /// public long TotalPromotedSize3 { get; set; } + /// + /// Gets or sets the size of generation 4. + /// public long GenerationSize4 { get; set; } + /// + /// Gets or sets the total promoted size of generation 4. + /// public long TotalPromotedSize4 { get; set; } + /// + /// Gets or sets the finalization promoted size. + /// public long FinalizationPromotedSize { get; set; } + /// + /// Gets or sets the finalization promoted count. + /// public long FinalizationPromotedCount { get; set; } + /// + /// Gets or sets the pinned object count. + /// public int PinnedObjectCount { get; set; } + /// + /// Gets or sets the sink block count. + /// public int SinkBlockCount { get; set; } + /// + /// Gets or sets the GC handle count. + /// public int GCHandleCount { get; set; } - + + /// protected internal override void WriteJson(Utf8JsonWriter writer, MarkerPayload payload, JsonSerializerOptions options) { writer.WriteNumber("totalHeapSize", TotalHeapSize); @@ -71,6 +132,10 @@ protected internal override void WriteJson(Utf8JsonWriter writer, MarkerPayload writer.WriteNumber("gcHandleCount", GCHandleCount); } + /// + /// Gets the schema for the GCHeapStatsEvent. + /// + /// The schema for the GCHeapStatsEvent. public static MarkerSchema Schema() => new() { diff --git a/src/Ultra.Core/Markers/GCRestartExecutionEngineEvent.cs b/src/Ultra.Core/Markers/GCRestartExecutionEngineEvent.cs index b94cbae..6bc8d79 100644 --- a/src/Ultra.Core/Markers/GCRestartExecutionEngineEvent.cs +++ b/src/Ultra.Core/Markers/GCRestartExecutionEngineEvent.cs @@ -4,15 +4,28 @@ namespace Ultra.Core.Markers; +/// +/// Represents an event that indicates the .NET garbage collector has restarted the execution engine. +/// public class GCRestartExecutionEngineEvent : FirefoxProfiler.MarkerPayload { + /// + /// The type identifier for this event. + /// public const string TypeId = "dotnet.gc.restart_execution_engine"; + /// + /// Initializes a new instance of the class. + /// public GCRestartExecutionEngineEvent() { Type = TypeId; } + /// + /// Gets the schema for the GC Restart Execution Engine event. + /// + /// A object that defines the schema. public static FirefoxProfiler.MarkerSchema Schema() => new() { @@ -26,4 +39,4 @@ public static FirefoxProfiler.MarkerSchema Schema() FirefoxProfiler.MarkerDisplayLocation.MarkerTable } }; -} \ No newline at end of file +} diff --git a/src/Ultra.Core/Markers/GCSuspendExecutionEngineEvent.cs b/src/Ultra.Core/Markers/GCSuspendExecutionEngineEvent.cs index d29b300..023cf92 100644 --- a/src/Ultra.Core/Markers/GCSuspendExecutionEngineEvent.cs +++ b/src/Ultra.Core/Markers/GCSuspendExecutionEngineEvent.cs @@ -6,25 +6,45 @@ namespace Ultra.Core.Markers; +/// +/// Represents an event that marks the suspension of the execution engine by the garbage collector. +/// public class GCSuspendExecutionEngineEvent : FirefoxProfiler.MarkerPayload { + /// + /// The type identifier for this event. + /// public const string TypeId = "dotnet.gc.suspend_execution_engine"; + /// + /// Initializes a new instance of the class. + /// public GCSuspendExecutionEngineEvent() { Type = TypeId; } - public string Reason { get; set; } + /// + /// Gets or sets the reason for the suspension. + /// + public string? Reason { get; set; } + /// + /// Gets or sets the count of suspensions. + /// public int Count { get; set; } - + + /// protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfiler.MarkerPayload payload, JsonSerializerOptions options) { writer.WriteString("reason", Reason); writer.WriteNumber("count", Count); } + /// + /// Returns the schema for this marker. + /// + /// The schema for this marker. public static FirefoxProfiler.MarkerSchema Schema() => new() { @@ -53,4 +73,4 @@ public static FirefoxProfiler.MarkerSchema Schema() }, }, }; -} \ No newline at end of file +} diff --git a/src/Ultra.Core/Markers/JitCompileEvent.cs b/src/Ultra.Core/Markers/JitCompileEvent.cs index c5278df..0d7d5af 100644 --- a/src/Ultra.Core/Markers/JitCompileEvent.cs +++ b/src/Ultra.Core/Markers/JitCompileEvent.cs @@ -6,27 +6,46 @@ namespace Ultra.Core.Markers; +/// +/// Represents a JIT compile event marker payload for the Firefox Profiler. +/// public class JitCompileEvent : FirefoxProfiler.MarkerPayload { - // Use CC instead of dotnet.jit.compile because Firefox Profiler is hardcoding styles based on names :( See https://github.com/firefox-devtools/profiler/blob/main/src/profile-logic/marker-styles.js - public const string TypeId = "CC"; + /// + /// The type identifier for the JIT compile event. + /// + public const string TypeId = "CC"; // Use CC instead of dotnet.jit.compile because Firefox Profiler is hardcoding styles based on names :( See https://github.com/firefox-devtools/profiler/blob/main/src/profile-logic/marker-styles.js + /// + /// Initializes a new instance of the class. + /// public JitCompileEvent() { Type = TypeId; FullName = string.Empty; } + /// + /// Gets or sets the full name of the method. + /// public string FullName { get; set; } + /// + /// Gets or sets the IL size of the method. + /// public int MethodILSize { get; set; } + /// protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfiler.MarkerPayload payload, JsonSerializerOptions options) { writer.WriteString("fullName", FullName); writer.WriteNumber("methodILSize", MethodILSize); } + /// + /// Gets the schema for the JIT compile event marker. + /// + /// The marker schema. public static FirefoxProfiler.MarkerSchema Schema() => new() { @@ -37,25 +56,25 @@ public static FirefoxProfiler.MarkerSchema Schema() Display = { - FirefoxProfiler.MarkerDisplayLocation.TimelineOverview, - FirefoxProfiler.MarkerDisplayLocation.MarkerChart, - FirefoxProfiler.MarkerDisplayLocation.MarkerTable + FirefoxProfiler.MarkerDisplayLocation.TimelineOverview, + FirefoxProfiler.MarkerDisplayLocation.MarkerChart, + FirefoxProfiler.MarkerDisplayLocation.MarkerTable }, Data = { - new FirefoxProfiler.MarkerDataItem() - { - Format = FirefoxProfiler.MarkerFormatType.String, - Key = "fullName", - Label = "Full Name", - }, - new FirefoxProfiler.MarkerDataItem() - { - Format = FirefoxProfiler.MarkerFormatType.Integer, - Key = "methodILSize", - Label = "Method IL Size", - }, + new FirefoxProfiler.MarkerDataItem() + { + Format = FirefoxProfiler.MarkerFormatType.String, + Key = "fullName", + Label = "Full Name", + }, + new FirefoxProfiler.MarkerDataItem() + { + Format = FirefoxProfiler.MarkerFormatType.Integer, + Key = "methodILSize", + Label = "Method IL Size", + }, }, }; -} \ No newline at end of file +} diff --git a/src/Ultra.Core/Ultra.Core.csproj b/src/Ultra.Core/Ultra.Core.csproj index 9bd39b4..e86ecd0 100644 --- a/src/Ultra.Core/Ultra.Core.csproj +++ b/src/Ultra.Core/Ultra.Core.csproj @@ -23,6 +23,7 @@ true true snupkg + True diff --git a/src/Ultra.Example/Program.cs b/src/Ultra.Example/Program.cs index ec3ed27..ba8c67b 100644 --- a/src/Ultra.Example/Program.cs +++ b/src/Ultra.Example/Program.cs @@ -1,3 +1,7 @@ +// 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. + // Sample program using Markdig and Scriban to create a workload example for profiling with ultra const int countBenchMarkdig = 500; diff --git a/src/Ultra.Tests/EtwUltraProfilerTests.cs b/src/Ultra.Tests/EtwUltraProfilerTests.cs new file mode 100644 index 0000000..6909ceb --- /dev/null +++ b/src/Ultra.Tests/EtwUltraProfilerTests.cs @@ -0,0 +1,14 @@ +// 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. + +using Ultra.Core; + +namespace Ultra.Tests; + +/// +/// No tests for for now as it requires running as admin. +/// +public class EtwUltraProfilerTests : VerifyBase +{ +} diff --git a/src/Ultra.Tests/FirefoxProfilerTests.cs b/src/Ultra.Tests/FirefoxProfilerTests.cs index d7d6420..c30577a 100644 --- a/src/Ultra.Tests/FirefoxProfilerTests.cs +++ b/src/Ultra.Tests/FirefoxProfilerTests.cs @@ -11,10 +11,10 @@ namespace Ultra.Tests; /// -/// No real tests here yet, just to check the serialization of Firefox profiler format +/// Basic tests for JSON serialization/deserialization of Firefox Profiler format. /// [TestClass] -public class FirefoxProfilerTests +public class FirefoxProfilerTests : VerifyBase { [TestMethod] public void TestMarker() @@ -23,159 +23,47 @@ public void TestMarker() { "type": "hello", "value": 1 } """; var marker = JsonSerializer.Deserialize(markerText, JsonProfilerContext.Default.MarkerPayload); + + Assert.IsNotNull(marker); + Assert.AreEqual("hello", marker.Type); + Assert.IsNotNull(marker.ExtensionData); + Assert.IsTrue(marker.ExtensionData.ContainsKey("value"), "The extension data is missing value"); + Assert.AreEqual(1.0, marker.ExtensionData["value"]); } [TestMethod] - public void TestSimple() + public async Task TestSerialize() { - var profile = new Profile(); - - profile.Meta.StartTime = 0; - profile.Meta.EndTime = 2000; - profile.Meta.Version = 29; - profile.Meta.PreprocessedProfileVersion = 50; - - profile.Meta.Product = "myapp.exe"; - profile.Meta.InitialSelectedThreads = new(); - profile.Meta.InitialSelectedThreads.Add(0); - - profile.Meta.Platform = "Windows"; - profile.Meta.Oscpu = RuntimeInformation.ProcessArchitecture.ToString(); - profile.Meta.LogicalCPUs = Environment.ProcessorCount; - // We don't have access to physical CPUs - //profile.Meta.PhysicalCPUs = Environment.ProcessorCount / 2; - //profile.Meta.CPUName = ""; // TBD - - - profile.Meta.InitialVisibleThreads = new(); - profile.Meta.InitialVisibleThreads.Add(0); - - profile.Meta.Stackwalk = 1; - - - profile.Meta.Interval = 1.0; - profile.Meta.Categories = - [ - new Category() - { - Name = "Kernel", - Color = ProfileColor.Orange, - }, - new Category() - { - Name = "Native User", - Color = ProfileColor.Blue, - }, - new Category() - { - Name = ".NET", - Color = ProfileColor.Green, - }, - new Category() - { - Name = "GC", - Color = ProfileColor.Yellow, - }, - new Category() - { - Name = "JIT", - Color = ProfileColor.Purple, - }, - ]; - - var thread = new FirefoxProfiler.Thread(); - thread.Name = "Main"; - thread.Tid = "125"; - thread.Pid = "My Process"; - - // Frame table - var frameTable = thread.FrameTable; - frameTable.Category.Add(0); - frameTable.Address.Add(-1); - frameTable.Line.Add(null); - frameTable.Column.Add(null); - frameTable.InlineDepth.Add(0); - frameTable.Subcategory.Add(null); - frameTable.Func.Add(0); - frameTable.NativeSymbol.Add(null); - frameTable.Implementation.Add(null); - frameTable.InnerWindowID.Add(null); - frameTable.Length = 1; - - // Function table - var funcTable = thread.FuncTable; - funcTable.Name.Add(0); // myfunction - funcTable.IsJS.Add(false); - funcTable.RelevantForJS.Add(false); - funcTable.Resource.Add(0); - funcTable.LineNumber.Add(null); - funcTable.ColumnNumber.Add(null); - funcTable.Length = 1; - - // Stack and prefix - var stackTable = thread.StackTable; - stackTable.Frame.Add(0); - stackTable.Prefix.Add(null); - stackTable.Category.Add(0); - stackTable.Subcategory.Add(0); - stackTable.Length = 1; - - // Samples - var samples = thread.Samples; - - samples.TimeDeltas = new(); - samples.TimeDeltas.Add(10); - samples.Stack.Add(0); - samples.TimeDeltas.Add(10); - samples.Stack.Add(0); - samples.TimeDeltas.Add(10); - samples.Stack.Add(0); - samples.TimeDeltas.Add(10); - samples.Stack.Add(0); - samples.WeightType = "samples"; - samples.Length = 4; - //samples.Responsiveness.Add(0); + var profile = CreateProfile(); - var strings = thread.StringArray; - strings.Add("myfunction"); - - // Resource table - thread.ResourceTable.Name.Add(0); - thread.ResourceTable.Lib.Add(0); - thread.ResourceTable.Host.Add(null); - //unknown: 0, - //library: 1, - //addon: 2, - //webhost: 3, - //otherhost: 4, - //url: 5, - thread.ResourceTable.Type.Add(1); - thread.ResourceTable.Length = 1; - thread.ProcessType = "default"; + var newOptions = new JsonSerializerOptions(JsonProfilerContext.Default.Options) + { + WriteIndented = true + }; + var result = JsonSerializer.Serialize(profile, newOptions); - //thread.JsAllocations = null; + await Verify(result); + } - profile.Threads.Add(thread); + [TestMethod] + public void TestDeserialize() + { + var profile = CreateProfile(); - var lib = new Lib(); - lib.Name = "mylib"; - lib.AddressStart = 0x10000000; - lib.AddressEnd = 0x20000000; - lib.AddressOffset = 0x0; - lib.Path = "/path/to/mylib"; - lib.DebugName = "mylib.pdb"; - lib.DebugPath = "/path/to/mylib.pdb"; - lib.BreakpadId = "1234567890"; - profile.Libs.Add(lib); + var newOptions = new JsonSerializerOptions(JsonProfilerContext.Default.Options) + { + WriteIndented = true + }; - var result = JsonSerializer.Serialize(profile, JsonProfilerContext.Default.Profile); + var result = JsonSerializer.Serialize(profile, newOptions); + var newProfile = JsonSerializer.Deserialize(result, newOptions); + var newSerialized = JsonSerializer.Serialize(newProfile, newOptions); - Console.WriteLine(result); + Assert.AreEqual(result, newSerialized); } - [TestMethod] - public void TestSimpleWithAddresses() + private static FirefoxProfiler.Profile CreateProfile() { var profile = new Profile(); @@ -240,43 +128,6 @@ public void TestSimpleWithAddresses() profile.Meta.MarkerSchema.Add(JitCompileEvent.Schema()); - //profile.Meta.MarkerSchema.Add(new FirefoxProfiler.MarkerSchema() - //{ - // Name = FirefoxProfiler.JitCompile.TypeId, - - // ChartLabel = "memory size (chart): {marker.data.memorySize} bytes - Hello", - // TableLabel = "memory size (table): {marker.data.memorySize} bytes", - - // Display = - // { - // FirefoxProfiler.MarkerDisplayLocation.TimelineOverview, - // FirefoxProfiler.MarkerDisplayLocation.TimelineMemory, - // FirefoxProfiler.MarkerDisplayLocation.StackChart, - // FirefoxProfiler.MarkerDisplayLocation.MarkerChart, - // FirefoxProfiler.MarkerDisplayLocation.MarkerTable - // }, - - // Data = - // { - // new FirefoxProfiler.MarkerDataItem() - // { - // Format = FirefoxProfiler.MarkerFormatType.Integer, - // Key = "memorySize", - // Label = "Memory Size", - // } - // }, - - // Graphs = new() - // { - // new FirefoxProfiler.MarkerGraph() - // { - // Key = "memorySize", - // Type = FirefoxProfiler.MarkerGraphType.LineFilled, - // Color = FirefoxProfiler.ProfileColor.Blue, - // } - // } - //}); - var thread = new FirefoxProfiler.Thread(); thread.Name = "Main"; thread.Tid = "125"; @@ -401,8 +252,6 @@ public void TestSimpleWithAddresses() lib.BreakpadId = "1234567890"; profile.Libs.Add(lib); - var result = JsonSerializer.Serialize(profile, JsonProfilerContext.Default.Options); - - Console.WriteLine(result); + return profile; } } diff --git a/src/Ultra.Tests/TestInitializer.cs b/src/Ultra.Tests/TestInitializer.cs new file mode 100644 index 0000000..335acba --- /dev/null +++ b/src/Ultra.Tests/TestInitializer.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. + +using System.Globalization; +using System.Runtime.CompilerServices; +using VerifyTests.DiffPlex; + +namespace Ultra.Tests; + +internal static class TestsInitializer +{ + [ModuleInitializer] + public static void Initialize() + { + VerifyDiffPlex.Initialize(OutputType.Compact); + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + Verifier.UseProjectRelativeDirectory("Verified"); + DiffEngine.DiffRunner.Disabled = true; + VerifierSettings.DontScrubSolutionDirectory(); + } +} \ No newline at end of file diff --git a/src/Ultra.Tests/Ultra.Tests.csproj b/src/Ultra.Tests/Ultra.Tests.csproj index 58a0213..d255bb7 100644 --- a/src/Ultra.Tests/Ultra.Tests.csproj +++ b/src/Ultra.Tests/Ultra.Tests.csproj @@ -9,7 +9,9 @@ - + + + diff --git a/src/Ultra.Tests/Verified/FirefoxProfilerTests.TestSerialize.verified.txt b/src/Ultra.Tests/Verified/FirefoxProfilerTests.TestSerialize.verified.txt new file mode 100644 index 0000000..c7e5d8e --- /dev/null +++ b/src/Ultra.Tests/Verified/FirefoxProfilerTests.TestSerialize.verified.txt @@ -0,0 +1,468 @@ +{ + "meta": { + "interval": 1, + "startTime": 0, + "endTime": 2000, + "processType": 0, + "categories": [ + { + "name": "Kernel", + "color": "orange", + "subcategories": [] + }, + { + "name": "Native User", + "color": "blue", + "subcategories": [] + }, + { + "name": ".NET", + "color": "green", + "subcategories": [] + }, + { + "name": "GC", + "color": "yellow", + "subcategories": [] + }, + { + "name": "JIT", + "color": "purple", + "subcategories": [] + } + ], + "product": "myapp.exe", + "stackwalk": 1, + "version": 29, + "preprocessedProfileVersion": 50, + "oscpu": "X64", + "platform": "Microsoft Windows NT 10.0.22631.0", + "logicalCPUs": 32, + "symbolicated": true, + "markerSchema": [ + { + "name": "CC", + "tableLabel": "JIT Compile: {marker.data.fullName}, ILSize: {marker.data.methodILSize}", + "chartLabel": "JIT Compile: {marker.data.fullName}, ILSize: {marker.data.methodILSize}", + "display": [ + "timeline-overview", + "marker-chart", + "marker-table" + ], + "data": [ + { + "key": "fullName", + "label": "Full Name", + "format": "string" + }, + { + "key": "methodILSize", + "label": "Method IL Size", + "format": "integer" + } + ] + } + ], + "sampleUnits": { + "time": "ms", + "eventDelay": "ms", + "threadCPUDelta": "ns" + }, + "initialVisibleThreads": [ + 0 + ], + "initialSelectedThreads": [ + 0 + ] + }, + "libs": [ + { + "addressStart": 268435456, + "addressEnd": 536870912, + "addressOffset": 0, + "arch": "x86_64", + "name": "mylib", + "path": "/path/to/mylib", + "debugName": "mylib.pdb", + "debugPath": "/path/to/mylib.pdb", + "breakpadId": "1234567890" + } + ], + "threads": [ + { + "processType": "default", + "processStartupTime": 0, + "registerTime": 0, + "pausedRanges": [], + "name": "Main", + "isMainThread": false, + "pid": "My Process", + "tid": "125", + "samples": { + "threadCPUDelta": [ + 10000000, + 10000000, + 10000000, + 10000000 + ], + "stack": [ + 0, + 0, + 0, + 0 + ], + "timeDeltas": [ + 0, + 10, + 10, + 10 + ], + "weightType": "samples", + "length": 4 + }, + "markers": { + "data": [ + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + }, + { + "type": "CC", + "fullName": "World", + "methodILSize": 100 + } + ], + "name": [ + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 + ], + "startTime": [ + 0, + 2, + 4, + 6, + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38 + ], + "endTime": [ + 1, + 3, + 5, + 7, + 9, + 11, + 13, + 15, + 17, + 19, + 21, + 23, + 25, + 27, + 29, + 31, + 33, + 35, + 37, + 39 + ], + "phase": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "category": [ + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 + ], + "threadId": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "length": 20 + }, + "stackTable": { + "frame": [ + 0 + ], + "category": [ + 0 + ], + "subcategory": [ + 0 + ], + "prefix": [ + null + ], + "length": 1 + }, + "frameTable": { + "address": [ + 0 + ], + "inlineDepth": [ + 0 + ], + "category": [ + 0 + ], + "subcategory": [ + null + ], + "func": [ + 0 + ], + "nativeSymbol": [ + 0 + ], + "innerWindowID": [ + null + ], + "implementation": [ + null + ], + "line": [ + null + ], + "column": [ + null + ], + "length": 1 + }, + "stringArray": [ + "myfunction", + "myfunction (native symbols)", + "myfunction (resource)", + "Memory Size" + ], + "funcTable": { + "name": [ + 0 + ], + "isJS": [ + false + ], + "relevantForJS": [ + false + ], + "resource": [ + 0 + ], + "fileName": [], + "lineNumber": [ + null + ], + "columnNumber": [ + null + ], + "length": 1 + }, + "resourceTable": { + "lib": [ + 0 + ], + "name": [ + 2 + ], + "host": [ + null + ], + "type": [ + 1 + ], + "length": 1 + }, + "nativeSymbols": { + "libIndex": [ + 0 + ], + "address": [ + 22 + ], + "name": [ + 1 + ], + "functionSize": [ + null + ], + "length": 1 + } + } + ] +} \ No newline at end of file diff --git a/src/Ultra/Program.cs b/src/Ultra/Program.cs index ca6a51d..a0815c3 100644 --- a/src/Ultra/Program.cs +++ b/src/Ultra/Program.cs @@ -9,6 +9,9 @@ namespace Ultra; +/// +/// Main entry point for the ultra command line. +/// internal class Program { static async Task Main(string[] args) @@ -472,4 +475,4 @@ public void UpdateTable() } } } -} \ No newline at end of file +}