diff --git a/src/Ultra.Core/EtwConverterToFirefox.cs b/src/Ultra.Core/EtwConverterToFirefox.cs index d9767e6..63b489d 100644 --- a/src/Ultra.Core/EtwConverterToFirefox.cs +++ b/src/Ultra.Core/EtwConverterToFirefox.cs @@ -281,6 +281,9 @@ private void ConvertProcess(TraceProcess process) var jitCompile = new JitCompileEvent { + MethodNamespace = methodJittingStarted.MethodNamespace, + MethodName = methodJittingStarted.MethodName, + MethodSignature = signature, FullName = $"{methodJittingStarted.MethodNamespace}.{methodJittingStarted.MethodName}{signature}", MethodILSize = methodJittingStarted.MethodILSize diff --git a/src/Ultra.Core/EtwUltraProfiler.cs b/src/Ultra.Core/EtwUltraProfiler.cs index 7265033..04f896c 100644 --- a/src/Ultra.Core/EtwUltraProfiler.cs +++ b/src/Ultra.Core/EtwUltraProfiler.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO.Compression; +using System.Text; using System.Text.Json; using ByteSizeLib; using Microsoft.Diagnostics.Tracing; @@ -392,13 +393,21 @@ public async Task Convert(string etlFile, List pIds, EtwUltraProfil var directory = Path.GetDirectoryName(etlFile); var etlFileNameWithoutExtension = Path.GetFileNameWithoutExtension(etlFile); - var jsonFinalFile = $"{ultraProfilerOptions.BaseOutputFileName ?? etlFileNameWithoutExtension}.json.gz"; + var baseFileName = $"{ultraProfilerOptions.BaseOutputFileName ?? etlFileNameWithoutExtension}"; + var jsonFinalFile = $"{baseFileName}.json.gz"; ultraProfilerOptions.LogProgress?.Invoke($"Converting to Firefox Profiler JSON"); await using var stream = File.Create(jsonFinalFile); await using var gzipStream = new GZipStream(stream, CompressionLevel.Optimal); await JsonSerializer.SerializeAsync(gzipStream, profile, FirefoxProfiler.JsonProfilerContext.Default.Profile); gzipStream.Flush(); + // Write the markdown report + { + using var mdStream = File.Create($"{baseFileName}_report.md"); + using var writer = new StreamWriter(mdStream, Encoding.UTF8); + MarkdownReportGenerator.Generate(profile, writer); + } + return jsonFinalFile; } diff --git a/src/Ultra.Core/MarkdownReportGenerator.cs b/src/Ultra.Core/MarkdownReportGenerator.cs new file mode 100644 index 0000000..d7dc512 --- /dev/null +++ b/src/Ultra.Core/MarkdownReportGenerator.cs @@ -0,0 +1,170 @@ +// 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.Markers; + +namespace Ultra.Core; + +/// +/// Generates a markdown report from a Firefox profile. +/// +internal sealed class MarkdownReportGenerator +{ + private readonly FirefoxProfiler.Profile _profile; + private readonly StreamWriter _writer; + + private MarkdownReportGenerator(FirefoxProfiler.Profile profile, StreamWriter writer) + { + _profile = profile; + _writer = writer; + } + + /// + /// Generates a markdown report from a Firefox profile. + /// + /// The Firefox profile. + /// The writer to write the markdown report. + public static void Generate(FirefoxProfiler.Profile profile, StreamWriter writer) + { + var generator = new MarkdownReportGenerator(profile, writer); + generator.Generate(); + } + + private void Generate() + { + var pidAndNameList = new HashSet(_profile.Threads.Select(x => new ProcessInfo(x.Pid, x.ProcessName))); + + _writer.WriteLine($"# Ultra Report for \\[{string.Join(", ", pidAndNameList.Select(x => x.Name))}]"); + _writer.WriteLine(); + _writer.WriteLine($"_Generated on {DateTime.Now:s}_"); + _writer.WriteLine(); + + foreach (var pidAndName in pidAndNameList) + { + var threads = _profile.Threads.Where(x => string.Equals(x.Pid, pidAndName.Pid, StringComparison.OrdinalIgnoreCase)).ToList(); + GenerateProcess(pidAndName, threads); + } + } + + private void GenerateProcess(ProcessInfo processInfo, List threads) + { + _writer.WriteLine($"## Process {processInfo.Name}"); + _writer.WriteLine(); + + GenerateJit(threads); + + _writer.WriteLine(); + _writer.WriteLine("_Report generated by [ultra](https://github.com/xoofx/ultra)_"); + } + + private void GenerateJit(List threads) + { + var jitEvents = CollectMarkersFromThreads(threads, EtwConverterToFirefox.CategoryJit); + + if (jitEvents.Count == 0) + { + return; + } + + double totalTime = 0.0; + long totalILSize = 0; + + Dictionary namespaceStats = new(StringComparer.Ordinal); + + // Sort by duration descending + jitEvents.Sort((left, right) => right.DurationInMs.CompareTo(left.DurationInMs)); + + foreach (var jitEvent in jitEvents) + { + totalTime += jitEvent.DurationInMs; + totalILSize += jitEvent.Data.MethodILSize; + + var ns = GetNamespace(jitEvent.Data.MethodNamespace); + var indexOfLastDot = ns.LastIndexOf('.'); + ns = indexOfLastDot > 0 ? ns.Substring(0, indexOfLastDot) : ""; + + if (!namespaceStats.TryGetValue(ns, out var stats)) + { + stats = (0, 0, 0); + } + + stats.DurationInMs += jitEvent.DurationInMs; + stats.ILSize += jitEvent.Data.MethodILSize; + stats.MethodCount++; + + namespaceStats[ns] = stats; + } + + _writer.WriteLine("### JIT Statistics"); + _writer.WriteLine(); + + _writer.WriteLine($"- Total JIT time: `{totalTime:0.0}ms`"); + _writer.WriteLine($"- Total JIT IL size: `{totalILSize}`"); + + _writer.WriteLine(); + _writer.WriteLine("#### JIT Top 10 Namespaces"); + _writer.WriteLine(); + + _writer.WriteLine("| Namespace | Duration (ms) | IL Size| Methods |"); + _writer.WriteLine("|-----------|---------------|--------|-------"); + var cumulativeTotalTime = 0.0; + foreach (var (namespaceName, stats) in namespaceStats.OrderByDescending(x => x.Value.DurationInMs)) + { + _writer.WriteLine($"| ``{namespaceName}`` | `{stats.DurationInMs:0.0}` | `{stats.ILSize}` |`{stats.MethodCount}` |"); + cumulativeTotalTime += stats.DurationInMs; + if (cumulativeTotalTime > totalTime * 0.9) + { + break; + } + } + + // TODO: Add a report for Generic Namespace arguments to namespace (e.g ``System.Collections.Generic.List`1[MyNamespace.MyClass...]`) + // MyNamespace.MyClass should be reported as a separate namespace that contributes to System.Collections + + _writer.WriteLine(); + _writer.WriteLine("#### JIT Top 10 Methods"); + _writer.WriteLine(); + _writer.WriteLine("| Method | Duration (ms) | IL Size"); + _writer.WriteLine("|--------|---------------|--------|"); + foreach (var jitEvent in jitEvents.Take(10)) + { + _writer.WriteLine($"| ``{jitEvent.Data.FullName}`` | `{jitEvent.DurationInMs:0.0}` | `{jitEvent.Data.MethodILSize}` |"); + } + } + + private static List> CollectMarkersFromThreads(List threads, int category) where TPayload: FirefoxProfiler.MarkerPayload + { + var markers = new List>(); + foreach (var thread in threads) + { + var threadMarkers = thread.Markers; + var markerLength = threadMarkers.Length; + for (int i = 0; i < markerLength; i++) + { + if (threadMarkers.Category[i] == category) + { + var payload = (TPayload) threadMarkers.Data[i]!; + var duration = threadMarkers.EndTime[i]!.Value - threadMarkers.StartTime[i]!.Value; + markers.Add(new(payload, duration)); + } + } + } + return markers; + } + + private static string GetNamespace(string fullTypeName) + { + var index = fullTypeName.IndexOf('`'); // For generics + if (index > 0) + { + fullTypeName = fullTypeName.Substring(0, index); + } + index = fullTypeName.LastIndexOf('.'); + return index > 0 ? fullTypeName.Substring(0, index) : ""; + } + + private record struct ProcessInfo(string Pid, string? Name); + + private record struct PayloadEvent(TPayload Data, double DurationInMs) where TPayload : FirefoxProfiler.MarkerPayload; +} \ No newline at end of file diff --git a/src/Ultra.Core/Markers/JitCompileEvent.cs b/src/Ultra.Core/Markers/JitCompileEvent.cs index 0d7d5af..9812554 100644 --- a/src/Ultra.Core/Markers/JitCompileEvent.cs +++ b/src/Ultra.Core/Markers/JitCompileEvent.cs @@ -23,8 +23,26 @@ public JitCompileEvent() { Type = TypeId; FullName = string.Empty; + MethodNamespace = string.Empty; + MethodName = string.Empty; + MethodSignature = string.Empty; } + /// + /// Gets or sets the namespace of the method. (Not serialized to JSON) + /// + public string MethodNamespace { get; set; } + + /// + /// Gets or sets the name of the method. (Not serialized to JSON) + /// + public string MethodName { get; set; } + + /// + /// Gets or sets the signature of the method. (Not serialized to JSON) + /// + public string MethodSignature { get; set; } + /// /// Gets or sets the full name of the method. /// diff --git a/src/ultra.sln.DotSettings b/src/ultra.sln.DotSettings index 3a93f7c..934192e 100644 --- a/src/ultra.sln.DotSettings +++ b/src/ultra.sln.DotSettings @@ -1,2 +1,3 @@  + IL True \ No newline at end of file