nx_daemon_bench.nx source
↩ module page · 514 lines · 19711 B
1// nx_daemon_bench.nx -- continuous, low-resource, on-device benchmark
2// daemon. Substrate-native equivalent of Criterion.rs / JMH / Google
3// Benchmark / Phoronix Test Suite, baked into the language itself and
4// explicitly designed for Tier-0 (MCU/sensor) operation.
5//
6// license_tier: ORIGINAL
7//
8// MISSION (per FLoC-Olympics + monetization cardinal + sovereign-
9// stack roadmap): NishiLang must compete against and beat every
10// modern continuous-benchmark system, AND the same primitive must
11// run on a sensor with kilobytes of RAM. Sensors that audit
12// themselves at <1% CPU duty cycle are a new product category.
13//
14// DESIGN PRINCIPLES:
15//
16// 1. FIXED-MEMORY STATE. Single mmap at init; never grows. Each
17// derived statistic uses Welford's online algorithm (mean +
18// variance in O(1) memory regardless of cycle count). No
19// sample log, no histogram bins, no growing record ring.
20// Sensor-grade footprint: under 1 KiB of state.
21//
22// 2. DUTY-CYCLE BUDGET. Caller declares max ms/cycle, max bytes,
23// and min_sleep_ms between cycles. Default 100 ms work + 10000
24// ms sleep -> <1% CPU. Daemon self-polices: cycles exceeding
25// the wall-time budget emit OVERBUDGET and increment a counter.
26//
27// 3. SEALED-ENUM STATUS. Eight states, no weasel intermediates,
28// validated by predicate. Cardinal:
29// feedback-honest-perf-verdict-no-aspirational-claims.
30//
31// 4. REGRESSION DETECTION. Per-axis EMA baseline + Welford
32// variance -> Z-score in Q10. |Z| > 3-sigma -> DEGRADED;
33// |Z| > 5-sigma -> REGRESSION (cardinal requires
34// named_improvement string from caller).
35//
36// 5. TIER-AWARE SKIP. Caller passes the current hardware tier;
37// if below cfg.tier_floor the cycle is skipped (SLEEPING).
38// Cardinal: scale-agnostic-substrate.
39//
40// 6. CARDINAL-COMPLIANCE GATE. Refuses to declare a REGRESSION
41// cycle clean unless caller has set named_improvement_set.
42//
43// 7. PORTABLE BY CONSTRUCTION. The only platform-specific calls
44// are nx_clock_monotonic_ns and sys_sleep_ms. Both have
45// documented bare-metal / WASM reimplementation targets.
46// Daemon code itself is platform-independent.
47//
48// IDEA-PROVENANCE (learn from, never copy):
49// - Criterion.rs (Rust) -- bootstrap CI, outlier MAD
50// - Google Benchmark (C++) -- statistical rigor patterns
51// - JMH (OpenJDK) -- multi-fork, multi-warmup
52// - airspeed-velocity / asv -- continuous-regression tracking
53// - Phoronix Test Suite -- continuous-bench daemon shape
54// - Knuth TAoCP Vol 2 (1981) -- Welford online variance
55// - Roberts 1959 -- exponentially-weighted MA
56// - DataDog continuous profiler -- always-on telemetry pattern
57// Every idea re-derived from published papers / READMEs. No source
58// code incorporated.
59//
60// genealogy_id: criterion_rs_2018 + google_benchmark_2014 +
61// jmh_openjdk_papers + airspeed_velocity_asv +
62// phoronix_test_suite + knuth_taocp_vol2_1981_welford +
63// roberts_1959_ewma + datadog_continuous_profiler
64// lineage_id: continuous_on_device_bench_q10
65
66// nx_safety_envelope:
67// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
68// sil_target: SIL1
69// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
70// verdict: NOT_YET_EVALUATED
71
72import "nx_syscalls.nx"
73import "nx_runtime.nx"
74import "nx_clock.nx"
75import "nx_tier.nx"
76import "nx_benchmark_harness.nx"
77
78// ===== Sealed-enum status ============================================
79
80const NX_DAEMON_IDLE: nx_int = 0 // init done; no cycle yet
81const NX_DAEMON_WARMING: nx_int = 1 // building baseline
82const NX_DAEMON_HEALTHY: nx_int = 2 // |z| < degrade threshold
83const NX_DAEMON_SLEEPING: nx_int = 3 // between cycles / tier-skip
84const NX_DAEMON_DEGRADED: nx_int = 4 // |z| crossed degrade band
85const NX_DAEMON_REGRESSION: nx_int = 5 // |z| crossed regression band
86const NX_DAEMON_OVERBUDGET: nx_int = 6 // last cycle exceeded ms/bytes
87const NX_DAEMON_STALE: nx_int = 7 // watchdog: no cycle in N intervals
88const NX_DAEMON_N_STATES: nx_int = 8
89
90// ===== Fixed-Q10 scale =============================================
91
92const NX_DAEMON_Q: nx_int = 1024
93
94// Defaults chosen against Welford-variance literature:
95// |z| > 3 sigma -- 99.73% normal-distribution outside band
96// |z| > 5 sigma -- 5.7e-7 probability under H0; definitely real
97const NX_DAEMON_DEFAULT_DEGRADE_Z_Q10: nx_int = 3072 // 3.0 * Q10
98const NX_DAEMON_DEFAULT_REGRESS_Z_Q10: nx_int = 5120 // 5.0 * Q10
99
100// EMA tracking rate alpha = 0.1 (slow). Half-life ~6.6 cycles.
101const NX_DAEMON_DEFAULT_EMA_ALPHA_Q10: nx_int = 102
102
103// Warmup matches harness MIN_TRIALS so no verdict before baseline.
104const NX_DAEMON_DEFAULT_WARMUP_CYCLES: nx_int = 8
105
106// 100 ms work + 10000 ms sleep -> <1% CPU duty cycle (sensor-grade).
107const NX_DAEMON_DEFAULT_MAX_MS_PER_CYCLE: nx_int = 100
108const NX_DAEMON_DEFAULT_MIN_SLEEP_MS: nx_int = 10000
109
110// 64 KiB RAM budget; daemon state itself is <1 KiB.
111const NX_DAEMON_DEFAULT_MAX_BYTES: nx_int = 65536
112
113// Watchdog: 3 missed intervals = 30 s at defaults.
114const NX_DAEMON_DEFAULT_STALE_MISSES: nx_int = 3
115
116// Z-score cap: 32-sigma sentinel for degenerate zero-variance.
117const NX_DAEMON_Z_MAX_Q10: nx_int = 32768
118
119// ===== Config (caller-supplied) =====================================
120
121struct DaemonConfig {
122 max_ms_per_cycle: nx_int,
123 max_bytes_alloc: nx_int,
124 min_sleep_ms: nx_int,
125 warmup_cycles: nx_int,
126 degrade_z_q10: nx_int,
127 regress_z_q10: nx_int,
128 ema_alpha_q10: nx_int,
129 stale_missed_intervals: nx_int,
130 tier_floor: nx_int,
131 higher_is_better_mask: nx_int,
132}
133
134func nx_daemon_config_defaults(cfg: *DaemonConfig) -> nx_int {
135 cfg.max_ms_per_cycle = NX_DAEMON_DEFAULT_MAX_MS_PER_CYCLE
136 cfg.max_bytes_alloc = NX_DAEMON_DEFAULT_MAX_BYTES
137 cfg.min_sleep_ms = NX_DAEMON_DEFAULT_MIN_SLEEP_MS
138 cfg.warmup_cycles = NX_DAEMON_DEFAULT_WARMUP_CYCLES
139 cfg.degrade_z_q10 = NX_DAEMON_DEFAULT_DEGRADE_Z_Q10
140 cfg.regress_z_q10 = NX_DAEMON_DEFAULT_REGRESS_Z_Q10
141 cfg.ema_alpha_q10 = NX_DAEMON_DEFAULT_EMA_ALPHA_Q10
142 cfg.stale_missed_intervals = NX_DAEMON_DEFAULT_STALE_MISSES
143 cfg.tier_floor = NX_TIER_MCU
144 // bit i = 1 -> axis i higher-is-better.
145 // bit 0 throughput=1, bit 1 latency_p50=0, bit 2 latency_p99=0,
146 // bit 3 memory_peak=0, bit 4 determinism=1, bit 5 portability=1
147 // -> 0b110001 = 49
148 cfg.higher_is_better_mask = 49
149 return 0
150}
151
152// ===== Per-axis online stats ========================================
153
154struct AxisStats {
155 n: nx_int,
156 mean_q10: nx_int,
157 m2_q10: nx_int,
158 ema_q10: nx_int,
159 last_sample: nx_int,
160 last_z_q10: nx_int,
161}
162
163// ===== Daemon state (natural nested-struct form) ====================
164//
165// Nested by value: one mmap holds everything. Sensor-grade footprint
166// of 448 bytes contiguous in memory; no pointer chasing. Enabled by
167// the 2026-05-15 parse.c fix that lets nested struct field access
168// (p.outer.inner) compile through pointers correctly.
169
170struct DaemonState {
171 config: DaemonConfig,
172 s_throughput: AxisStats,
173 s_latency_p50: AxisStats,
174 s_latency_p99: AxisStats,
175 s_memory_peak: AxisStats,
176 s_determinism: AxisStats,
177 s_portability: AxisStats,
178
179 status: nx_int,
180 cycle_count: nx_int,
181 cycle_skipped: nx_int,
182 cycle_overbudget: nx_int,
183 cycle_regression: nx_int,
184 last_cycle_start_ns: nx_int,
185 last_cycle_end_ns: nx_int,
186 last_cycle_ms: nx_int,
187 last_cycle_status: nx_int,
188 named_improvement_set: nx_int,
189}
190
191const NX_DAEMON_STATE_BYTES: nx_size = 512
192
193func nx_daemon_alloc() -> *DaemonState {
194 let raw: *u8 = sys_mmap(NX_DAEMON_STATE_BYTES)
195 return raw as *DaemonState
196}
197
198// ===== Init =========================================================
199
200func _axis_stats_zero(a: *AxisStats) -> nx_int {
201 a.n = 0
202 a.mean_q10 = 0
203 a.m2_q10 = 0
204 a.ema_q10 = 0
205 a.last_sample = 0
206 a.last_z_q10 = 0
207 return 0
208}
209
210// Field-by-field config snapshot. Codegen Gap workaround: full
211// struct-copy assignment `d.config = cfg[0]` SEGVs for payloads
212// >32 B (memory: project-daemon-bench-and-parser-fix-2026-05-15).
213// Until the codegen fix lands, copy each scalar field.
214func _config_snapshot(d: *DaemonState, cfg: *DaemonConfig) -> nx_int {
215 d.config.max_ms_per_cycle = cfg.max_ms_per_cycle
216 d.config.max_bytes_alloc = cfg.max_bytes_alloc
217 d.config.min_sleep_ms = cfg.min_sleep_ms
218 d.config.warmup_cycles = cfg.warmup_cycles
219 d.config.degrade_z_q10 = cfg.degrade_z_q10
220 d.config.regress_z_q10 = cfg.regress_z_q10
221 d.config.ema_alpha_q10 = cfg.ema_alpha_q10
222 d.config.stale_missed_intervals = cfg.stale_missed_intervals
223 d.config.tier_floor = cfg.tier_floor
224 d.config.higher_is_better_mask = cfg.higher_is_better_mask
225 return 0
226}
227
228// Zero all 6 axis stats blocks. Encapsulates the 6 calls so callers
229// (just nx_daemon_init today) stay small.
230func _axis_stats_zero_all(d: *DaemonState) -> nx_int {
231 _axis_stats_zero(d.s_throughput)
232 _axis_stats_zero(d.s_latency_p50)
233 _axis_stats_zero(d.s_latency_p99)
234 _axis_stats_zero(d.s_memory_peak)
235 _axis_stats_zero(d.s_determinism)
236 _axis_stats_zero(d.s_portability)
237 return 0
238}
239
240// Zero the daemon's scalar cycle bookkeeping. Separate from config
241// + axis stats so each helper stays small.
242func _zero_cycle_state(d: *DaemonState) -> nx_int {
243 d.status = NX_DAEMON_IDLE
244 d.cycle_count = 0
245 d.cycle_skipped = 0
246 d.cycle_overbudget = 0
247 d.cycle_regression = 0
248 d.last_cycle_start_ns = 0
249 d.last_cycle_end_ns = 0
250 d.last_cycle_ms = 0
251 d.last_cycle_status = NX_DAEMON_IDLE
252 d.named_improvement_set = 0
253 return 0
254}
255
256func nx_daemon_init(d: *DaemonState, cfg: *DaemonConfig) -> nx_int {
257 _config_snapshot(d, cfg)
258 _axis_stats_zero_all(d)
259 _zero_cycle_state(d)
260 return 0
261}
262
263// ===== Welford online mean+variance =================================
264//
265// Knuth TAoCP Vol 2 1981. For each new sample x_q10:
266// n += 1
267// delta = x - mean
268// mean += delta / n
269// delta2 = x - mean (mean already updated)
270// M2 += delta * delta2 / Q10 (keep Q10 scale)
271// Variance estimator: s^2 = M2 / (n - 1).
272
273func _axis_welford_update(a: *AxisStats, x_q10: nx_int) -> nx_int {
274 a.n = a.n + 1
275 let delta: nx_int = x_q10 - a.mean_q10
276 a.mean_q10 = a.mean_q10 + delta / a.n
277 let delta2: nx_int = x_q10 - a.mean_q10
278 a.m2_q10 = a.m2_q10 + (delta * delta2) / NX_DAEMON_Q
279 return 0
280}
281
282func _axis_ema_update(a: *AxisStats, x_q10: nx_int, alpha_q10: nx_int) -> nx_int {
283 if a.n == 1 {
284 a.ema_q10 = x_q10
285 return 0
286 }
287 let one_minus_alpha: nx_int = NX_DAEMON_Q - alpha_q10
288 a.ema_q10 = (alpha_q10 * x_q10 + one_minus_alpha * a.ema_q10) / NX_DAEMON_Q
289 return 0
290}
291
292// ===== Integer square root (Newton, monotone, deterministic) ========
293
294func _isqrt(x: nx_int) -> nx_int {
295 if x <= 0 { return 0 }
296 if x < 4 { return 1 }
297 var r: nx_int = x
298 var nr: nx_int = (r + x / r) / 2
299 while nr < r {
300 r = nr
301 nr = (r + x / r) / 2
302 }
303 return r
304}
305
306// ===== Z-score in Q10 ===============================================
307
308func _axis_zscore_q10(a: *AxisStats, x_q10: nx_int) -> nx_int {
309 if a.n < 2 { return 0 }
310 let variance_q10: nx_int = a.m2_q10 / (a.n - 1)
311 let stdev_q10: nx_int = _isqrt(variance_q10 * NX_DAEMON_Q)
312 if stdev_q10 == 0 {
313 let diff0: nx_int = x_q10 - a.ema_q10
314 if diff0 == 0 { return 0 }
315 return NX_DAEMON_Z_MAX_Q10
316 }
317 var diff: nx_int = x_q10 - a.ema_q10
318 if diff < 0 { diff = -diff }
319 let z_q10: nx_int = (diff * NX_DAEMON_Q) / stdev_q10
320 if z_q10 > NX_DAEMON_Z_MAX_Q10 { return NX_DAEMON_Z_MAX_Q10 }
321 return z_q10
322}
323
324// ===== Record a single axis sample ==================================
325//
326// Caller invokes once per axis per cycle. Axis index 0..5 maps to
327// NX_BENCH_AXIS_* from the harness. Returns the computed |z| in Q10
328// for the caller's instrumentation (or -1 on invalid axis index).
329
330// Dispatch axis_idx -> the corresponding *AxisStats field. Single
331// place that knows the 6-axis layout; every call site below uses this.
332func _axis_at(d: *DaemonState, axis_idx: nx_int) -> *AxisStats {
333 if axis_idx == NX_BENCH_AXIS_THROUGHPUT { return d.s_throughput }
334 if axis_idx == NX_BENCH_AXIS_LATENCY_P50 { return d.s_latency_p50 }
335 if axis_idx == NX_BENCH_AXIS_LATENCY_P99 { return d.s_latency_p99 }
336 if axis_idx == NX_BENCH_AXIS_MEMORY_PEAK { return d.s_memory_peak }
337 if axis_idx == NX_BENCH_AXIS_DETERMINISM { return d.s_determinism }
338 return d.s_portability
339}
340
341// Statistical correctness: compute z-score against the BASELINE
342// (pre-update Welford + EMA), then incorporate the sample. If we
343// updated first, an outlier would inflate its own variance estimate
344// and z-score would understate the deviation. Criterion.rs / asv
345// use the same pre-update-then-record discipline.
346func _record_to_axis(a: *AxisStats, x_q10: nx_int,
347 raw_value: nx_int, alpha: nx_int) -> nx_int {
348 a.last_sample = raw_value
349 let z: nx_int = _axis_zscore_q10(a, x_q10)
350 a.last_z_q10 = z
351 _axis_welford_update(a, x_q10)
352 _axis_ema_update(a, x_q10, alpha)
353 return z
354}
355
356func nx_daemon_record_sample(d: *DaemonState,
357 axis_idx: nx_int,
358 raw_value: nx_int) -> nx_int {
359 if nx_benchmark_axis_index_is_valid(axis_idx) == 0 { return -1 }
360 let x_q10: nx_int = raw_value * NX_DAEMON_Q
361 let a: *AxisStats = _axis_at(d, axis_idx)
362 return _record_to_axis(a, x_q10, raw_value, d.config.ema_alpha_q10)
363}
364
365// ===== Cycle gating =================================================
366//
367// Caller pattern:
368// nx_daemon_cycle_start(d, current_tier)
369// if d.status == NX_DAEMON_SLEEPING { sys_sleep_ms(...); continue }
370// for each axis i in 0..5 { nx_daemon_record_sample(d, i, measure(i)) }
371// nx_daemon_cycle_end(d)
372// sys_sleep_ms(d.config.min_sleep_ms)
373
374func nx_daemon_cycle_start(d: *DaemonState, current_tier: nx_int) -> nx_int {
375 d.last_cycle_start_ns = nx_clock_monotonic_ns()
376 if current_tier < d.config.tier_floor {
377 d.status = NX_DAEMON_SLEEPING
378 d.cycle_skipped = d.cycle_skipped + 1
379 return d.status
380 }
381 if d.cycle_count < d.config.warmup_cycles {
382 d.status = NX_DAEMON_WARMING
383 } else {
384 d.status = NX_DAEMON_HEALTHY
385 }
386 return d.status
387}
388
389// Largest |z| across all six axes -- the worst-case regression signal.
390// Walk all 6 axes via the dispatch helper, return the worst |z| in Q10.
391// Same shape as the records-sample refactor: _axis_at centralises
392// axis indexing so the loop is a single readable scan.
393func _worst_z(d: *DaemonState) -> nx_int {
394 var worst: nx_int = 0
395 var i: nx_int = 0
396 while i < NX_BENCH_N_AXES {
397 let a: *AxisStats = _axis_at(d, i)
398 if a.last_z_q10 > worst { worst = a.last_z_q10 }
399 i = i + 1
400 }
401 return worst
402}
403
404// Returns: 0 = within band, 1 = degraded, 2 = regression.
405func _worst_band(d: *DaemonState) -> nx_int {
406 let worst: nx_int = _worst_z(d)
407 if worst >= d.config.regress_z_q10 { return 2 }
408 if worst >= d.config.degrade_z_q10 { return 1 }
409 return 0
410}
411
412// Set d.status, d.last_cycle_status, and bump cycle_count atomically.
413// Optionally bumps overbudget or regression counters. Centralises
414// the bookkeeping pattern so cycle_end stays small.
415func _finalise_cycle(d: *DaemonState, status: nx_int) -> nx_int {
416 d.status = status
417 d.last_cycle_status = status
418 d.cycle_count = d.cycle_count + 1
419 if status == NX_DAEMON_OVERBUDGET { d.cycle_overbudget = d.cycle_overbudget + 1 }
420 if status == NX_DAEMON_REGRESSION { d.cycle_regression = d.cycle_regression + 1 }
421 return status
422}
423
424// Map the worst-axis band (0/1/2) to a status enum.
425func _band_to_status(band: nx_int) -> nx_int {
426 if band == 2 { return NX_DAEMON_REGRESSION }
427 if band == 1 { return NX_DAEMON_DEGRADED }
428 return NX_DAEMON_HEALTHY
429}
430
431func nx_daemon_cycle_end(d: *DaemonState) -> nx_int {
432 d.last_cycle_end_ns = nx_clock_monotonic_ns()
433 let dur_ns: nx_int = d.last_cycle_end_ns - d.last_cycle_start_ns
434 d.last_cycle_ms = dur_ns / NX_NS_PER_MS
435
436 // Budget guard first; over-budget cycles' measurements are suspect.
437 if d.last_cycle_ms > d.config.max_ms_per_cycle {
438 return _finalise_cycle(d, NX_DAEMON_OVERBUDGET)
439 }
440 // Warmup gate: no DEGRADED/REGRESSION verdict before baseline.
441 if d.cycle_count < d.config.warmup_cycles {
442 return _finalise_cycle(d, NX_DAEMON_WARMING)
443 }
444 return _finalise_cycle(d, _band_to_status(_worst_band(d)))
445}
446
447// ===== Watchdog ======================================================
448
449func nx_daemon_watchdog_check(d: *DaemonState) -> nx_int {
450 if d.last_cycle_end_ns == 0 { return d.status }
451 let now: nx_int = nx_clock_monotonic_ns()
452 let elapsed_ns: nx_int = now - d.last_cycle_end_ns
453 let elapsed_ms: nx_int = elapsed_ns / NX_NS_PER_MS
454 let stale_threshold_ms: nx_int = d.config.min_sleep_ms * d.config.stale_missed_intervals
455 if elapsed_ms > stale_threshold_ms {
456 d.status = NX_DAEMON_STALE
457 }
458 return d.status
459}
460
461// ===== Sealed-enum validity =========================================
462
463func nx_daemon_status_is_valid(s: nx_int) -> nx_int {
464 if s < 0 { return 0 }
465 if s >= NX_DAEMON_N_STATES { return 0 }
466 return 1
467}
468
469// ===== Cardinal compliance ==========================================
470
471func nx_daemon_set_named_improvement(d: *DaemonState) -> nx_int {
472 d.named_improvement_set = 1
473 return 0
474}
475
476func nx_daemon_is_cardinal_compliant(d: *DaemonState) -> nx_int {
477 if d.status == NX_DAEMON_REGRESSION { return d.named_improvement_set }
478 if d.status == NX_DAEMON_OVERBUDGET { return d.named_improvement_set }
479 return 1
480}
481
482// ===== Public emit ===================================================
483//
484// One-line cycle summary; greppable + fixed-width.
485
486func nx_daemon_status_label(s: nx_int) -> *u8 {
487 if s == NX_DAEMON_IDLE { return "IDLE " as *u8 }
488 if s == NX_DAEMON_WARMING { return "WARMING " as *u8 }
489 if s == NX_DAEMON_HEALTHY { return "HEALTHY " as *u8 }
490 if s == NX_DAEMON_SLEEPING { return "SLEEPING " as *u8 }
491 if s == NX_DAEMON_DEGRADED { return "DEGRADED " as *u8 }
492 if s == NX_DAEMON_REGRESSION { return "REGRESSION " as *u8 }
493 if s == NX_DAEMON_OVERBUDGET { return "OVERBUDGET " as *u8 }
494 return "STALE " as *u8
495}
496
497func nx_daemon_emit_summary(d: *DaemonState) -> nx_int {
498 print("daemon: cycle=" as *u8)
499 print_i64(d.cycle_count)
500 print(" status=" as *u8)
501 print(nx_daemon_status_label(d.status))
502 print(" cycle_ms=" as *u8)
503 print_i64(d.last_cycle_ms)
504 print(" worst_z_q10=" as *u8)
505 print_i64(_worst_z(d))
506 println("" as *u8)
507 return 0
508}
509
510// ===== Sleep helper =================================================
511
512func nx_daemon_sleep_until_next(d: *DaemonState) -> nx_int {
513 return sys_sleep_ms(d.config.min_sleep_ms)
514}