Skip to content

Commit af4d384

Browse files
committed
Add reporting for JIT
1 parent 439b76d commit af4d384

5 files changed

+202
-1
lines changed

src/Ultra.Core/EtwConverterToFirefox.cs

+3
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ private void ConvertProcess(TraceProcess process)
281281

282282
var jitCompile = new JitCompileEvent
283283
{
284+
MethodNamespace = methodJittingStarted.MethodNamespace,
285+
MethodName = methodJittingStarted.MethodName,
286+
MethodSignature = signature,
284287
FullName =
285288
$"{methodJittingStarted.MethodNamespace}.{methodJittingStarted.MethodName}{signature}",
286289
MethodILSize = methodJittingStarted.MethodILSize

src/Ultra.Core/EtwUltraProfiler.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Diagnostics;
66
using System.IO.Compression;
7+
using System.Text;
78
using System.Text.Json;
89
using ByteSizeLib;
910
using Microsoft.Diagnostics.Tracing;
@@ -392,13 +393,21 @@ public async Task<string> Convert(string etlFile, List<int> pIds, EtwUltraProfil
392393

393394
var directory = Path.GetDirectoryName(etlFile);
394395
var etlFileNameWithoutExtension = Path.GetFileNameWithoutExtension(etlFile);
395-
var jsonFinalFile = $"{ultraProfilerOptions.BaseOutputFileName ?? etlFileNameWithoutExtension}.json.gz";
396+
var baseFileName = $"{ultraProfilerOptions.BaseOutputFileName ?? etlFileNameWithoutExtension}";
397+
var jsonFinalFile = $"{baseFileName}.json.gz";
396398
ultraProfilerOptions.LogProgress?.Invoke($"Converting to Firefox Profiler JSON");
397399
await using var stream = File.Create(jsonFinalFile);
398400
await using var gzipStream = new GZipStream(stream, CompressionLevel.Optimal);
399401
await JsonSerializer.SerializeAsync(gzipStream, profile, FirefoxProfiler.JsonProfilerContext.Default.Profile);
400402
gzipStream.Flush();
401403

404+
// Write the markdown report
405+
{
406+
using var mdStream = File.Create($"{baseFileName}_report.md");
407+
using var writer = new StreamWriter(mdStream, Encoding.UTF8);
408+
MarkdownReportGenerator.Generate(profile, writer);
409+
}
410+
402411
return jsonFinalFile;
403412
}
404413

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// Licensed under the BSD-Clause 2 license.
3+
// See license.txt file in the project root for full license information.
4+
5+
using Ultra.Core.Markers;
6+
7+
namespace Ultra.Core;
8+
9+
/// <summary>
10+
/// Generates a markdown report from a Firefox profile.
11+
/// </summary>
12+
internal sealed class MarkdownReportGenerator
13+
{
14+
private readonly FirefoxProfiler.Profile _profile;
15+
private readonly StreamWriter _writer;
16+
17+
private MarkdownReportGenerator(FirefoxProfiler.Profile profile, StreamWriter writer)
18+
{
19+
_profile = profile;
20+
_writer = writer;
21+
}
22+
23+
/// <summary>
24+
/// Generates a markdown report from a Firefox profile.
25+
/// </summary>
26+
/// <param name="profile">The Firefox profile.</param>
27+
/// <param name="writer">The writer to write the markdown report.</param>
28+
public static void Generate(FirefoxProfiler.Profile profile, StreamWriter writer)
29+
{
30+
var generator = new MarkdownReportGenerator(profile, writer);
31+
generator.Generate();
32+
}
33+
34+
private void Generate()
35+
{
36+
var pidAndNameList = new HashSet<ProcessInfo>(_profile.Threads.Select(x => new ProcessInfo(x.Pid, x.ProcessName)));
37+
38+
_writer.WriteLine($"# Ultra Report for \\[{string.Join(", ", pidAndNameList.Select(x => x.Name))}]");
39+
_writer.WriteLine();
40+
_writer.WriteLine($"_Generated on {DateTime.Now:s}_");
41+
_writer.WriteLine();
42+
43+
foreach (var pidAndName in pidAndNameList)
44+
{
45+
var threads = _profile.Threads.Where(x => string.Equals(x.Pid, pidAndName.Pid, StringComparison.OrdinalIgnoreCase)).ToList();
46+
GenerateProcess(pidAndName, threads);
47+
}
48+
}
49+
50+
private void GenerateProcess(ProcessInfo processInfo, List<FirefoxProfiler.Thread> threads)
51+
{
52+
_writer.WriteLine($"## Process {processInfo.Name}");
53+
_writer.WriteLine();
54+
55+
GenerateJit(threads);
56+
57+
_writer.WriteLine();
58+
_writer.WriteLine("_Report generated by [ultra](https://github.com/xoofx/ultra)_");
59+
}
60+
61+
private void GenerateJit(List<FirefoxProfiler.Thread> threads)
62+
{
63+
var jitEvents = CollectMarkersFromThreads<JitCompileEvent>(threads, EtwConverterToFirefox.CategoryJit);
64+
65+
if (jitEvents.Count == 0)
66+
{
67+
return;
68+
}
69+
70+
double totalTime = 0.0;
71+
long totalILSize = 0;
72+
73+
Dictionary<string, (double DurationInMs, long ILSize, int MethodCount)> namespaceStats = new(StringComparer.Ordinal);
74+
75+
// Sort by duration descending
76+
jitEvents.Sort((left, right) => right.DurationInMs.CompareTo(left.DurationInMs));
77+
78+
foreach (var jitEvent in jitEvents)
79+
{
80+
totalTime += jitEvent.DurationInMs;
81+
totalILSize += jitEvent.Data.MethodILSize;
82+
83+
var ns = GetNamespace(jitEvent.Data.MethodNamespace);
84+
var indexOfLastDot = ns.LastIndexOf('.');
85+
ns = indexOfLastDot > 0 ? ns.Substring(0, indexOfLastDot) : "<no namespace>";
86+
87+
if (!namespaceStats.TryGetValue(ns, out var stats))
88+
{
89+
stats = (0, 0, 0);
90+
}
91+
92+
stats.DurationInMs += jitEvent.DurationInMs;
93+
stats.ILSize += jitEvent.Data.MethodILSize;
94+
stats.MethodCount++;
95+
96+
namespaceStats[ns] = stats;
97+
}
98+
99+
_writer.WriteLine("### JIT Statistics");
100+
_writer.WriteLine();
101+
102+
_writer.WriteLine($"- Total JIT time: `{totalTime:0.0}ms`");
103+
_writer.WriteLine($"- Total JIT IL size: `{totalILSize}`");
104+
105+
_writer.WriteLine();
106+
_writer.WriteLine("#### JIT Top 10 Namespaces");
107+
_writer.WriteLine();
108+
109+
_writer.WriteLine("| Namespace | Duration (ms) | IL Size| Methods |");
110+
_writer.WriteLine("|-----------|---------------|--------|-------");
111+
var cumulativeTotalTime = 0.0;
112+
foreach (var (namespaceName, stats) in namespaceStats.OrderByDescending(x => x.Value.DurationInMs))
113+
{
114+
_writer.WriteLine($"| ``{namespaceName}`` | `{stats.DurationInMs:0.0}` | `{stats.ILSize}` |`{stats.MethodCount}` |");
115+
cumulativeTotalTime += stats.DurationInMs;
116+
if (cumulativeTotalTime > totalTime * 0.9)
117+
{
118+
break;
119+
}
120+
}
121+
122+
// TODO: Add a report for Generic Namespace arguments to namespace (e.g ``System.Collections.Generic.List`1[MyNamespace.MyClass...]`)
123+
// MyNamespace.MyClass should be reported as a separate namespace that contributes to System.Collections
124+
125+
_writer.WriteLine();
126+
_writer.WriteLine("#### JIT Top 10 Methods");
127+
_writer.WriteLine();
128+
_writer.WriteLine("| Method | Duration (ms) | IL Size");
129+
_writer.WriteLine("|--------|---------------|--------|");
130+
foreach (var jitEvent in jitEvents.Take(10))
131+
{
132+
_writer.WriteLine($"| ``{jitEvent.Data.FullName}`` | `{jitEvent.DurationInMs:0.0}` | `{jitEvent.Data.MethodILSize}` |");
133+
}
134+
}
135+
136+
private static List<PayloadEvent<TPayload>> CollectMarkersFromThreads<TPayload>(List<FirefoxProfiler.Thread> threads, int category) where TPayload: FirefoxProfiler.MarkerPayload
137+
{
138+
var markers = new List<PayloadEvent<TPayload>>();
139+
foreach (var thread in threads)
140+
{
141+
var threadMarkers = thread.Markers;
142+
var markerLength = threadMarkers.Length;
143+
for (int i = 0; i < markerLength; i++)
144+
{
145+
if (threadMarkers.Category[i] == category)
146+
{
147+
var payload = (TPayload) threadMarkers.Data[i]!;
148+
var duration = threadMarkers.EndTime[i]!.Value - threadMarkers.StartTime[i]!.Value;
149+
markers.Add(new(payload, duration));
150+
}
151+
}
152+
}
153+
return markers;
154+
}
155+
156+
private static string GetNamespace(string fullTypeName)
157+
{
158+
var index = fullTypeName.IndexOf('`'); // For generics
159+
if (index > 0)
160+
{
161+
fullTypeName = fullTypeName.Substring(0, index);
162+
}
163+
index = fullTypeName.LastIndexOf('.');
164+
return index > 0 ? fullTypeName.Substring(0, index) : "<no namespace>";
165+
}
166+
167+
private record struct ProcessInfo(string Pid, string? Name);
168+
169+
private record struct PayloadEvent<TPayload>(TPayload Data, double DurationInMs) where TPayload : FirefoxProfiler.MarkerPayload;
170+
}

src/Ultra.Core/Markers/JitCompileEvent.cs

+18
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,26 @@ public JitCompileEvent()
2323
{
2424
Type = TypeId;
2525
FullName = string.Empty;
26+
MethodNamespace = string.Empty;
27+
MethodName = string.Empty;
28+
MethodSignature = string.Empty;
2629
}
2730

31+
/// <summary>
32+
/// Gets or sets the namespace of the method. (Not serialized to JSON)
33+
/// </summary>
34+
public string MethodNamespace { get; set; }
35+
36+
/// <summary>
37+
/// Gets or sets the name of the method. (Not serialized to JSON)
38+
/// </summary>
39+
public string MethodName { get; set; }
40+
41+
/// <summary>
42+
/// Gets or sets the signature of the method. (Not serialized to JSON)
43+
/// </summary>
44+
public string MethodSignature { get; set; }
45+
2846
/// <summary>
2947
/// Gets or sets the full name of the method.
3048
/// </summary>

src/ultra.sln.DotSettings

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IL/@EntryIndexedValue">IL</s:String>
23
<s:Boolean x:Key="/Default/UserDictionary/Words/=Markdig/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

0 commit comments

Comments
 (0)