PerformanceDiagnostics.cs

Static performance sampler for playtests. Records frame, controller, simulation and world step times into fixed ring buffers and returns average, P95 and max statistics from Snapshot.

using System;
using System.Diagnostics;

namespace Socer;

// Opt-in measurements for playtests. The hot path only checks Enabled when idle.
public static class PerformanceDiagnostics
{
    const int Capacity=600;
    static readonly double[] frameMs=new double[Capacity];
    static readonly double[] controllerMs=new double[Capacity];
    static readonly double[] simulationMs=new double[Capacity];
    static readonly double[] worldMs=new double[Capacity];
    static int frames,worldFrames;
    public static bool Enabled {get;private set;}

    public static void Start()
    {
        Array.Clear(frameMs);Array.Clear(controllerMs);Array.Clear(simulationMs);Array.Clear(worldMs);
        frames=worldFrames=0;Enabled=true;
    }
    public static void Stop()=>Enabled=false;
    public static long Begin()=>Enabled?Stopwatch.GetTimestamp():0;
    public static double Milliseconds(long started)=>started==0?0:
        (Stopwatch.GetTimestamp()-started)*1000.0/Stopwatch.Frequency;
    public static void RecordFrame(float delta,long started,double simulation)
    {
        // Long frames are precisely the regressions this sampler must report.
        // Filtering >500 ms hid a continuous model recompile loop at 1 FPS.
        if(!Enabled || !float.IsFinite(delta) || delta<=0)return;
        int index=frames++%Capacity;
        frameMs[index]=delta*1000;
        controllerMs[index]=Milliseconds(started);
        simulationMs[index]=simulation;
    }
    public static void RecordWorld(long started)
    {
        if(!Enabled)return;
        worldMs[worldFrames++%Capacity]=Milliseconds(started);
    }
    static object Describe(double[] values,int count)
    {
        count=Math.Min(count,Capacity);
        if(count==0)return new{Average=0d,P95=0d,Maximum=0d};
        var sample=new double[count];Array.Copy(values,sample,count);Array.Sort(sample);
        double sum=0;foreach(double value in sample)sum+=value;
        return new{Average=Math.Round(sum/count,2),P95=Math.Round(sample[(int)Math.Ceiling(count*.95)-1],2),
            Maximum=Math.Round(sample[^1],2)};
    }
    public static object Snapshot()=>new
    {
        Enabled,Frames=Math.Min(frames,Capacity),WorldFrames=Math.Min(worldFrames,Capacity),
        Frame=Describe(frameMs,frames),Controller=Describe(controllerMs,frames),
        Simulation=Describe(simulationMs,frames),World=Describe(worldMs,worldFrames)
    };
}