Enhance Profiler with memory allocation metrics

This commit is contained in:
max
2026-08-05 13:25:14 +02:00
parent 7853a768de
commit d9582aecdd
2 changed files with 58 additions and 23 deletions
+30 -18
View File
@@ -13,7 +13,7 @@ namespace Nerfed.Editor.Systems
private int selectedFrame = 0; private int selectedFrame = 0;
private int previousSelectedFrame = -1; private int previousSelectedFrame = -1;
private IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)>> orderedCombinedData = null; private IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)>> orderedCombinedData = null;
private IOrderedEnumerable<KeyValuePair<int, Profiler.RollingThreadMetrics>> orderedThreadRollingData = null; private IOrderedEnumerable<KeyValuePair<int, Profiler.RollingThreadMetrics>> orderedThreadRollingData = null;
private readonly ProfilerVisualizer.TimelineState timelineState = new ProfilerVisualizer.TimelineState(); private readonly ProfilerVisualizer.TimelineState timelineState = new ProfilerVisualizer.TimelineState();
private readonly List<Profiler.Frame> frameSnapshot = new List<Profiler.Frame>(256); private readonly List<Profiler.Frame> frameSnapshot = new List<Profiler.Frame>(256);
@@ -143,14 +143,16 @@ namespace Nerfed.Editor.Systems
ImGui.BeginChild("Hierachy", new Vector2(150, 0), ImGuiChildFlags.ResizeX); ImGui.BeginChild("Hierachy", new Vector2(150, 0), ImGuiChildFlags.ResizeX);
if (ImGui.BeginTable("ProfilerData", 6, tableFlags, new Vector2(0, 0))) if (ImGui.BeginTable("ProfilerData", 8, tableFlags, new Vector2(0, 0)))
{ {
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.40f, 0); ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.30f, 0);
ImGui.TableSetupColumn("category", ImGuiTableColumnFlags.WidthStretch, 0.15f, 1); ImGui.TableSetupColumn("category", ImGuiTableColumnFlags.WidthStretch, 0.12f, 1);
ImGui.TableSetupColumn("tags", ImGuiTableColumnFlags.WidthStretch, 0.10f, 2); ImGui.TableSetupColumn("tags", ImGuiTableColumnFlags.WidthStretch, 0.08f, 2);
ImGui.TableSetupColumn("thread", ImGuiTableColumnFlags.WidthStretch, 0.10f, 3); ImGui.TableSetupColumn("thread", ImGuiTableColumnFlags.WidthStretch, 0.08f, 3);
ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.10f, 4); ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.10f, 4);
ImGui.TableSetupColumn("self", ImGuiTableColumnFlags.WidthStretch, 0.10f, 5); ImGui.TableSetupColumn("self", ImGuiTableColumnFlags.WidthStretch, 0.10f, 5);
ImGui.TableSetupColumn("alloc(B)", ImGuiTableColumnFlags.WidthStretch, 0.11f, 6);
ImGui.TableSetupColumn("self alloc", ImGuiTableColumnFlags.WidthStretch, 0.11f, 7);
ImGui.TableSetupScrollFreeze(0, 1); // Make row always visible ImGui.TableSetupScrollFreeze(0, 1); // Make row always visible
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
@@ -191,6 +193,10 @@ namespace Nerfed.Editor.Systems
ImGui.Text($"{node.ElapsedMilliseconds():0.000}"); ImGui.Text($"{node.ElapsedMilliseconds():0.000}");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text($"{node.SelfMilliseconds():0.000}"); ImGui.Text($"{node.SelfMilliseconds():0.000}");
ImGui.TableNextColumn();
ImGui.Text($"{node.AllocatedBytes}");
ImGui.TableNextColumn();
ImGui.Text($"{node.SelfAllocatedBytes()}");
if (isOpen) if (isOpen)
{ {
@@ -202,7 +208,7 @@ namespace Nerfed.Editor.Systems
} }
} }
private static void DrawCombined(in IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)>> orderedCombinedData) private static void DrawCombined(in IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)>> orderedCombinedData)
{ {
if(orderedCombinedData == null) if(orderedCombinedData == null)
{ {
@@ -211,18 +217,20 @@ namespace Nerfed.Editor.Systems
ImGui.BeginChild("Combined", new Vector2(0, 0)); ImGui.BeginChild("Combined", new Vector2(0, 0));
if (ImGui.BeginTable("ProfilerCombinedData", 6, tableFlags, new Vector2(0, 0))) if (ImGui.BeginTable("ProfilerCombinedData", 8, tableFlags, new Vector2(0, 0)))
{ {
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.45f, 0); ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.32f, 0);
ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.14f, 1); ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.11f, 1);
ImGui.TableSetupColumn("self", ImGuiTableColumnFlags.WidthStretch, 0.14f, 2); ImGui.TableSetupColumn("self", ImGuiTableColumnFlags.WidthStretch, 0.11f, 2);
ImGui.TableSetupColumn("calls", ImGuiTableColumnFlags.WidthStretch, 0.10f, 3); ImGui.TableSetupColumn("calls", ImGuiTableColumnFlags.WidthStretch, 0.08f, 3);
ImGui.TableSetupColumn("avg", ImGuiTableColumnFlags.WidthStretch, 0.10f, 4); ImGui.TableSetupColumn("avg", ImGuiTableColumnFlags.WidthStretch, 0.09f, 4);
ImGui.TableSetupColumn("p95", ImGuiTableColumnFlags.WidthStretch, 0.10f, 5); ImGui.TableSetupColumn("p95", ImGuiTableColumnFlags.WidthStretch, 0.09f, 5);
ImGui.TableSetupColumn("alloc(B)", ImGuiTableColumnFlags.WidthStretch, 0.10f, 6);
ImGui.TableSetupColumn("self alloc", ImGuiTableColumnFlags.WidthStretch, 0.10f, 7);
ImGui.TableSetupScrollFreeze(0, 1); // Make row always visible ImGui.TableSetupScrollFreeze(0, 1); // Make row always visible
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
foreach (KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)> combinedData in orderedCombinedData) foreach (KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)> combinedData in orderedCombinedData)
{ {
ImGui.TableNextRow(); ImGui.TableNextRow();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@@ -237,6 +245,10 @@ namespace Nerfed.Editor.Systems
ImGui.Text($"{combinedData.Value.avgMs:0.000}"); ImGui.Text($"{combinedData.Value.avgMs:0.000}");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text($"{combinedData.Value.p95Ms:0.000}"); ImGui.Text($"{combinedData.Value.p95Ms:0.000}");
ImGui.TableNextColumn();
ImGui.Text($"{combinedData.Value.allocBytes}");
ImGui.TableNextColumn();
ImGui.Text($"{combinedData.Value.selfAllocBytes}");
} }
ImGui.EndTable(); ImGui.EndTable();
@@ -245,10 +257,10 @@ namespace Nerfed.Editor.Systems
ImGui.EndChild(); ImGui.EndChild();
} }
private static IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms)>> CalculateCombinedData(Profiler.Frame frame) private static IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)>> CalculateCombinedData(Profiler.Frame frame)
{ {
IReadOnlyDictionary<string, Profiler.RollingLabelMetrics> rollingData = Profiler.GetRollingLabelMetricsSnapshot(); 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); Dictionary<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)> combinedRecordData = new Dictionary<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)>(128);
foreach (KeyValuePair<string, Profiler.LabelMetrics> metric in frame.LabelMetrics) foreach (KeyValuePair<string, Profiler.LabelMetrics> metric in frame.LabelMetrics)
{ {
Profiler.RollingLabelMetrics rolling = default; Profiler.RollingLabelMetrics rolling = default;
@@ -257,7 +269,7 @@ namespace Nerfed.Editor.Systems
rolling = found; rolling = found;
} }
combinedRecordData[metric.Key] = (metric.Value.InclusiveMs, metric.Value.SelfMs, metric.Value.Calls, rolling.AverageMs, rolling.P95Ms); combinedRecordData[metric.Key] = (metric.Value.InclusiveMs, metric.Value.SelfMs, metric.Value.Calls, rolling.AverageMs, rolling.P95Ms, metric.Value.AllocatedBytes, metric.Value.SelfAllocatedBytes);
} }
return combinedRecordData.OrderByDescending(x => x.Value.ms); return combinedRecordData.OrderByDescending(x => x.Value.ms);
} }
+28 -5
View File
@@ -39,13 +39,15 @@ public static class Profiler
public readonly struct LabelMetrics public readonly struct LabelMetrics
{ {
public LabelMetrics(double inclusiveMs, double selfMs, uint calls, double minInclusiveMs, double maxInclusiveMs) public LabelMetrics(double inclusiveMs, double selfMs, uint calls, double minInclusiveMs, double maxInclusiveMs, long allocatedBytes, long selfAllocatedBytes)
{ {
InclusiveMs = inclusiveMs; InclusiveMs = inclusiveMs;
SelfMs = selfMs; SelfMs = selfMs;
Calls = calls; Calls = calls;
MinInclusiveMs = minInclusiveMs; MinInclusiveMs = minInclusiveMs;
MaxInclusiveMs = maxInclusiveMs; MaxInclusiveMs = maxInclusiveMs;
AllocatedBytes = allocatedBytes;
SelfAllocatedBytes = selfAllocatedBytes;
} }
public double InclusiveMs { get; } public double InclusiveMs { get; }
@@ -53,6 +55,8 @@ public static class Profiler
public uint Calls { get; } public uint Calls { get; }
public double MinInclusiveMs { get; } public double MinInclusiveMs { get; }
public double MaxInclusiveMs { get; } public double MaxInclusiveMs { get; }
public long AllocatedBytes { get; }
public long SelfAllocatedBytes { get; }
} }
public readonly struct RollingLabelMetrics public readonly struct RollingLabelMetrics
@@ -340,6 +344,8 @@ public static class Profiler
{ {
double inclusiveMs = node.ElapsedMilliseconds(); double inclusiveMs = node.ElapsedMilliseconds();
double selfMs = node.SelfMilliseconds(); double selfMs = node.SelfMilliseconds();
long allocBytes = node.AllocatedBytes;
long selfAllocBytes = node.SelfAllocatedBytes();
if (labelMetrics.TryGetValue(node.Label, out LabelMetrics current)) if (labelMetrics.TryGetValue(node.Label, out LabelMetrics current))
{ {
@@ -348,11 +354,13 @@ public static class Profiler
current.SelfMs + selfMs, current.SelfMs + selfMs,
current.Calls + 1, current.Calls + 1,
Math.Min(current.MinInclusiveMs, inclusiveMs), Math.Min(current.MinInclusiveMs, inclusiveMs),
Math.Max(current.MaxInclusiveMs, inclusiveMs)); Math.Max(current.MaxInclusiveMs, inclusiveMs),
current.AllocatedBytes + allocBytes,
current.SelfAllocatedBytes + selfAllocBytes);
} }
else else
{ {
labelMetrics[node.Label] = new LabelMetrics(inclusiveMs, selfMs, 1, inclusiveMs, inclusiveMs); labelMetrics[node.Label] = new LabelMetrics(inclusiveMs, selfMs, 1, inclusiveMs, inclusiveMs, allocBytes, selfAllocBytes);
} }
if (categoryMetrics.TryGetValue(node.Category, out LabelMetrics categoryCurrent)) if (categoryMetrics.TryGetValue(node.Category, out LabelMetrics categoryCurrent))
@@ -362,11 +370,13 @@ public static class Profiler
categoryCurrent.SelfMs + selfMs, categoryCurrent.SelfMs + selfMs,
categoryCurrent.Calls + 1, categoryCurrent.Calls + 1,
Math.Min(categoryCurrent.MinInclusiveMs, inclusiveMs), Math.Min(categoryCurrent.MinInclusiveMs, inclusiveMs),
Math.Max(categoryCurrent.MaxInclusiveMs, inclusiveMs)); Math.Max(categoryCurrent.MaxInclusiveMs, inclusiveMs),
categoryCurrent.AllocatedBytes + allocBytes,
categoryCurrent.SelfAllocatedBytes + selfAllocBytes);
} }
else else
{ {
categoryMetrics[node.Category] = new LabelMetrics(inclusiveMs, selfMs, 1, inclusiveMs, inclusiveMs); categoryMetrics[node.Category] = new LabelMetrics(inclusiveMs, selfMs, 1, inclusiveMs, inclusiveMs, allocBytes, selfAllocBytes);
} }
for (int i = 0; i < node.Children.Count; i++) for (int i = 0; i < node.Children.Count; i++)
@@ -423,8 +433,11 @@ public static class Profiler
public long EndTime { get; private set; } public long EndTime { get; private set; }
public int ManagedThreadId { get; private set; } public int ManagedThreadId { get; private set; }
public List<ScopeNode> Children { get; } = new List<ScopeNode>(); public List<ScopeNode> Children { get; } = new List<ScopeNode>();
public long AllocatedBytes { get; private set; }
internal ScopeNode Parent { get; private set; } internal ScopeNode Parent { get; private set; }
internal long ChildrenDurationTicks { get; private set; } internal long ChildrenDurationTicks { get; private set; }
internal long ChildrenAllocatedBytes { get; private set; }
private long allocatedBytesAtStart;
internal void Reset(string label, string category, ulong tagMask, int managedThreadId, ScopeNode parent) internal void Reset(string label, string category, ulong tagMask, int managedThreadId, ScopeNode parent)
{ {
@@ -436,7 +449,10 @@ public static class Profiler
StartTime = Stopwatch.GetTimestamp(); StartTime = Stopwatch.GetTimestamp();
EndTime = 0; EndTime = 0;
ChildrenDurationTicks = 0; ChildrenDurationTicks = 0;
ChildrenAllocatedBytes = 0;
AllocatedBytes = 0;
Children.Clear(); Children.Clear();
allocatedBytesAtStart = GC.GetAllocatedBytesForCurrentThread();
} }
internal void End() internal void End()
@@ -447,9 +463,11 @@ public static class Profiler
} }
EndTime = Stopwatch.GetTimestamp(); EndTime = Stopwatch.GetTimestamp();
AllocatedBytes = Math.Max(0, GC.GetAllocatedBytesForCurrentThread() - allocatedBytesAtStart);
if (Parent != null) if (Parent != null)
{ {
Parent.ChildrenDurationTicks += Math.Max(0, EndTime - StartTime); Parent.ChildrenDurationTicks += Math.Max(0, EndTime - StartTime);
Parent.ChildrenAllocatedBytes += AllocatedBytes;
} }
} }
@@ -465,6 +483,11 @@ public static class Profiler
return ((double)selfTicks) * 1000 / Stopwatch.Frequency; return ((double)selfTicks) * 1000 / Stopwatch.Frequency;
} }
public long SelfAllocatedBytes()
{
return Math.Max(0, AllocatedBytes - ChildrenAllocatedBytes);
}
internal ScopeNode AddChild(string label, string category, ulong tagMask) internal ScopeNode AddChild(string label, string category, ulong tagMask)
{ {
ScopeNode child = RentNode(label, category, tagMask, ManagedThreadId, this); ScopeNode child = RentNode(label, category, tagMask, ManagedThreadId, this);