nx_perf_lib.nx source
↩ module page · 209 lines · 10491 B
1// nx_perf_lib.nx -- FRAME-BUDGET TELEMETRY, the AAA way, as ONE sovereign ruler (GE32 + GE15, 2026-09-02).
2//
3// Operator: "evaluate at minimum the way AAA teams do -- every frame is a budget that cannot be overspent;
4// frame time and frametime CONSISTENCY, spikes, p50/p95/p99". The estate measured all of that in page
5// JavaScript (?perf=1: p50/p95/p99/jank/worst) and NOTHING read it (gameengine GE15). This lib moves the
6// arithmetic into NishiLang so the ENGINE (wasm), the served HUD and the pacing referee gate all compute the
7// same numbers from the same window -- one ruler, three consumers -- and the page is left with a door call
8// and a display line, which is the interop-only law.
9//
10// WASM-COMPATIBLE BY CONSTRUCTION: no allocation, no syscalls, no floating point. The caller hands in a
11// region of PF_WORDS i64 (an engine arena offset, or a native mmap) and every function is a pure operation
12// over it. Integer milliseconds throughout; the page rounds the rAF interval before it crosses.
13//
14// WINDOW vs CUMULATIVE, deliberately both: a ring buffer reports the RECENT, not the WORST (estate law), so
15// the header carries cumulative counters (frames ever, worst ever, over-budget ever) beside the PF_N-frame
16// window the percentiles are computed over. Percentiles use the nearest-rank method over a 0..PF_CLAMP_MS
17// histogram (rank = ceil(permil x n / 1000)). pf_push preserves legacy capped input; pf_push_raw retains
18// stalls beyond the histogram and resolves their ranks from the raw ring. An EMPTY window answers PF_UNOBSERVED (-1) to every statistic -- a check that reads 0
19// frames as 0 ms would pass on the empty set.
20//
21// COMPOSES nx_frame_budget (the estate's per-frame budget ruler, microseconds): the budget in ms for a target
22// fps is fb_budget_us(fps)/1000 and fits/headroom are fb_fits/fb_headroom_us -- not re-derived here.
23// license_tier: ORIGINAL No hw writes (Rule 26).
24import "_hdl_build/nx_frame_budget.nx"
25
26const PF_N: i64 = 256 // the window: ~4.3 s at 60 Hz, the same order as the page's 600-frame ?perf probe at 10 s
27const PF_HDR: i64 = 8 // header words before the samples (layout below)
28const PF_CLAMP_MS: i64 = 1000 // a frame longer than a second is recorded AT the clamp (and still counts as worst)
29const PF_HBINS: i64 = 1001 // histogram bins 0..PF_CLAMP_MS inclusive
30const PF_WORDS: i64 = 1265 // PF_HDR + PF_N + PF_HBINS -- the region a caller must provide (8 + 256 + 1001)
31const PF_UNOBSERVED: i64 = 0 - 1
32const PF_PERMIL: i64 = 1000
33const PF_US_PER_MS: i64 = 1000
34const PF_SPIKE_MULT: i64 = 2 // the shipped-industry jank definition the page already used: a frame above 2x the window median
35// header slots
36const PF_H_COUNT: i64 = 0 // samples in the window (<= PF_N)
37const PF_H_HEAD: i64 = 1 // next write slot
38const PF_H_TOTAL: i64 = 2 // frames ever pushed
39const PF_H_WORST: i64 = 3 // worst frame ever (ms, clamped)
40const PF_H_OVER: i64 = 4 // frames ever over the budget handed to pf_push
41const PF_H_SUM: i64 = 5 // sum of ms ever pushed (clamped values)
42const PF_H_BUDGET: i64 = 6 // the budget (ms) the last push was judged against
43const PF_H_RESERVED: i64 = 7
44// referee verdicts (three-state: a referee that cannot see must abstain, never acquit)
45const PF_GREEN: i64 = 0
46const PF_RED: i64 = 1
47const PF_ABSTAIN: i64 = 3
48
49func pf_words() -> i64 { return PF_WORDS }
50func pf_window() -> i64 { return PF_N }
51func pf_clamp_ms() -> i64 { return PF_CLAMP_MS }
52
53func pf_init(r: *i64) -> i64 {
54 var i: i64 = 0
55 while i < PF_HDR + PF_N { r[i] = 0; i = i + 1 }
56 return 0
57}
58// SELF-HEAL: a header that cannot be right (a count past the window, a head past the ring) means the region
59// was never initialised or was clobbered; reset rather than index past the ring. Reported through the total
60// (which restarts at 0), never hidden.
61func pf_valid(r: *i64) -> i64 {
62 if r[PF_H_COUNT] < 0 { return 0 }
63 if r[PF_H_COUNT] > PF_N { return 0 }
64 if r[PF_H_HEAD] < 0 { return 0 }
65 if r[PF_H_HEAD] >= PF_N { return 0 }
66 return 1
67}
68func pf_push(r: *i64, ms: i64, budget_ms: i64) -> i64 {
69 var m: i64 = ms
70 if m > PF_CLAMP_MS { m = PF_CLAMP_MS }
71 return pf_push_raw(r, m, budget_ms)
72}
73// Additive entry point: keep legacy callers' cap while enabling truthful raw-frame consumers.
74func pf_push_raw(r: *i64, ms: i64, budget_ms: i64) -> i64 {
75 if pf_valid(r) == 0 { pf_init(r) }
76 var m: i64 = ms
77 if m < 0 { m = 0 }
78 r[PF_HDR + r[PF_H_HEAD]] = m
79 r[PF_H_HEAD] = (r[PF_H_HEAD] + 1) % PF_N
80 if r[PF_H_COUNT] < PF_N { r[PF_H_COUNT] = r[PF_H_COUNT] + 1 }
81 r[PF_H_TOTAL] = r[PF_H_TOTAL] + 1
82 if m > r[PF_H_WORST] { r[PF_H_WORST] = m }
83 r[PF_H_SUM] = r[PF_H_SUM] + m
84 r[PF_H_BUDGET] = budget_ms
85 if budget_ms > 0 { if m > budget_ms { r[PF_H_OVER] = r[PF_H_OVER] + 1 } }
86 return r[PF_H_COUNT]
87}
88func pf_n(r: *i64) -> i64 { return r[PF_H_COUNT] }
89func pf_total(r: *i64) -> i64 { return r[PF_H_TOTAL] }
90func pf_worst_ever(r: *i64) -> i64 { if r[PF_H_TOTAL] == 0 { return PF_UNOBSERVED } return r[PF_H_WORST] }
91func pf_over(r: *i64) -> i64 { return r[PF_H_OVER] }
92func pf_budget(r: *i64) -> i64 { return r[PF_H_BUDGET] }
93// mean over everything ever pushed, in tenths of a ms so the HUD can print one decimal without floats
94func pf_mean_x10(r: *i64) -> i64 { if r[PF_H_TOTAL] == 0 { return PF_UNOBSERVED } return r[PF_H_SUM]*10 / r[PF_H_TOTAL] }
95// the i-th OLDEST sample in the window (0 = oldest), PF_UNOBSERVED past the count
96func pf_sample(r: *i64, i: i64) -> i64 {
97 let n: i64 = r[PF_H_COUNT]
98 if i < 0 { return PF_UNOBSERVED }
99 if i >= n { return PF_UNOBSERVED }
100 var start: i64 = r[PF_H_HEAD] - n
101 if start < 0 { start = start + PF_N }
102 return r[PF_HDR + (start + i) % PF_N]
103}
104// histogram of the window into the tail of the region (rebuilt per query: PF_N adds, PF_HBINS clears)
105func pf_hist(r: *i64) -> i64 {
106 let h: i64 = PF_HDR + PF_N
107 var b: i64 = 0
108 while b < PF_HBINS { r[h + b] = 0; b = b + 1 }
109 let n: i64 = r[PF_H_COUNT]
110 var i: i64 = 0
111 while i < n {
112 var v: i64 = r[PF_HDR + i]
113 if v > PF_CLAMP_MS { v = PF_CLAMP_MS }
114 if v >= 0 { if v <= PF_CLAMP_MS { r[h + v] = r[h + v] + 1 } }
115 i = i + 1
116 }
117 return n
118}
119// Only queried when the requested rank lands in the overflow bin. Search values, not milliseconds,
120// so a long suspend interval never implies a loop proportional to the pause duration.
121func pf_tail_rank(r: *i64, rank: i64) -> i64 {
122 let n: i64 = r[PF_H_COUNT]
123 var lo: i64 = PF_CLAMP_MS
124 var hi: i64 = lo
125 var i: i64 = 0
126 while i < n { if r[PF_HDR+i] > hi { hi = r[PF_HDR+i] }; i = i + 1 }
127 while lo < hi {
128 let mid: i64 = lo + (hi-lo)/2
129 var below: i64 = 0
130 i = 0
131 while i < n { if r[PF_HDR+i] <= mid { below = below + 1 }; i = i + 1 }
132 if below >= rank { hi = mid } else { lo = mid + 1 }
133 }
134 return lo
135}
136// nearest-rank percentile of the window: the smallest ms value whose cumulative count reaches
137// ceil(permil x n / 1000). p50 of 1..100 is 50, p99 is 99, p100 is 100.
138func pf_pct(r: *i64, permil: i64) -> i64 {
139 let n: i64 = pf_hist(r)
140 if n <= 0 { return PF_UNOBSERVED }
141 var rank: i64 = (permil * n + PF_PERMIL - 1) / PF_PERMIL
142 if rank < 1 { rank = 1 }
143 if rank > n { rank = n }
144 let h: i64 = PF_HDR + PF_N
145 var cum: i64 = 0
146 var b: i64 = 0
147 while b < PF_HBINS {
148 cum = cum + r[h + b]
149 if cum >= rank { if b == PF_CLAMP_MS { return pf_tail_rank(r, rank) }; return b }
150 b = b + 1
151 }
152 return PF_CLAMP_MS
153}
154func pf_worst(r: *i64) -> i64 { return pf_pct(r, PF_PERMIL) }
155// frames in the window above PF_SPIKE_MULT x the window median -- the jank count
156func pf_spikes(r: *i64) -> i64 {
157 let n: i64 = r[PF_H_COUNT]
158 if n <= 0 { return PF_UNOBSERVED }
159 let med: i64 = pf_pct(r, 500)
160 let bar: i64 = med * PF_SPIKE_MULT
161 var c: i64 = 0
162 var i: i64 = 0
163 while i < n {
164 if r[PF_HDR + i] > bar { c = c + 1 }
165 i = i + 1
166 }
167 return c
168}
169// ABSOLUTE long-frame axis, beside the RELATIVE jank multiple above. The W3C Long Animation Frames standard
170// defines a long frame as one whose duration reaches 50 milliseconds and discards timing below it (ref
171// ge-loaf on the gameengine board); the field judges a web page on that absolute bar regardless of our own
172// budget, so the window counts it in the same units. UNOBSERVED on an empty window, like every statistic here.
173const PF_LONG_FRAME_MS: i64 = 50
174func pf_long(r: *i64) -> i64 {
175 let n: i64 = r[PF_H_COUNT]
176 if n <= 0 { return PF_UNOBSERVED }
177 var c: i64 = 0
178 var i: i64 = 0
179 while i < n {
180 if r[PF_HDR + i] >= PF_LONG_FRAME_MS { c = c + 1 }
181 i = i + 1
182 }
183 return c
184}
185func pf_spikes_permil(r: *i64) -> i64 {
186 let n: i64 = r[PF_H_COUNT]
187 if n <= 0 { return PF_UNOBSERVED }
188 return pf_spikes(r) * PF_PERMIL / n
189}
190// ---- the budget half composes the incumbent ruler ----
191func pf_budget_ms(fps: i64) -> i64 { if fps <= 0 { return PF_UNOBSERVED } return fb_budget_us(fps) / PF_US_PER_MS }
192func pf_fits(ms: i64, budget_ms: i64) -> i64 { return fb_fits(ms * PF_US_PER_MS, budget_ms * PF_US_PER_MS) }
193
194// ---- THE PACING REFEREE (GE15): three-state, bands as permil OF THE BUDGET so one conf serves every display ----
195// p50 <= budget x p50_permil/1000, p95 <= budget x p95_permil/1000, p99 <= budget x p99_permil/1000,
196// jank permil <= jank_permil_max. ABSTAINS below min_n samples: a pacing check that passes on zero frames is
197// the passes-on-the-empty-set defect wearing a deploy costume. Returns PF_GREEN / PF_RED / PF_ABSTAIN; the
198// failing conjunct is written into why[0] (1 p50, 2 p95, 3 p99, 4 jank, 0 none) so the message can name it.
199func pf_referee(r: *i64, budget_ms: i64, min_n: i64, p50_permil: i64, p95_permil: i64, p99_permil: i64, jank_permil_max: i64, why: *i64) -> i64 {
200 why[0] = 0
201 let n: i64 = r[PF_H_COUNT]
202 if n < min_n { return PF_ABSTAIN }
203 if budget_ms <= 0 { return PF_ABSTAIN }
204 if pf_pct(r, 500) * PF_PERMIL > budget_ms * p50_permil { why[0] = 1; return PF_RED }
205 if pf_pct(r, 950) * PF_PERMIL > budget_ms * p95_permil { why[0] = 2; return PF_RED }
206 if pf_pct(r, 990) * PF_PERMIL > budget_ms * p99_permil { why[0] = 3; return PF_RED }
207 if pf_spikes_permil(r) > jank_permil_max { why[0] = 4; return PF_RED }
208 return PF_GREEN
209}