2024-10-20 23:17:41 +02:00
|
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
using System.Diagnostics;
|
2024-07-05 14:32:58 +02:00
|
|
|
|
|
|
|
|
|
namespace Nerfed.Runtime;
|
|
|
|
|
|
|
|
|
|
public struct ProfilerScope : IDisposable
|
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
public ProfilerScope(string label)
|
|
|
|
|
{
|
2024-07-05 14:32:58 +02:00
|
|
|
|
Profiler.BeginSample(label);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
2024-07-05 14:32:58 +02:00
|
|
|
|
Profiler.EndSample();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static class Profiler
|
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
public class Frame(uint frameCount)
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
public uint FrameCount { get; } = frameCount;
|
|
|
|
|
public long StartTime { get; } = Stopwatch.GetTimestamp();
|
|
|
|
|
public long EndTime { get; private set; }
|
2024-10-19 23:41:05 +02:00
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
// Use a concurrent list to collect all thread root nodes per frame.
|
|
|
|
|
public ConcurrentBag<ScopeNode> RootNodes = new ConcurrentBag<ScopeNode>();
|
|
|
|
|
|
|
|
|
|
internal void End()
|
|
|
|
|
{
|
|
|
|
|
EndTime = Stopwatch.GetTimestamp();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double ElapsedMilliseconds()
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
long elapsedTicks = EndTime - StartTime;
|
2024-10-19 23:41:05 +02:00
|
|
|
|
return ((double)(elapsedTicks * 1000)) / Stopwatch.Frequency;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
public class ScopeNode(string label)
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
public string Label { get; } = label;
|
|
|
|
|
public long StartTime { get; private set; } = Stopwatch.GetTimestamp(); // Start time in ticks
|
|
|
|
|
public long EndTime { get; private set; }
|
|
|
|
|
public int ManagedThreadId { get; } = Environment.CurrentManagedThreadId;
|
|
|
|
|
public List<ScopeNode> Children { get; } = new List<ScopeNode>();
|
2024-10-19 23:41:05 +02:00
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
internal void End()
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
EndTime = Stopwatch.GetTimestamp(); // End time in ticks
|
2024-10-19 23:41:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double ElapsedMilliseconds()
|
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
return ((double)(EndTime - StartTime)) * 1000 / Stopwatch.Frequency; // Convert ticks to ms
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add a child node (used for nested scopes)
|
|
|
|
|
internal ScopeNode AddChild(string label)
|
|
|
|
|
{
|
|
|
|
|
ScopeNode child = new ScopeNode(label);
|
|
|
|
|
Children.Add(child);
|
|
|
|
|
return child;
|
2024-10-19 23:41:05 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private const int maxFrames = 128;
|
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
public static bool IsRecording { get; private set; } = true;
|
|
|
|
|
|
|
|
|
|
// 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<Stack<ScopeNode>> threadLocalScopes = new ThreadLocal<Stack<ScopeNode>>(() => new Stack<ScopeNode>(), true);
|
2024-10-19 23:41:05 +02:00
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
private static Frame currentFrame = null;
|
|
|
|
|
private static uint frameCount = 0;
|
2024-10-19 23:41:05 +02:00
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
public static void SetActive(bool isRecording)
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
IsRecording = isRecording;
|
2024-10-19 23:41:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Conditional("PROFILING")]
|
|
|
|
|
public static void BeginFrame()
|
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
if (!IsRecording)
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
currentFrame = new Frame(frameCount);
|
2024-10-19 23:41:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-11 23:46:32 +02:00
|
|
|
|
[Conditional("PROFILING")]
|
2024-10-19 23:41:05 +02:00
|
|
|
|
public static void EndFrame()
|
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
if (!IsRecording)
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-07-05 14:32:58 +02:00
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
foreach (Stack<ScopeNode> scopes in threadLocalScopes.Values)
|
|
|
|
|
{
|
|
|
|
|
if (scopes.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
// Pop the left over root nodes.
|
|
|
|
|
ScopeNode currentScope = scopes.Pop();
|
|
|
|
|
currentScope.End();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clean up the thread-local stack to ensure it's empty for the next frame.
|
|
|
|
|
scopes.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentFrame.End();
|
|
|
|
|
Frames.Enqueue(currentFrame);
|
|
|
|
|
frameCount++;
|
2024-07-05 14:32:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-11 23:46:32 +02:00
|
|
|
|
[Conditional("PROFILING")]
|
2024-10-19 23:41:05 +02:00
|
|
|
|
public static void BeginSample(string label)
|
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
if (!IsRecording)
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
Stack<ScopeNode> scopes = threadLocalScopes.Value; // Get the stack for the current thread
|
|
|
|
|
|
|
|
|
|
if (scopes.Count == 0)
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
// First scope for this thread (new root for this thread)
|
|
|
|
|
ScopeNode rootScopeNode = new ScopeNode($"Thread-{Environment.CurrentManagedThreadId}");
|
|
|
|
|
scopes.Push(rootScopeNode);
|
|
|
|
|
currentFrame.RootNodes.Add(rootScopeNode); // Add root node to the frame list
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a new child under the current top of the stack
|
|
|
|
|
ScopeNode newScope = scopes.Peek().AddChild(label);
|
|
|
|
|
|
|
|
|
|
scopes.Push(newScope); // Push new scope to the thread's stack
|
2024-10-19 23:41:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Conditional("PROFILING")]
|
|
|
|
|
public static void EndSample()
|
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
if (!IsRecording)
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
Stack<ScopeNode> scopes = threadLocalScopes.Value;
|
2024-07-05 14:32:58 +02:00
|
|
|
|
|
2024-10-20 23:17:41 +02:00
|
|
|
|
if (scopes.Count > 0)
|
2024-10-19 23:41:05 +02:00
|
|
|
|
{
|
2024-10-20 23:17:41 +02:00
|
|
|
|
// Only pop if this is not the root node.
|
|
|
|
|
//ScopeNode currentScope = scopes.Count > 1 ? scopes.Pop() : scopes.Peek();
|
|
|
|
|
ScopeNode currentScope = scopes.Pop();
|
|
|
|
|
currentScope.End();
|
2024-10-19 23:41:05 +02:00
|
|
|
|
}
|
2024-07-05 14:32:58 +02:00
|
|
|
|
}
|
|
|
|
|
}
|