Enhance Profiler with Label Metrics and Rolling Windows
- Introduced LabelMetrics and RollingLabelMetrics structs to capture detailed profiling data. - Implemented a RollingWindow class to maintain a rolling average of metrics. - Updated Frame class to collect and store label metrics, including memory allocation and garbage collection statistics. - Enhanced ScopeNode class to support self-time calculations and child duration tracking. - Improved the Profiler class to manage frame budgets and rolling metrics. - Refactored the ProfilerVisualizer to support new timeline rendering features, including zoom and pan functionality. - Added tooltip support for detailed node information in the flame graph. - Enhanced BoundedQueue to be thread-safe with proper locking mechanisms for concurrent access.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using ImGuiNET;
|
||||
using MoonTools.ECS;
|
||||
using Nerfed.Runtime;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Nerfed.Editor.Systems
|
||||
{
|
||||
@@ -12,7 +13,9 @@ namespace Nerfed.Editor.Systems
|
||||
|
||||
private int selectedFrame = 0;
|
||||
private int previousSelectedFrame = -1;
|
||||
private IOrderedEnumerable<KeyValuePair<string, (double ms, uint calls)>> orderedCombinedData = null;
|
||||
private IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)>> orderedCombinedData = null;
|
||||
private readonly ProfilerVisualizer.TimelineState timelineState = new ProfilerVisualizer.TimelineState();
|
||||
private readonly List<Profiler.Frame> frameSnapshot = new List<Profiler.Frame>(256);
|
||||
|
||||
public EditorProfilerWindow(World world) : base(world)
|
||||
{
|
||||
@@ -25,49 +28,88 @@ namespace Nerfed.Editor.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
if (Profiler.CopyFramesTo(frameSnapshot) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFrame = Math.Clamp(selectedFrame, 0, frameSnapshot.Count - 1);
|
||||
timelineState.SelectedFrameIndex = Math.Clamp(timelineState.SelectedFrameIndex, -1, frameSnapshot.Count - 1);
|
||||
timelineState.VisibleFrameCount = Math.Clamp(timelineState.VisibleFrameCount, 1, frameSnapshot.Count);
|
||||
|
||||
ImGui.Begin("Profiler");
|
||||
|
||||
ImGui.BeginChild("Toolbar", new System.Numerics.Vector2(0, 0), ImGuiChildFlags.AutoResizeY);
|
||||
ImGui.BeginChild("Toolbar", new Vector2(0, 0), ImGuiChildFlags.AutoResizeY);
|
||||
if (ImGui.RadioButton("Recording", Profiler.IsRecording))
|
||||
{
|
||||
Profiler.SetActive(!Profiler.IsRecording);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.Text("Follow");
|
||||
ImGui.SameLine();
|
||||
ImGui.Checkbox("##follow-timeline", ref timelineState.FollowLatest);
|
||||
ImGui.SameLine();
|
||||
|
||||
int visibleFrameCount = timelineState.VisibleFrameCount;
|
||||
ImGui.SetNextItemWidth(130f);
|
||||
if (ImGui.SliderInt("Window", ref visibleFrameCount, 1, frameSnapshot.Count))
|
||||
{
|
||||
timelineState.VisibleFrameCount = visibleFrameCount;
|
||||
timelineState.FollowLatest = false;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Reset Zoom"))
|
||||
{
|
||||
timelineState.Zoom = 1f;
|
||||
timelineState.PanTicks = 0d;
|
||||
timelineState.FollowLatest = true;
|
||||
}
|
||||
|
||||
if (Profiler.IsRecording)
|
||||
{
|
||||
// Select last frame when recording to see latest frame data.
|
||||
selectedFrame = Profiler.Frames.Count - 1;
|
||||
}
|
||||
if (ImGui.SliderInt(string.Empty, ref selectedFrame, 0, Profiler.Frames.Count - 1))
|
||||
{
|
||||
// Stop recording when browsing frames.
|
||||
Profiler.SetActive(false);
|
||||
selectedFrame = frameSnapshot.Count - 1;
|
||||
}
|
||||
|
||||
Profiler.Frame frame = Profiler.Frames.ElementAt(selectedFrame);
|
||||
int sliderFrame = selectedFrame;
|
||||
if (ImGui.SliderInt("Frame", ref sliderFrame, 0, frameSnapshot.Count - 1))
|
||||
{
|
||||
selectedFrame = sliderFrame;
|
||||
timelineState.SelectedFrameIndex = selectedFrame;
|
||||
timelineState.FollowLatest = false;
|
||||
}
|
||||
|
||||
Profiler.Frame frame = frameSnapshot[selectedFrame];
|
||||
double ms = frame.ElapsedMilliseconds();
|
||||
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($"Alloc: {frame.AllocatedBytesDelta / 1024d:0.0} KB | GC: G0 {frame.Gen0CollectionsDelta}, G1 {frame.Gen1CollectionsDelta}, G2 {frame.Gen2CollectionsDelta}");
|
||||
ImGui.EndChild();
|
||||
|
||||
if (!Profiler.IsRecording) {
|
||||
if (previousSelectedFrame != selectedFrame)
|
||||
{
|
||||
previousSelectedFrame = selectedFrame;
|
||||
orderedCombinedData = CalculateCombinedData(frame);
|
||||
}
|
||||
|
||||
DrawFlameGraph(frame);
|
||||
|
||||
DrawHierachy(frame);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
DrawCombined(orderedCombinedData);
|
||||
ProfilerVisualizer.TimelineRenderResult timelineResult = DrawFlameGraph(frameSnapshot, timelineState);
|
||||
if (timelineResult.SelectionChanged)
|
||||
{
|
||||
selectedFrame = timelineResult.SelectedFrameIndex;
|
||||
}
|
||||
|
||||
selectedFrame = Math.Clamp(selectedFrame, 0, frameSnapshot.Count - 1);
|
||||
frame = frameSnapshot[selectedFrame];
|
||||
|
||||
if (previousSelectedFrame != selectedFrame)
|
||||
{
|
||||
previousSelectedFrame = selectedFrame;
|
||||
orderedCombinedData = CalculateCombinedData(frame);
|
||||
}
|
||||
|
||||
DrawHierachy(frame);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
DrawCombined(orderedCombinedData);
|
||||
|
||||
ImGui.End();
|
||||
}
|
||||
|
||||
@@ -78,13 +120,14 @@ namespace Nerfed.Editor.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.BeginChild("Hierachy", new System.Numerics.Vector2(150, 0), ImGuiChildFlags.ResizeX);
|
||||
ImGui.BeginChild("Hierachy", new Vector2(150, 0), ImGuiChildFlags.ResizeX);
|
||||
|
||||
if (ImGui.BeginTable("ProfilerData", 3, tableFlags, new System.Numerics.Vector2(0, 0)))
|
||||
if (ImGui.BeginTable("ProfilerData", 4, tableFlags, new Vector2(0, 0)))
|
||||
{
|
||||
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.8f, 0);
|
||||
ImGui.TableSetupColumn("thread", ImGuiTableColumnFlags.WidthStretch, 0.2f, 1);
|
||||
ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.2f, 1);
|
||||
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.TableSetupScrollFreeze(0, 1); // Make row always visible
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
@@ -119,6 +162,8 @@ namespace Nerfed.Editor.Systems
|
||||
ImGui.Text($"{node.ManagedThreadId}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text($"{node.ElapsedMilliseconds():0.000}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text($"{node.SelfMilliseconds():0.000}");
|
||||
|
||||
if (isOpen)
|
||||
{
|
||||
@@ -130,24 +175,27 @@ namespace Nerfed.Editor.Systems
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawCombined(in IOrderedEnumerable<KeyValuePair<string, (double ms, uint calls)>> orderedCombinedData)
|
||||
private static void DrawCombined(in IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)>> orderedCombinedData)
|
||||
{
|
||||
if(orderedCombinedData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.BeginChild("Combined", new System.Numerics.Vector2(0, 0));
|
||||
ImGui.BeginChild("Combined", new Vector2(0, 0));
|
||||
|
||||
if (ImGui.BeginTable("ProfilerCombinedData", 3, tableFlags, new System.Numerics.Vector2(0, 0)))
|
||||
if (ImGui.BeginTable("ProfilerCombinedData", 6, tableFlags, new Vector2(0, 0)))
|
||||
{
|
||||
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.6f, 0);
|
||||
ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.2f, 1);
|
||||
ImGui.TableSetupColumn("calls", ImGuiTableColumnFlags.WidthStretch, 0.2f, 2);
|
||||
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.45f, 0);
|
||||
ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.14f, 1);
|
||||
ImGui.TableSetupColumn("self", ImGuiTableColumnFlags.WidthStretch, 0.14f, 2);
|
||||
ImGui.TableSetupColumn("calls", ImGuiTableColumnFlags.WidthStretch, 0.10f, 3);
|
||||
ImGui.TableSetupColumn("avg", ImGuiTableColumnFlags.WidthStretch, 0.10f, 4);
|
||||
ImGui.TableSetupColumn("p95", ImGuiTableColumnFlags.WidthStretch, 0.10f, 5);
|
||||
ImGui.TableSetupScrollFreeze(0, 1); // Make row always visible
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
foreach (KeyValuePair<string, (double ms, uint calls)> combinedData in orderedCombinedData)
|
||||
foreach (KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)> combinedData in orderedCombinedData)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
@@ -155,7 +203,13 @@ namespace Nerfed.Editor.Systems
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text($"{combinedData.Value.ms:0.000}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text($"{combinedData.Value.selfMs:0.000}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text($"{combinedData.Value.calls}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text($"{combinedData.Value.avgMs:0.000}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text($"{combinedData.Value.p95Ms:0.000}");
|
||||
}
|
||||
|
||||
ImGui.EndTable();
|
||||
@@ -164,41 +218,31 @@ namespace Nerfed.Editor.Systems
|
||||
ImGui.EndChild();
|
||||
}
|
||||
|
||||
private static IOrderedEnumerable<KeyValuePair<string, (double ms, uint calls)>> CalculateCombinedData(Profiler.Frame frame)
|
||||
private static IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)>> CalculateCombinedData(Profiler.Frame frame)
|
||||
{
|
||||
Dictionary<string, (double ms, uint calls)> combinedRecordData = new Dictionary<string, (double ms, uint calls)>(128);
|
||||
foreach (Profiler.ScopeNode node in frame.RootNodes)
|
||||
IReadOnlyDictionary<string, Profiler.RollingLabelMetrics> rollingData = Profiler.GetRollingLabelMetricsSnapshot();
|
||||
Dictionary<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)> combinedRecordData = new Dictionary<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)>(128);
|
||||
foreach (KeyValuePair<string, Profiler.LabelMetrics> metric in frame.LabelMetrics)
|
||||
{
|
||||
CalculateCombinedData(node, in combinedRecordData);
|
||||
Profiler.RollingLabelMetrics rolling = default;
|
||||
if (rollingData.TryGetValue(metric.Key, out Profiler.RollingLabelMetrics found))
|
||||
{
|
||||
rolling = found;
|
||||
}
|
||||
|
||||
combinedRecordData[metric.Key] = (metric.Value.InclusiveMs, metric.Value.SelfMs, metric.Value.Calls, rolling.AverageMs, rolling.P95Ms);
|
||||
}
|
||||
return combinedRecordData.OrderByDescending(x => x.Value.ms);
|
||||
}
|
||||
|
||||
private static void CalculateCombinedData(Profiler.ScopeNode node, in Dictionary<string, (double ms, uint calls)> combinedRecordData)
|
||||
private static ProfilerVisualizer.TimelineRenderResult DrawFlameGraph(IReadOnlyList<Profiler.Frame> frames, ProfilerVisualizer.TimelineState timelineState)
|
||||
{
|
||||
if (combinedRecordData.TryGetValue(node.Label, out (double ms, uint calls) combined))
|
||||
if (frames == null || frames.Count == 0)
|
||||
{
|
||||
combinedRecordData[node.Label] = (combined.ms + node.ElapsedMilliseconds(), combined.calls + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedRecordData.Add(node.Label, (node.ElapsedMilliseconds(), 1));
|
||||
return default;
|
||||
}
|
||||
|
||||
for (int i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
CalculateCombinedData(node.Children[i], combinedRecordData);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawFlameGraph(Profiler.Frame frame)
|
||||
{
|
||||
if (frame == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ProfilerVisualizer.RenderFlameGraph(frame);
|
||||
return ProfilerVisualizer.RenderTimeline(frames, timelineState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user