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 sealed class RollingWindow
|
||||||
{
|
{
|
||||||
private readonly double[] values;
|
private readonly double[] values;
|
||||||
|
private readonly double[] sortBuffer; // pre-allocated; avoids per-snapshot heap allocation
|
||||||
private int index;
|
private int index;
|
||||||
private int count;
|
private int count;
|
||||||
|
|
||||||
public RollingWindow(int capacity)
|
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)
|
public void Add(double value)
|
||||||
@@ -138,21 +141,20 @@ public static class Profiler
|
|||||||
double sum = 0;
|
double sum = 0;
|
||||||
double min = double.MaxValue;
|
double min = double.MaxValue;
|
||||||
double max = double.MinValue;
|
double max = double.MinValue;
|
||||||
double[] sorted = new double[count];
|
|
||||||
|
|
||||||
int start = (index - count + values.Length) % values.Length;
|
int start = (index - count + values.Length) % values.Length;
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
double value = values[(start + i) % values.Length];
|
double value = values[(start + i) % values.Length];
|
||||||
sorted[i] = value;
|
sortBuffer[i] = value;
|
||||||
sum += value;
|
sum += value;
|
||||||
min = Math.Min(min, value);
|
min = Math.Min(min, value);
|
||||||
max = Math.Max(max, value);
|
max = Math.Max(max, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Array.Sort(sorted);
|
Array.Sort(sortBuffer, 0, count);
|
||||||
int percentileIndex = (int)Math.Ceiling((count - 1) * 0.95d);
|
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);
|
return new RollingLabelMetrics(sum / count, min, max, p95, count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,6 +162,7 @@ public static class Profiler
|
|||||||
private sealed class RollingThreadWindow
|
private sealed class RollingThreadWindow
|
||||||
{
|
{
|
||||||
private readonly double[] durations;
|
private readonly double[] durations;
|
||||||
|
private readonly double[] sortBuffer; // pre-allocated; avoids per-snapshot heap allocation
|
||||||
private readonly byte[] misses;
|
private readonly byte[] misses;
|
||||||
private int index;
|
private int index;
|
||||||
private int count;
|
private int count;
|
||||||
@@ -168,6 +171,7 @@ public static class Profiler
|
|||||||
{
|
{
|
||||||
int size = Math.Max(8, capacity);
|
int size = Math.Max(8, capacity);
|
||||||
durations = new double[size];
|
durations = new double[size];
|
||||||
|
sortBuffer = new double[size];
|
||||||
misses = new byte[size];
|
misses = new byte[size];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +196,6 @@ public static class Profiler
|
|||||||
double sum = 0;
|
double sum = 0;
|
||||||
double max = double.MinValue;
|
double max = double.MinValue;
|
||||||
int budgetMisses = 0;
|
int budgetMisses = 0;
|
||||||
double[] sorted = new double[count];
|
|
||||||
|
|
||||||
int start = (index - count + durations.Length) % durations.Length;
|
int start = (index - count + durations.Length) % durations.Length;
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
@@ -202,39 +205,44 @@ public static class Profiler
|
|||||||
sum += value;
|
sum += value;
|
||||||
max = Math.Max(max, value);
|
max = Math.Max(max, value);
|
||||||
budgetMisses += misses[at];
|
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);
|
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);
|
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 List<ScopeNode> rootNodes = new List<ScopeNode>(8);
|
||||||
private readonly object rootNodesLock = new object();
|
private readonly object rootNodesLock = new object();
|
||||||
private readonly Dictionary<string, LabelMetrics> labelMetrics = new Dictionary<string, LabelMetrics>(128, StringComparer.Ordinal);
|
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<string, LabelMetrics> categoryMetrics = new Dictionary<string, LabelMetrics>(32, StringComparer.Ordinal);
|
||||||
private readonly Dictionary<int, ThreadMetrics> threadMetrics = new Dictionary<int, ThreadMetrics>(16);
|
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 IReadOnlyList<ScopeNode> RootNodes => rootNodes;
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, LabelMetrics> LabelMetrics => labelMetrics;
|
public IReadOnlyDictionary<string, LabelMetrics> LabelMetrics => labelMetrics;
|
||||||
public IReadOnlyDictionary<string, LabelMetrics> CategoryMetrics => categoryMetrics;
|
public IReadOnlyDictionary<string, LabelMetrics> CategoryMetrics => categoryMetrics;
|
||||||
public IReadOnlyDictionary<int, ThreadMetrics> ThreadMetrics => threadMetrics;
|
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 AllocatedBytesEnd { get; private set; }
|
||||||
public long AllocatedBytesDelta { get; private set; }
|
public long AllocatedBytesDelta { get; private set; }
|
||||||
public int Gen0CollectionsStart { get; } = GC.CollectionCount(0);
|
public int Gen0CollectionsStart { get; private set; }
|
||||||
public int Gen1CollectionsStart { get; } = GC.CollectionCount(1);
|
public int Gen1CollectionsStart { get; private set; }
|
||||||
public int Gen2CollectionsStart { get; } = GC.CollectionCount(2);
|
public int Gen2CollectionsStart { get; private set; }
|
||||||
public int Gen0CollectionsEnd { get; private set; }
|
public int Gen0CollectionsEnd { get; private set; }
|
||||||
public int Gen1CollectionsEnd { get; private set; }
|
public int Gen1CollectionsEnd { get; private set; }
|
||||||
public int Gen2CollectionsEnd { get; private set; }
|
public int Gen2CollectionsEnd { get; private set; }
|
||||||
@@ -244,6 +252,31 @@ public static class Profiler
|
|||||||
public bool OverBudget { get; private set; }
|
public bool OverBudget { get; private set; }
|
||||||
public double BudgetMilliseconds { 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)
|
internal void AddRootNode(ScopeNode rootNode)
|
||||||
{
|
{
|
||||||
lock (rootNodesLock)
|
lock (rootNodesLock)
|
||||||
@@ -282,6 +315,7 @@ public static class Profiler
|
|||||||
labelMetrics.Clear();
|
labelMetrics.Clear();
|
||||||
categoryMetrics.Clear();
|
categoryMetrics.Clear();
|
||||||
threadMetrics.Clear();
|
threadMetrics.Clear();
|
||||||
|
knownThreadIds.Clear();
|
||||||
lock (rootNodesLock)
|
lock (rootNodesLock)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < rootNodes.Count; i++)
|
for (int i = 0; i < rootNodes.Count; i++)
|
||||||
@@ -289,11 +323,12 @@ public static class Profiler
|
|||||||
AccumulateLabelMetrics(rootNodes[i]);
|
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
|
else
|
||||||
{
|
{
|
||||||
threadMetrics[threadId] = new ThreadMetrics(inclusiveMs, selfMs, 1, false);
|
threadMetrics[threadId] = new ThreadMetrics(inclusiveMs, selfMs, 1, false);
|
||||||
|
knownThreadIds.Add(threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < node.Children.Count; i++)
|
for (int i = 0; i < node.Children.Count; i++)
|
||||||
@@ -368,10 +404,10 @@ public static class Profiler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int[] keys = threadMetrics.Keys.ToArray();
|
// knownThreadIds avoids Keys.ToArray() allocation
|
||||||
for (int i = 0; i < keys.Length; i++)
|
for (int i = 0; i < knownThreadIds.Count; i++)
|
||||||
{
|
{
|
||||||
int key = keys[i];
|
int key = knownThreadIds[i];
|
||||||
ThreadMetrics metric = threadMetrics[key];
|
ThreadMetrics metric = threadMetrics[key];
|
||||||
threadMetrics[key] = new ThreadMetrics(metric.InclusiveMs, metric.SelfMs, metric.Calls, metric.InclusiveMs > perThreadBudget);
|
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)
|
internal void Reset(string label, string category, ulong tagMask, int managedThreadId, ScopeNode parent)
|
||||||
{
|
{
|
||||||
Label = label;
|
Label = label;
|
||||||
Category = string.IsNullOrWhiteSpace(category) ? DefaultCategory : category;
|
Category = string.IsNullOrEmpty(category) ? DefaultCategory : category;
|
||||||
TagMask = tagMask;
|
TagMask = tagMask;
|
||||||
ManagedThreadId = managedThreadId;
|
ManagedThreadId = managedThreadId;
|
||||||
Parent = parent;
|
Parent = parent;
|
||||||
@@ -429,7 +465,6 @@ public static class Profiler
|
|||||||
return ((double)selfTicks) * 1000 / Stopwatch.Frequency;
|
return ((double)selfTicks) * 1000 / Stopwatch.Frequency;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a child node (used for nested scopes)
|
|
||||||
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);
|
||||||
@@ -448,13 +483,27 @@ public static class Profiler
|
|||||||
public static CaptureMode Mode { get; set; } = CaptureMode.Instrumented;
|
public static CaptureMode Mode { get; set; } = CaptureMode.Instrumented;
|
||||||
public static int SamplingStride { get; set; } = 8;
|
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);
|
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.
|
// trackAllValues=false; registeredThreadStates avoids ThreadLocal.Values allocating a ReadOnlyCollection each call
|
||||||
private static readonly ThreadLocal<ThreadProfilerState> threadStates = new ThreadLocal<ThreadProfilerState>(() => new ThreadProfilerState(), true);
|
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 ConcurrentDictionary<int, string> threadRootLabelCache = new ConcurrentDictionary<int, string>();
|
||||||
private static readonly ConcurrentBag<ScopeNode> nodePool = new ConcurrentBag<ScopeNode>();
|
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<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 Dictionary<int, RollingThreadWindow> rollingThreadWindows = new Dictionary<int, RollingThreadWindow>(16);
|
||||||
private static readonly object rollingWindowsLock = new object();
|
private static readonly object rollingWindowsLock = new object();
|
||||||
@@ -474,13 +523,7 @@ public static class Profiler
|
|||||||
|
|
||||||
public static int CopyFramesTo(List<Frame> destination)
|
public static int CopyFramesTo(List<Frame> destination)
|
||||||
{
|
{
|
||||||
destination.Clear();
|
return Frames.CopyTo(destination);
|
||||||
foreach (Frame frame in Frames)
|
|
||||||
{
|
|
||||||
destination.Add(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
return destination.Count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyDictionary<string, RollingLabelMetrics> GetRollingLabelMetricsSnapshot()
|
public static IReadOnlyDictionary<string, RollingLabelMetrics> GetRollingLabelMetricsSnapshot()
|
||||||
@@ -524,7 +567,7 @@ public static class Profiler
|
|||||||
FinalizeCurrentFrame();
|
FinalizeCurrentFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
currentFrame = new Frame(frameCount);
|
currentFrame = RentFrame(frameCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("PROFILING")]
|
[Conditional("PROFILING")]
|
||||||
@@ -574,17 +617,14 @@ public static class Profiler
|
|||||||
|
|
||||||
if (scopes.Count == 0)
|
if (scopes.Count == 0)
|
||||||
{
|
{
|
||||||
// First scope for this thread (new root for this thread)
|
|
||||||
int threadId = state.ThreadId;
|
int threadId = state.ThreadId;
|
||||||
ScopeNode rootScopeNode = RentNode(GetThreadRootLabel(threadId), DefaultCategory, 0, threadId, null);
|
ScopeNode rootScopeNode = RentNode(GetThreadRootLabel(threadId), DefaultCategory, 0, threadId, null);
|
||||||
scopes.Push(rootScopeNode);
|
scopes.Push(rootScopeNode);
|
||||||
frame.AddRootNode(rootScopeNode);
|
frame.AddRootNode(rootScopeNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new child under the current top of the stack
|
|
||||||
ScopeNode newScope = scopes.Peek().AddChild(label, category, tagMask);
|
ScopeNode newScope = scopes.Peek().AddChild(label, category, tagMask);
|
||||||
|
scopes.Push(newScope);
|
||||||
scopes.Push(newScope); // Push new scope to the thread's stack
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("PROFILING")]
|
[Conditional("PROFILING")]
|
||||||
@@ -628,6 +668,17 @@ public static class Profiler
|
|||||||
return threadRootLabelCache.GetOrAdd(threadId, static id => $"Thread-{id}");
|
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)
|
private static ScopeNode RentNode(string label, string category, ulong tagMask, int managedThreadId, ScopeNode parent)
|
||||||
{
|
{
|
||||||
if (!nodePool.TryTake(out ScopeNode node))
|
if (!nodePool.TryTake(out ScopeNode node))
|
||||||
@@ -658,17 +709,19 @@ public static class Profiler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (ThreadProfilerState state in threadStates.Values)
|
lock (registeredThreadStatesLock)
|
||||||
{
|
{
|
||||||
Stack<ScopeNode> scopes = state.Scopes;
|
for (int i = 0; i < registeredThreadStates.Count; i++)
|
||||||
while (scopes.Count > 0)
|
|
||||||
{
|
{
|
||||||
ScopeNode currentScope = scopes.Pop();
|
ThreadProfilerState state = registeredThreadStates[i];
|
||||||
currentScope.End();
|
Stack<ScopeNode> scopes = state.Scopes;
|
||||||
}
|
while (scopes.Count > 0)
|
||||||
|
{
|
||||||
|
scopes.Pop().End();
|
||||||
|
}
|
||||||
|
|
||||||
scopes.Clear();
|
state.CaptureDecisions.Clear();
|
||||||
state.CaptureDecisions.Clear();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.End(FrameBudgetMilliseconds);
|
frame.End(FrameBudgetMilliseconds);
|
||||||
@@ -679,6 +732,8 @@ public static class Profiler
|
|||||||
{
|
{
|
||||||
ReturnNodeTree(evictedFrame.RootNodes[i]);
|
ReturnNodeTree(evictedFrame.RootNodes[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
framePool.Add(evictedFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateRollingWindows(frame);
|
UpdateRollingWindows(frame);
|
||||||
@@ -691,7 +746,7 @@ public static class Profiler
|
|||||||
{
|
{
|
||||||
lock (rollingWindowsLock)
|
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))
|
if (!rollingWindows.TryGetValue(pair.Key, out RollingWindow window))
|
||||||
{
|
{
|
||||||
@@ -708,7 +763,7 @@ public static class Profiler
|
|||||||
{
|
{
|
||||||
lock (rollingWindowsLock)
|
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))
|
if (!rollingThreadWindows.TryGetValue(pair.Key, out RollingThreadWindow window))
|
||||||
{
|
{
|
||||||
@@ -720,4 +775,4 @@ public static class Profiler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,15 +283,22 @@ public static class ProfilerVisualizer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isZoom = io.KeyCtrl;
|
if (!io.KeyCtrl && !io.KeyShift)
|
||||||
if (isZoom)
|
{
|
||||||
|
return; // plain scroll goes to ImGui vertical scrolling
|
||||||
|
}
|
||||||
|
|
||||||
|
float wheel = io.MouseWheel;
|
||||||
|
io.MouseWheel = 0; // consume so the child window doesn't also scroll vertically
|
||||||
|
|
||||||
|
if (io.KeyCtrl)
|
||||||
{
|
{
|
||||||
float previousZoom = state.Zoom;
|
float previousZoom = state.Zoom;
|
||||||
double visibleStartTicksBefore = timelineStartTicks + state.PanTicks;
|
double visibleStartTicksBefore = timelineStartTicks + state.PanTicks;
|
||||||
double mouseT = Math.Clamp((ImGui.GetMousePos().X - originX) / Math.Max(1f, canvasWidth), 0f, 1f);
|
double mouseT = Math.Clamp((ImGui.GetMousePos().X - originX) / Math.Max(1f, canvasWidth), 0f, 1f);
|
||||||
double pivotTick = visibleStartTicksBefore + (visibleDurationTicks * mouseT);
|
double pivotTick = visibleStartTicksBefore + (visibleDurationTicks * mouseT);
|
||||||
|
|
||||||
state.Zoom = Math.Clamp(state.Zoom * MathF.Pow(1.12f, io.MouseWheel), 1f, 128f);
|
state.Zoom = Math.Clamp(state.Zoom * MathF.Pow(1.12f, wheel), 1f, 128f);
|
||||||
if (Math.Abs(previousZoom - state.Zoom) > float.Epsilon)
|
if (Math.Abs(previousZoom - state.Zoom) > float.Epsilon)
|
||||||
{
|
{
|
||||||
double newVisibleDurationTicks = Math.Max(1d, timelineDurationTicks / state.Zoom);
|
double newVisibleDurationTicks = Math.Max(1d, timelineDurationTicks / state.Zoom);
|
||||||
@@ -303,7 +310,8 @@ public static class ProfilerVisualizer
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
state.PanTicks -= io.MouseWheel * (visibleDurationTicks * 0.10d);
|
// Shift + scroll: horizontal pan
|
||||||
|
state.PanTicks -= wheel * (visibleDurationTicks * 0.10d);
|
||||||
state.FollowLatest = false;
|
state.FollowLatest = false;
|
||||||
userNavigated = true;
|
userNavigated = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,21 @@ public class BoundedQueue<T> : IEnumerable<T>, ICollection, IReadOnlyCollection<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Iterates the internal Queue<T> directly (struct enumerator, no allocation) under the lock.
|
||||||
|
public int CopyTo(List<T> destination)
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
destination.Clear();
|
||||||
|
foreach (T item in queue)
|
||||||
|
{
|
||||||
|
destination.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerator<T> GetEnumerator()
|
public IEnumerator<T> GetEnumerator()
|
||||||
{
|
{
|
||||||
T[] snapshot;
|
T[] snapshot;
|
||||||
|
|||||||
Reference in New Issue
Block a user