nx_frameab.nx
buildroot/runtime/nx_frameab.nx
about
nx_frameab.nx -- THE FIRST REAL A/B ON THE ESTATE: two frametrace windows, three axes, one corrected verdict.
WHY THIS ORGAN AND WHY NOW. nx_abstat_lib shipped the two-arm contrast arithmetic, and the acceptance panel
supplies exactly ONE voting lens (a single percept scalar over a whole frame), so K=1 and the family-wise
correction was a no-op with nothing to correct across. This organ is the wiring that fixes that for the
PERFORMANCE subject, and it needs no new capture, no lens and no calibration corpus, because the arms
ALREADY EXIST: knowledge/store/frametrace- is append-only and its last column carries EVERY INDIVIDUAL
FRAME TIME of the window -- 256 replicated samples per row against a floor of 30.
THE CONFOUND GUARD IS THE POINT, NOT A COURTESY. The live plane holds beach rows at budget 94, 84 and 6 ms.
Budget is derived from the display refresh, so ROWS AT DIFFERENT BUDGETS CAME FROM DIFFERENT MACHINES, and
contrasting them would measure the hardware while reporting a verdict about the build. This organ REFUSES
such a pair by name (CONFOUNDED-BUDGET) instead of returning a confident number.
* A comparison whose arms differ in a dimension you did not intend to vary is not an experiment, and it
fails in the flattering direction: a faster machine reads as a better build.
THREE AXES, ALL PER-FRAME, ALL FROM THE SAME WINDOW, SO ALL WITH REAL WITHIN-ARM VARIANCE:
0 frame time ms -- LOWER is better
1 over-budget indicator -- LOWER is better (1 when the frame missed its budget)
2 jank indicator -- LOWER is better (1 when the frame exceeded 2x its own window median,
the shipped-industry definition nx_perf_lib already uses)
K=3 puts the Bonferroni critical t at 2447 rather than 2007: three correlated views of one window would
otherwise give three chances to find an improvement. Bonferroni is conservative under ANY correlation,
which is exactly why it is the right correction for axes drawn from the same frames.
THE MINIMUM DETECTABLE EFFECT IS DERIVED, NOT PICKED. Frame samples are INTEGER MILLISECONDS, so an effect
below 1 ms is beneath the instrument's own resolution and must not be called an improvement however
significant it is. That is the MDE for axis 0 and its provenance is printed. The indicator axes are already
rates, so their MDE is 0 and BELOW-MDE can never fire on them -- stated here rather than left to be found.
KNOWN LIMIT, NAMED RATHER THAN HIDDEN: a rate axis whose arms are BOTH constant (every frame over budget in
one window, none in the other) has an undefined t statistic, so this organ reports INSUFFICIENT with the
reason ZERO-VARIANCE even though the underlying shift is real and large. That is the safe direction --
abstain, never acquit -- but the right instrument for a rate is a two-proportion test, which this organ
does not have. Measured live on its own fixture: a 100-percent-to-0 over-budget shift read INSUFF.
COMPOSES, NEVER RE-IMPLEMENTS: fpc_row_frames for the CSV (the incumbent parser, with its own bad-input
handling), pf_sample/pf_n/pf_budget/pf_pct for the ring, ab_push/ab_axis_verdict/ab_family_verdict for the
statistics. This organ owns only the SELECTION of two rows and the refusal.
dependencies 3 imports · 1 importers
imports: nx_syscalls.nxnx_frame_pacing_lib.nxnx_abstat_lib.nx
imported by: nx_frameab_gate.nx
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 49 | const FAB_ROWCAP: i64 = 65536 |
| 58 | const FAB_AXES: i64 = 3 |
| 59 | const FAB_AX_FRAME: i64 = 0 |
| 60 | const FAB_AX_OVER: i64 = 1 |
| 61 | const FAB_AX_JANK: i64 = 2 |
| 62 | const FAB_MDE_MS_DEFAULT: i64 = 1 // DERIVED: samples are integer ms, so 1 ms is the instrument resolution |
| 63 | const FAB_JANK_MULT: i64 = 2 // nx_perf_lib PF_SPIKE_MULT, the shipped jank definition |
| 64 | const FAB_MEDIAN_PERMIL: i64 = 500 |
| 65 | const FAB_US_PER_MS: i64 = 1000 // a window MEAN is carried in microseconds so it survives a 1 ms frame timer |
| 66 | const FAB_PERMIL: i64 = 1000 |
| 67 | const FAB_NUMCAP: i64 = 32 |
| 69 | const FAB_EXIT_OK: i64 = 0 |
| 70 | const FAB_EXIT_REGRESSED: i64 = 1 |
| 71 | const FAB_EXIT_USAGE: i64 = 2 |
| 72 | const FAB_EXIT_INSUFF: i64 = 3 |
| 73 | const FAB_EXIT_REFUSED: i64 = 4 |
| 74 | const FAB_EXIT_IO: i64 = 5 |
functions
| 76 | func fab_p(s: *u8) -> i64 { sys_write(1, s, fpc_slen(s)); return 0 } |
| 77 | func fab_pn(v: i64) -> i64 |
| 91 | func fab_kv(k: *u8, v: i64) -> i64 { fab_p(k); fab_p("=" as *u8); fab_pn(v); fab_p("\n" as *u8); return 0 } |
| 92 | func fab_has_prefix(s: *u8, n: i64, p: *u8) -> i64 |
| 103 | func fab_last_two(plane: *u8, world: *u8, out_b: *u8, out_a: *u8, rows_seen: *i64) -> i64 |
| 143 | func fab_confounded(bud_b: i64, bud_a: i64) -> i64 |
| 148 | func fab_over_of(ms: i64, budget: i64) -> i64 { if ms > budget { return 1 } return 0 } |
| 149 | func fab_jank_of(ms: i64, median: i64) -> i64 |
| 158 | func fab_push_arm(reg: *i64, arm: i64, r: *i64, budget: i64) -> i64 |
| 180 | func fab_push_window(reg: *i64, arm: i64, r: *i64, budget: i64) -> i64 |
| 201 | func fab_axis_name(a: i64) -> *u8 called by 1: main |
| 206 | func fab_ax_word(v: i64) -> *u8 called by 1: main |
| 213 | func fab_reason_word(v: i64) -> *u8 called by 1: main |
| 222 | func main(argc: i64, argv: *i64) -> i64 |