Skip to content

Commit

Permalink
Add reporting for JIT
Browse files Browse the repository at this point in the history
  • Loading branch information
xoofx committed Dec 11, 2024
1 parent 439b76d commit af4d384
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/Ultra.Core/EtwConverterToFirefox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion src/Ultra.Core/EtwUltraProfiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Diagnostics;
using System.IO.Compression;
using System.Text;
using System.Text.Json;
using ByteSizeLib;
using Microsoft.Diagnostics.Tracing;
Expand Down Expand Up @@ -392,13 +393,21 @@ public async Task<string> Convert(string etlFile, List<int> 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;
}

Expand Down
170 changes: 170 additions & 0 deletions src/Ultra.Core/MarkdownReportGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Generates a markdown report from a Firefox profile.
/// </summary>
internal sealed class MarkdownReportGenerator
{
private readonly FirefoxProfiler.Profile _profile;
private readonly StreamWriter _writer;

private MarkdownReportGenerator(FirefoxProfiler.Profile profile, StreamWriter writer)
{
_profile = profile;
_writer = writer;
}

/// <summary>
/// Generates a markdown report from a Firefox profile.
/// </summary>
/// <param name="profile">The Firefox profile.</param>
/// <param name="writer">The writer to write the markdown report.</param>
public static void Generate(FirefoxProfiler.Profile profile, StreamWriter writer)
{
var generator = new MarkdownReportGenerator(profile, writer);
generator.Generate();
}

private void Generate()
{
var pidAndNameList = new HashSet<ProcessInfo>(_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<FirefoxProfiler.Thread> 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<FirefoxProfiler.Thread> threads)
{
var jitEvents = CollectMarkersFromThreads<JitCompileEvent>(threads, EtwConverterToFirefox.CategoryJit);

if (jitEvents.Count == 0)
{
return;
}

double totalTime = 0.0;
long totalILSize = 0;

Dictionary<string, (double DurationInMs, long ILSize, int MethodCount)> 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) : "<no namespace>";

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<PayloadEvent<TPayload>> CollectMarkersFromThreads<TPayload>(List<FirefoxProfiler.Thread> threads, int category) where TPayload: FirefoxProfiler.MarkerPayload
{
var markers = new List<PayloadEvent<TPayload>>();
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) : "<no namespace>";
}

private record struct ProcessInfo(string Pid, string? Name);

private record struct PayloadEvent<TPayload>(TPayload Data, double DurationInMs) where TPayload : FirefoxProfiler.MarkerPayload;
}
18 changes: 18 additions & 0 deletions src/Ultra.Core/Markers/JitCompileEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,26 @@ public JitCompileEvent()
{
Type = TypeId;
FullName = string.Empty;
MethodNamespace = string.Empty;
MethodName = string.Empty;
MethodSignature = string.Empty;
}

/// <summary>
/// Gets or sets the namespace of the method. (Not serialized to JSON)
/// </summary>
public string MethodNamespace { get; set; }

/// <summary>
/// Gets or sets the name of the method. (Not serialized to JSON)
/// </summary>
public string MethodName { get; set; }

/// <summary>
/// Gets or sets the signature of the method. (Not serialized to JSON)
/// </summary>
public string MethodSignature { get; set; }

/// <summary>
/// Gets or sets the full name of the method.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/ultra.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
<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">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IL/@EntryIndexedValue">IL</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Markdig/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

0 comments on commit af4d384

Please sign in to comment.