-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFrameStatsMath.cs
More file actions
143 lines (130 loc) · 5.46 KB
/
Copy pathFrameStatsMath.cs
File metadata and controls
143 lines (130 loc) · 5.46 KB
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
140
141
142
143
// Compile-included by both server (net9.0, for unit tests) and client (net471, for the
// in-raid recorder). Pure math — no Unity, no SPT types.
namespace CompoundingPerf;
/// <summary>
/// Frame-time statistics for benchmark dumps. All inputs are per-frame durations in
/// SECONDS (Unity's unscaledDeltaTime); outputs use FPS and milliseconds, matching how
/// benchmarking tools report (avg FPS, 1% low FPS, hitch counts).
/// </summary>
public static class FrameStatsMath
{
// Plain class, not a positional record — this file compile-includes into the net471
// client, which lacks the IsExternalInit type that record init-setters require.
public sealed class FrameStatsResult
{
public FrameStatsResult(
int frames, double durationSec, double avgFps, double onePercentLowFps,
double pointOnePercentLowFps, int hitchesOver50Ms, int hitchesOver100Ms,
double maxFrameMs, double maxFrameAtSec)
{
Frames = frames;
DurationSec = durationSec;
AvgFps = avgFps;
OnePercentLowFps = onePercentLowFps;
PointOnePercentLowFps = pointOnePercentLowFps;
HitchesOver50Ms = hitchesOver50Ms;
HitchesOver100Ms = hitchesOver100Ms;
MaxFrameMs = maxFrameMs;
MaxFrameAtSec = maxFrameAtSec;
}
public int Frames { get; }
public double DurationSec { get; }
public double AvgFps { get; }
public double OnePercentLowFps { get; }
public double PointOnePercentLowFps { get; }
public int HitchesOver50Ms { get; }
public int HitchesOver100Ms { get; }
public double MaxFrameMs { get; }
/// <summary>Seconds from recording start (warmup included) to the start of the
/// worst frame. A value near warmup+DurationSec means the spike is the
/// end-of-raid teardown, not a mid-raid freeze.</summary>
public double MaxFrameAtSec { get; }
}
// NOTE: a Brendan-Gregg m-value multimodality flag was prototyped here (BDN's
// heuristic) and CUT — empirically it reads backwards on frame data: a hitch tail
// blows out the histogram range so the main mode collapses to one bin (scores
// "unimodal"), while clean bell data drowns in fine-bin sampling noise (scores
// "multimodal"). Robust percentile-based binning would be needed; not worth it for
// an optional warning. Verified before shipping rather than after.
/// <summary>
/// Compute summary stats over per-frame durations (seconds). Returns null when there
/// are too few samples to be meaningful (under 2 seconds' worth at 30fps).
/// <paramref name="warmupSkipSeconds"/> discards the leading samples covering that
/// much wall time — the spawn-in/asset-streaming window produces multi-second frames
/// that say nothing about gameplay performance and would dominate the lows.
/// </summary>
public static FrameStatsResult? Compute(float[] frameSeconds, int count, double warmupSkipSeconds = 0)
{
if (frameSeconds is null)
{
return null;
}
// Trim warmup: advance past leading frames until we've consumed the skip window.
var start = 0;
if (warmupSkipSeconds > 0)
{
double consumed = 0;
while (start < count && consumed < warmupSkipSeconds)
{
consumed += frameSeconds[start];
start++;
}
}
if (count - start < 60)
{
return null;
}
// Re-sum the warmup window so MaxFrameAtSec is an offset from recording start,
// which matches how a human remembers the raid ("it froze ~2 minutes in").
double warmupConsumed = 0;
for (var i = 0; i < start; i++)
{
warmupConsumed += frameSeconds[i];
}
double total = 0;
var hitch50 = 0;
var hitch100 = 0;
double maxSec = 0;
double maxAtSec = warmupConsumed;
for (var i = start; i < count; i++)
{
var s = frameSeconds[i];
if (s > 0.100) { hitch100++; hitch50++; }
else if (s > 0.050) { hitch50++; }
if (s > maxSec) { maxSec = s; maxAtSec = warmupConsumed + total; }
total += s;
}
if (total <= 0)
{
return null;
}
// Sort a copy descending to find the worst tails. "1% low FPS" is the standard
// benchmark metric: the average frame rate across the slowest 1% of frames.
var kept = count - start;
var sorted = new float[kept];
Array.Copy(frameSeconds, start, sorted, 0, kept);
Array.Sort(sorted);
Array.Reverse(sorted); // index 0 = slowest frame
return new FrameStatsResult(
kept,
total,
kept / total,
TailFps(sorted, kept, 0.01),
TailFps(sorted, kept, 0.001),
hitch50,
hitch100,
maxSec * 1000.0,
maxAtSec);
}
/// <summary>Average FPS across the slowest <paramref name="fraction"/> of frames.</summary>
private static double TailFps(float[] sortedDesc, int count, double fraction)
{
var n = Math.Max(1, (int)(count * fraction));
double sum = 0;
for (var i = 0; i < n; i++)
{
sum += sortedDesc[i];
}
return n / sum;
}
}