1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
namespace Godosa.Core.Diagnostics;
/// <summary>One snapshot of the rolling frame window (game-loop spec §5). Times in ms.</summary>
public readonly record struct FrameStatsSummary(
int Frames,
double Fps,
double AvgMs,
double P50Ms,
double P95Ms,
double P99Ms,
double MaxMs,
double OnePercentLowFps,
int Hitches,
double AvgWorkMs,
double SimTickRate,
double AvgTickMs,
int GpuSamples = 0,
double AvgGpuMs = 0);
/// <summary>
/// Rolling window of render-frame timings (game-loop spec §5). FPS is frames ÷
/// window time, percentiles are nearest-rank over frame times, 1% low is the
/// mean of the slowest 1% of frames, and a hitch is a frame over 2× the median.
/// Buffers are preallocated so recording and summarizing don't allocate.
/// </summary>
public sealed class FrameTimeStats
{
private readonly double[] _intervals;
private readonly double[] _work;
private readonly int[] _ticks;
private readonly double[] _sim;
private readonly double[] _sorted;
private readonly double[] _gpu;
private int _next, _gpuNext, _gpuCount;
public FrameTimeStats(int capacity = 240)
{
_intervals = new double[capacity];
_work = new double[capacity];
_ticks = new int[capacity];
_sim = new double[capacity];
_sorted = new double[capacity];
_gpu = new double[capacity];
}
/// <summary>GPU time of one frame (timer queries report a few frames late, so these
/// roll separately from <see cref="Record"/>).</summary>
public void RecordGpu(double seconds)
{
_gpu[_gpuNext] = seconds;
_gpuNext = (_gpuNext + 1) % Capacity;
if (_gpuCount < Capacity) _gpuCount++;
}
public int Capacity => _intervals.Length;
public int Count { get; private set; }
/// <param name="intervalSeconds">Time since the previous frame started.</param>
/// <param name="workSeconds">CPU time spent inside this frame's render callback.</param>
/// <param name="simTicks">Simulation ticks run this frame.</param>
/// <param name="simSeconds">CPU time those ticks took.</param>
public void Record(double intervalSeconds, double workSeconds, int simTicks, double simSeconds)
{
_intervals[_next] = intervalSeconds;
_work[_next] = workSeconds;
_ticks[_next] = simTicks;
_sim[_next] = simSeconds;
_next = (_next + 1) % Capacity;
if (Count < Capacity) Count++;
}
/// <summary>Copies frame intervals in ms, oldest first; returns how many were written.</summary>
public int CopyIntervalsMs(Span<float> destination)
{
int n = Math.Min(Count, destination.Length);
int start = (_next - n + Capacity) % Capacity;
for (int i = 0; i < n; i++)
destination[i] = (float)(_intervals[(start + i) % Capacity] * 1000.0);
return n;
}
public FrameStatsSummary Summarize()
{
int n = Count;
if (n == 0) return default(FrameStatsSummary) with { GpuSamples = _gpuCount, AvgGpuMs = GpuAverageMs() };
double total = 0, work = 0, sim = 0;
int ticks = 0;
for (int i = 0; i < n; i++)
{
total += _intervals[i];
work += _work[i];
ticks += _ticks[i];
sim += _sim[i];
_sorted[i] = _intervals[i];
}
Array.Sort(_sorted, 0, n);
double median = NearestRank(50, n);
int slowest = Math.Max(1, (int)Math.Ceiling(n * 0.01));
double slowestSum = 0;
for (int i = n - slowest; i < n; i++) slowestSum += _sorted[i];
int hitches = 0;
for (int i = 0; i < n; i++)
if (_intervals[i] > 2 * median) hitches++;
return new FrameStatsSummary(
Frames: n,
Fps: total > 0 ? n / total : 0,
AvgMs: total / n * 1000,
P50Ms: median * 1000,
P95Ms: NearestRank(95, n) * 1000,
P99Ms: NearestRank(99, n) * 1000,
MaxMs: _sorted[n - 1] * 1000,
OnePercentLowFps: slowestSum > 0 ? slowest / slowestSum : 0,
Hitches: hitches,
AvgWorkMs: work / n * 1000,
SimTickRate: total > 0 ? ticks / total : 0,
AvgTickMs: ticks > 0 ? sim / ticks * 1000 : 0,
GpuSamples: _gpuCount,
AvgGpuMs: GpuAverageMs());
}
private double GpuAverageMs()
{
if (_gpuCount == 0) return 0;
double sum = 0;
for (int i = 0; i < _gpuCount; i++) sum += _gpu[i];
return sum / _gpuCount * 1000;
}
/// <summary>Nearest-rank percentile of the first <paramref name="n"/> sorted intervals.</summary>
private double NearestRank(int percent, int n)
{
int rank = Math.Max(1, (int)Math.Ceiling(percent / 100.0 * n));
return _sorted[rank - 1];
}
}
|