Enhance Profiler with pre-allocated sort buffers and frame management improvements
This commit is contained in:
+108
-53
@@ -110,12 +110,15 @@ public static class Profiler
|
||||
private sealed class RollingWindow
|
||||
{
|
||||
private readonly double[] values;
|
||||
private readonly double[] sortBuffer; // pre-allocated; avoids per-snapshot heap allocation
|
||||
private int index;
|
||||
private int count;
|
||||
|
||||
public RollingWindow(int capacity)
|
||||
{
|
||||
values = new double[Math.Max(8, capacity)];
|
||||
int size = Math.Max(8, capacity);
|
||||
values = new double[size];
|
||||
sortBuffer = new double[size];
|
||||
}
|
||||
|
||||
public void Add(double value)
|
||||
@@ -138,21 +141,20 @@ public static class Profiler
|
||||
double sum = 0;
|
||||
double min = double.MaxValue;
|
||||
double max = double.MinValue;
|
||||
double[] sorted = new double[count];
|
||||
|
||||
int start = (index - count + values.Length) % values.Length;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double value = values[(start + i) % values.Length];
|
||||
sorted[i] = value;
|
||||
sortBuffer[i] = value;
|
||||
sum += value;
|
||||
min = Math.Min(min, value);
|
||||
max = Math.Max(max, value);
|
||||
}
|
||||
|
||||
Array.Sort(sorted);
|
||||
Array.Sort(sortBuffer, 0, count);
|
||||
int percentileIndex = (int)Math.Ceiling((count - 1) * 0.95d);
|
||||
double p95 = sorted[Math.Clamp(percentileIndex, 0, count - 1)];
|
||||
double p95 = sortBuffer[Math.Clamp(percentileIndex, 0, count - 1)];
|
||||
return new RollingLabelMetrics(sum / count, min, max, p95, count);
|
||||
}
|
||||
}
|
||||
@@ -160,6 +162,7 @@ public static class Profiler
|
||||
private sealed class RollingThreadWindow
|
||||
{
|
||||
private readonly double[] durations;
|
||||
private readonly double[] sortBuffer; // pre-allocated; avoids per-snapshot heap allocation
|
||||
private readonly byte[] misses;
|
||||
private int index;
|
||||
private int count;
|
||||
@@ -168,6 +171,7 @@ public static class Profiler
|
||||
{
|
||||
int size = Math.Max(8, capacity);
|
||||
durations = new double[size];
|
||||
sortBuffer = new double[size];
|
||||
misses = new byte[size];
|
||||
}
|
||||
|
||||
@@ -192,7 +196,6 @@ public static class Profiler
|
||||
double sum = 0;
|
||||
double max = double.MinValue;
|
||||
int budgetMisses = 0;
|
||||
double[] sorted = new double[count];
|
||||
|
||||
int start = (index - count + durations.Length) % durations.Length;
|
||||
for (int i = 0; i < count; i++)
|
||||
@@ -202,39 +205,44 @@ public static class Profiler
|
||||
sum += value;
|
||||
max = Math.Max(max, value);
|
||||
budgetMisses += misses[at];
|
||||
sorted[i] = value;
|
||||
sortBuffer[i] = value;
|
||||
}
|
||||
|
||||
Array.Sort(sorted);
|
||||
Array.Sort(sortBuffer, 0, count);
|
||||
int percentileIndex = (int)Math.Ceiling((count - 1) * 0.95d);
|
||||
double p95 = sorted[Math.Clamp(percentileIndex, 0, count - 1)];
|
||||
double p95 = sortBuffer[Math.Clamp(percentileIndex, 0, count - 1)];
|
||||
return new RollingThreadMetrics(sum / count, p95, max, count, budgetMisses);
|
||||
}
|
||||
}
|
||||
|
||||
public class Frame(uint frameCount)
|
||||
public class Frame
|
||||
{
|
||||
public uint FrameCount { get; } = frameCount;
|
||||
public long StartTime { get; } = Stopwatch.GetTimestamp();
|
||||
public long EndTime { get; private set; }
|
||||
|
||||
private readonly List<ScopeNode> rootNodes = new List<ScopeNode>(8);
|
||||
private readonly object rootNodesLock = new object();
|
||||
private readonly Dictionary<string, LabelMetrics> labelMetrics = new Dictionary<string, LabelMetrics>(128, StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, LabelMetrics> categoryMetrics = new Dictionary<string, LabelMetrics>(32, StringComparer.Ordinal);
|
||||
private readonly Dictionary<int, ThreadMetrics> threadMetrics = new Dictionary<int, ThreadMetrics>(16);
|
||||
private readonly List<int> knownThreadIds = new List<int>(16); // avoids Keys.ToArray() in ApplyThreadBudgetFlags
|
||||
|
||||
public uint FrameCount { get; private set; }
|
||||
public long StartTime { get; private set; }
|
||||
public long EndTime { get; private set; }
|
||||
|
||||
public IReadOnlyList<ScopeNode> RootNodes => rootNodes;
|
||||
|
||||
public IReadOnlyDictionary<string, LabelMetrics> LabelMetrics => labelMetrics;
|
||||
public IReadOnlyDictionary<string, LabelMetrics> CategoryMetrics => categoryMetrics;
|
||||
public IReadOnlyDictionary<int, ThreadMetrics> ThreadMetrics => threadMetrics;
|
||||
public long AllocatedBytesStart { get; } = GC.GetTotalAllocatedBytes(false);
|
||||
|
||||
// Return concrete types so callers can use the struct enumerator and avoid boxing.
|
||||
internal Dictionary<string, LabelMetrics> LabelMetricsRaw => labelMetrics;
|
||||
internal Dictionary<int, ThreadMetrics> ThreadMetricsRaw => threadMetrics;
|
||||
|
||||
public long AllocatedBytesStart { get; private set; }
|
||||
public long AllocatedBytesEnd { get; private set; }
|
||||
public long AllocatedBytesDelta { get; private set; }
|
||||
public int Gen0CollectionsStart { get; } = GC.CollectionCount(0);
|
||||
public int Gen1CollectionsStart { get; } = GC.CollectionCount(1);
|
||||
public int Gen2CollectionsStart { get; } = GC.CollectionCount(2);
|
||||
public int Gen0CollectionsStart { get; private set; }
|
||||
public int Gen1CollectionsStart { get; private set; }
|
||||
public int Gen2CollectionsStart { get; private set; }
|
||||
public int Gen0CollectionsEnd { get; private set; }
|
||||
public int Gen1CollectionsEnd { get; private set; }
|
||||
public int Gen2CollectionsEnd { get; private set; }
|
||||
@@ -244,6 +252,31 @@ public static class Profiler
|
||||
public bool OverBudget { get; private set; }
|
||||
public double BudgetMilliseconds { get; private set; }
|
||||
|
||||
internal void Reset(uint frameCount)
|
||||
{
|
||||
FrameCount = frameCount;
|
||||
StartTime = Stopwatch.GetTimestamp();
|
||||
EndTime = 0;
|
||||
OverBudget = false;
|
||||
BudgetMilliseconds = 0;
|
||||
AllocatedBytesStart = GC.GetTotalAllocatedBytes(false);
|
||||
AllocatedBytesEnd = 0;
|
||||
AllocatedBytesDelta = 0;
|
||||
Gen0CollectionsStart = GC.CollectionCount(0);
|
||||
Gen1CollectionsStart = GC.CollectionCount(1);
|
||||
Gen2CollectionsStart = GC.CollectionCount(2);
|
||||
Gen0CollectionsEnd = 0;
|
||||
Gen1CollectionsEnd = 0;
|
||||
Gen2CollectionsEnd = 0;
|
||||
Gen0CollectionsDelta = 0;
|
||||
Gen1CollectionsDelta = 0;
|
||||
Gen2CollectionsDelta = 0;
|
||||
lock (rootNodesLock)
|
||||
{
|
||||
rootNodes.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
internal void AddRootNode(ScopeNode rootNode)
|
||||
{
|
||||
lock (rootNodesLock)
|
||||
@@ -282,6 +315,7 @@ public static class Profiler
|
||||
labelMetrics.Clear();
|
||||
categoryMetrics.Clear();
|
||||
threadMetrics.Clear();
|
||||
knownThreadIds.Clear();
|
||||
lock (rootNodesLock)
|
||||
{
|
||||
for (int i = 0; i < rootNodes.Count; i++)
|
||||
@@ -289,11 +323,12 @@ public static class Profiler
|
||||
AccumulateLabelMetrics(rootNodes[i]);
|
||||
}
|
||||
|
||||
foreach (ScopeNode rootNode in rootNodes)
|
||||
for (int i = 0; i < rootNodes.Count; i++)
|
||||
{
|
||||
for (int i = 0; i < rootNode.Children.Count; i++)
|
||||
ScopeNode rootNode = rootNodes[i];
|
||||
for (int j = 0; j < rootNode.Children.Count; j++)
|
||||
{
|
||||
AccumulateThreadMetrics(rootNode.ManagedThreadId, rootNode.Children[i]);
|
||||
AccumulateThreadMetrics(rootNode.ManagedThreadId, rootNode.Children[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +387,7 @@ public static class Profiler
|
||||
else
|
||||
{
|
||||
threadMetrics[threadId] = new ThreadMetrics(inclusiveMs, selfMs, 1, false);
|
||||
knownThreadIds.Add(threadId);
|
||||
}
|
||||
|
||||
for (int i = 0; i < node.Children.Count; i++)
|
||||
@@ -368,10 +404,10 @@ public static class Profiler
|
||||
return;
|
||||
}
|
||||
|
||||
int[] keys = threadMetrics.Keys.ToArray();
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
// knownThreadIds avoids Keys.ToArray() allocation
|
||||
for (int i = 0; i < knownThreadIds.Count; i++)
|
||||
{
|
||||
int key = keys[i];
|
||||
int key = knownThreadIds[i];
|
||||
ThreadMetrics metric = threadMetrics[key];
|
||||
threadMetrics[key] = new ThreadMetrics(metric.InclusiveMs, metric.SelfMs, metric.Calls, metric.InclusiveMs > perThreadBudget);
|
||||
}
|
||||
@@ -393,7 +429,7 @@ public static class Profiler
|
||||
internal void Reset(string label, string category, ulong tagMask, int managedThreadId, ScopeNode parent)
|
||||
{
|
||||
Label = label;
|
||||
Category = string.IsNullOrWhiteSpace(category) ? DefaultCategory : category;
|
||||
Category = string.IsNullOrEmpty(category) ? DefaultCategory : category;
|
||||
TagMask = tagMask;
|
||||
ManagedThreadId = managedThreadId;
|
||||
Parent = parent;
|
||||
@@ -429,7 +465,6 @@ public static class Profiler
|
||||
return ((double)selfTicks) * 1000 / Stopwatch.Frequency;
|
||||
}
|
||||
|
||||
// Add a child node (used for nested scopes)
|
||||
internal ScopeNode AddChild(string label, string category, ulong tagMask)
|
||||
{
|
||||
ScopeNode child = RentNode(label, category, tagMask, ManagedThreadId, this);
|
||||
@@ -448,13 +483,27 @@ public static class Profiler
|
||||
public static CaptureMode Mode { get; set; } = CaptureMode.Instrumented;
|
||||
public static int SamplingStride { get; set; } = 8;
|
||||
|
||||
// Store only the last x amount of frames in memory.
|
||||
public static readonly BoundedQueue<Frame> Frames = new(maxFrames);
|
||||
|
||||
// Use ThreadLocal to store a stack of ScopeNodes per thread and enable tracking of thread-local values.
|
||||
private static readonly ThreadLocal<ThreadProfilerState> threadStates = new ThreadLocal<ThreadProfilerState>(() => new ThreadProfilerState(), true);
|
||||
// trackAllValues=false; registeredThreadStates avoids ThreadLocal.Values allocating a ReadOnlyCollection each call
|
||||
private static readonly ThreadLocal<ThreadProfilerState> threadStates =
|
||||
new ThreadLocal<ThreadProfilerState>(() =>
|
||||
{
|
||||
ThreadProfilerState state = new ThreadProfilerState();
|
||||
lock (registeredThreadStatesLock)
|
||||
{
|
||||
registeredThreadStates.Add(state);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
private static readonly List<ThreadProfilerState> registeredThreadStates = new List<ThreadProfilerState>(8);
|
||||
private static readonly object registeredThreadStatesLock = new object();
|
||||
|
||||
private static readonly ConcurrentDictionary<int, string> threadRootLabelCache = new ConcurrentDictionary<int, string>();
|
||||
private static readonly ConcurrentBag<ScopeNode> nodePool = new ConcurrentBag<ScopeNode>();
|
||||
private static readonly ConcurrentBag<Frame> framePool = new ConcurrentBag<Frame>(); // pooled; avoids per-frame Frame allocation
|
||||
private static readonly Dictionary<string, RollingWindow> rollingWindows = new Dictionary<string, RollingWindow>(256, StringComparer.Ordinal);
|
||||
private static readonly Dictionary<int, RollingThreadWindow> rollingThreadWindows = new Dictionary<int, RollingThreadWindow>(16);
|
||||
private static readonly object rollingWindowsLock = new object();
|
||||
@@ -474,13 +523,7 @@ public static class Profiler
|
||||
|
||||
public static int CopyFramesTo(List<Frame> destination)
|
||||
{
|
||||
destination.Clear();
|
||||
foreach (Frame frame in Frames)
|
||||
{
|
||||
destination.Add(frame);
|
||||
}
|
||||
|
||||
return destination.Count;
|
||||
return Frames.CopyTo(destination);
|
||||
}
|
||||
|
||||
public static IReadOnlyDictionary<string, RollingLabelMetrics> GetRollingLabelMetricsSnapshot()
|
||||
@@ -524,7 +567,7 @@ public static class Profiler
|
||||
FinalizeCurrentFrame();
|
||||
}
|
||||
|
||||
currentFrame = new Frame(frameCount);
|
||||
currentFrame = RentFrame(frameCount);
|
||||
}
|
||||
|
||||
[Conditional("PROFILING")]
|
||||
@@ -574,17 +617,14 @@ public static class Profiler
|
||||
|
||||
if (scopes.Count == 0)
|
||||
{
|
||||
// First scope for this thread (new root for this thread)
|
||||
int threadId = state.ThreadId;
|
||||
ScopeNode rootScopeNode = RentNode(GetThreadRootLabel(threadId), DefaultCategory, 0, threadId, null);
|
||||
scopes.Push(rootScopeNode);
|
||||
frame.AddRootNode(rootScopeNode);
|
||||
}
|
||||
|
||||
// Create a new child under the current top of the stack
|
||||
ScopeNode newScope = scopes.Peek().AddChild(label, category, tagMask);
|
||||
|
||||
scopes.Push(newScope); // Push new scope to the thread's stack
|
||||
scopes.Push(newScope);
|
||||
}
|
||||
|
||||
[Conditional("PROFILING")]
|
||||
@@ -628,6 +668,17 @@ public static class Profiler
|
||||
return threadRootLabelCache.GetOrAdd(threadId, static id => $"Thread-{id}");
|
||||
}
|
||||
|
||||
private static Frame RentFrame(uint count)
|
||||
{
|
||||
if (!framePool.TryTake(out Frame frame))
|
||||
{
|
||||
frame = new Frame();
|
||||
}
|
||||
|
||||
frame.Reset(count);
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static ScopeNode RentNode(string label, string category, ulong tagMask, int managedThreadId, ScopeNode parent)
|
||||
{
|
||||
if (!nodePool.TryTake(out ScopeNode node))
|
||||
@@ -658,17 +709,19 @@ public static class Profiler
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ThreadProfilerState state in threadStates.Values)
|
||||
lock (registeredThreadStatesLock)
|
||||
{
|
||||
Stack<ScopeNode> scopes = state.Scopes;
|
||||
while (scopes.Count > 0)
|
||||
for (int i = 0; i < registeredThreadStates.Count; i++)
|
||||
{
|
||||
ScopeNode currentScope = scopes.Pop();
|
||||
currentScope.End();
|
||||
}
|
||||
ThreadProfilerState state = registeredThreadStates[i];
|
||||
Stack<ScopeNode> scopes = state.Scopes;
|
||||
while (scopes.Count > 0)
|
||||
{
|
||||
scopes.Pop().End();
|
||||
}
|
||||
|
||||
scopes.Clear();
|
||||
state.CaptureDecisions.Clear();
|
||||
state.CaptureDecisions.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
frame.End(FrameBudgetMilliseconds);
|
||||
@@ -679,6 +732,8 @@ public static class Profiler
|
||||
{
|
||||
ReturnNodeTree(evictedFrame.RootNodes[i]);
|
||||
}
|
||||
|
||||
framePool.Add(evictedFrame);
|
||||
}
|
||||
|
||||
UpdateRollingWindows(frame);
|
||||
@@ -691,7 +746,7 @@ public static class Profiler
|
||||
{
|
||||
lock (rollingWindowsLock)
|
||||
{
|
||||
foreach (KeyValuePair<string, LabelMetrics> pair in frame.LabelMetrics)
|
||||
foreach (KeyValuePair<string, LabelMetrics> pair in frame.LabelMetricsRaw)
|
||||
{
|
||||
if (!rollingWindows.TryGetValue(pair.Key, out RollingWindow window))
|
||||
{
|
||||
@@ -708,7 +763,7 @@ public static class Profiler
|
||||
{
|
||||
lock (rollingWindowsLock)
|
||||
{
|
||||
foreach (KeyValuePair<int, ThreadMetrics> pair in frame.ThreadMetrics)
|
||||
foreach (KeyValuePair<int, ThreadMetrics> pair in frame.ThreadMetricsRaw)
|
||||
{
|
||||
if (!rollingThreadWindows.TryGetValue(pair.Key, out RollingThreadWindow window))
|
||||
{
|
||||
@@ -720,4 +775,4 @@ public static class Profiler
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user