Add thread metrics and category support to Profiler and visualizer

This commit is contained in:
max
2026-08-05 11:47:21 +02:00
parent 2d4139fb2c
commit 83f77d1ebe
3 changed files with 350 additions and 18 deletions
+79 -5
View File
@@ -14,6 +14,7 @@ namespace Nerfed.Editor.Systems
private int selectedFrame = 0;
private int previousSelectedFrame = -1;
private IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)>> orderedCombinedData = null;
private IOrderedEnumerable<KeyValuePair<int, Profiler.RollingThreadMetrics>> orderedThreadRollingData = null;
private readonly ProfilerVisualizer.TimelineState timelineState = new ProfilerVisualizer.TimelineState();
private readonly List<Profiler.Frame> frameSnapshot = new List<Profiler.Frame>(256);
@@ -67,6 +68,22 @@ namespace Nerfed.Editor.Systems
timelineState.FollowLatest = true;
}
ImGui.SameLine();
int mode = (int)Profiler.Mode;
ImGui.SetNextItemWidth(130f);
if (ImGui.Combo("Mode", ref mode, "Instrumented\0Sampled\0"))
{
Profiler.Mode = (Profiler.CaptureMode)mode;
}
ImGui.SameLine();
int stride = Profiler.SamplingStride;
ImGui.SetNextItemWidth(90f);
if (ImGui.SliderInt("Stride", ref stride, 1, 64))
{
Profiler.SamplingStride = stride;
}
if (Profiler.IsRecording)
{
// Select last frame when recording to see latest frame data.
@@ -86,6 +103,7 @@ namespace Nerfed.Editor.Systems
double s = 1000;
ImGui.Text($"Frame: {frame.FrameCount} ({ms:0.000} ms | {(s / ms):0} fps)");
ImGui.Text($"Budget: {frame.BudgetMilliseconds:0.00} ms ({(frame.OverBudget ? "over" : "within")})");
ImGui.Text($"Thread Budget: {Profiler.ThreadBudgetMilliseconds:0.00} ms | Capture: {Profiler.Mode}");
ImGui.Text($"Alloc: {frame.AllocatedBytesDelta / 1024d:0.0} KB | GC: G0 {frame.Gen0CollectionsDelta}, G1 {frame.Gen1CollectionsDelta}, G2 {frame.Gen2CollectionsDelta}");
ImGui.EndChild();
@@ -102,8 +120,11 @@ namespace Nerfed.Editor.Systems
{
previousSelectedFrame = selectedFrame;
orderedCombinedData = CalculateCombinedData(frame);
orderedThreadRollingData = CalculateThreadRollingData();
}
DrawThreadRolling(orderedThreadRollingData);
DrawHierachy(frame);
ImGui.SameLine();
@@ -122,12 +143,14 @@ namespace Nerfed.Editor.Systems
ImGui.BeginChild("Hierachy", new Vector2(150, 0), ImGuiChildFlags.ResizeX);
if (ImGui.BeginTable("ProfilerData", 4, tableFlags, new Vector2(0, 0)))
if (ImGui.BeginTable("ProfilerData", 6, tableFlags, new Vector2(0, 0)))
{
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.6f, 0);
ImGui.TableSetupColumn("thread", ImGuiTableColumnFlags.WidthStretch, 0.15f, 1);
ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.15f, 1);
ImGui.TableSetupColumn("self", ImGuiTableColumnFlags.WidthStretch, 0.15f, 2);
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.40f, 0);
ImGui.TableSetupColumn("category", ImGuiTableColumnFlags.WidthStretch, 0.15f, 1);
ImGui.TableSetupColumn("tags", ImGuiTableColumnFlags.WidthStretch, 0.10f, 2);
ImGui.TableSetupColumn("thread", ImGuiTableColumnFlags.WidthStretch, 0.10f, 3);
ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.10f, 4);
ImGui.TableSetupColumn("self", ImGuiTableColumnFlags.WidthStretch, 0.10f, 5);
ImGui.TableSetupScrollFreeze(0, 1); // Make row always visible
ImGui.TableHeadersRow();
@@ -158,6 +181,10 @@ namespace Nerfed.Editor.Systems
isOpen = ImGui.TreeNodeEx(node.Label, treeNodeFlags);
}
ImGui.TableNextColumn();
ImGui.Text($"{node.Category}");
ImGui.TableNextColumn();
ImGui.Text($"0x{node.TagMask:X}");
ImGui.TableNextColumn();
ImGui.Text($"{node.ManagedThreadId}");
ImGui.TableNextColumn();
@@ -235,6 +262,53 @@ namespace Nerfed.Editor.Systems
return combinedRecordData.OrderByDescending(x => x.Value.ms);
}
private static IOrderedEnumerable<KeyValuePair<int, Profiler.RollingThreadMetrics>> CalculateThreadRollingData()
{
IReadOnlyDictionary<int, Profiler.RollingThreadMetrics> rollingData = Profiler.GetRollingThreadMetricsSnapshot();
return rollingData.OrderByDescending(x => x.Value.P95Ms);
}
private static void DrawThreadRolling(in IOrderedEnumerable<KeyValuePair<int, Profiler.RollingThreadMetrics>> orderedThreadRollingData)
{
if (orderedThreadRollingData == null)
{
return;
}
ImGui.BeginChild("ThreadRolling", new Vector2(0, 140), ImGuiChildFlags.Border);
if (ImGui.BeginTable("ProfilerThreadRollingData", 6, tableFlags, new Vector2(0, 0)))
{
ImGui.TableSetupColumn("thread", ImGuiTableColumnFlags.WidthStretch, 0.15f, 0);
ImGui.TableSetupColumn("avg", ImGuiTableColumnFlags.WidthStretch, 0.20f, 1);
ImGui.TableSetupColumn("p95", ImGuiTableColumnFlags.WidthStretch, 0.20f, 2);
ImGui.TableSetupColumn("max", ImGuiTableColumnFlags.WidthStretch, 0.20f, 3);
ImGui.TableSetupColumn("samples", ImGuiTableColumnFlags.WidthStretch, 0.15f, 4);
ImGui.TableSetupColumn("misses", ImGuiTableColumnFlags.WidthStretch, 0.15f, 5);
ImGui.TableHeadersRow();
foreach (KeyValuePair<int, Profiler.RollingThreadMetrics> metric in orderedThreadRollingData)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"T{metric.Key}");
ImGui.TableNextColumn();
ImGui.Text($"{metric.Value.AverageMs:0.000}");
ImGui.TableNextColumn();
ImGui.Text($"{metric.Value.P95Ms:0.000}");
ImGui.TableNextColumn();
ImGui.Text($"{metric.Value.MaxMs:0.000}");
ImGui.TableNextColumn();
ImGui.Text($"{metric.Value.Samples}");
ImGui.TableNextColumn();
ImGui.Text($"{metric.Value.BudgetMisses}");
}
ImGui.EndTable();
}
ImGui.EndChild();
}
private static ProfilerVisualizer.TimelineRenderResult DrawFlameGraph(IReadOnlyList<Profiler.Frame> frames, ProfilerVisualizer.TimelineState timelineState)
{
if (frames == null || frames.Count == 0)