code wiki / (root) / nx_daemon_bench.nx

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}