code wiki / _hdl_build / nx_clock_tickless.nx

nx_clock_tickless.nx source

↩ module page · 134 lines · 12697 B

1// nx_clock_tickless.nx -- the TICKLESS driver (the sched_tickless.raw lesson: don't burn a constant tick when 2// idle). From ONE spark it runs a BOUNDED number of beats, but between beats it SLEEPS exactly until the next due 3// job (the minimum next_due) instead of waking every tick -- so a registry with sparse jobs costs only as many 4// wakeups as there are due events, not one-per-tick. Bounded (TLN_MAXBEATS) so a single spark can never run away. 5// Reduces the external trigger from per-beat to per-WINDOW. Reuses the gated nx_clock library (load/dispatch/save). 6// nx_clock_tickless [registry] [tickfile] (run the elf directly to pass args; defaults = the live registry) 7// license_tier: ORIGINAL expect_exit: 0 8import "nx_clock_sched.nx" 9import "nx_syscalls.nx" 10const TLN_MAGIC_1000000: i64 = 1000000 11 12const TLN_MAXBEATS: i64 = 30 // beats per window; the NAS supervisor respawns after each window (respawns stay well under the crash-loop guard) 13const TLN_WINDOW_SECS: i64 = 1800 // 2026-08-04: a window is now bounded by REAL seconds, not by a beat 14 // count whose duration depended on how long the children took. ~30min 15 // keeps the old window cadence (30 beats x ~60s) and the life budget. 16const TLN_MAXDISPATCH: i64 = 240 // hard per-window dispatch ceiling: a saturated registry can never turn 17 // one window into an unbounded run (the EDF loop exits on whichever of 18 // budget/ceiling comes first, and the supervisor respawns fresh). 19const TLN_TICK_MS: i64 = 1000 // 1s per logical tick (server cadence; a sitecheck at interval 30 = a probe every ~30s) 20const TLN_MAXWINDOWS: i64 = 120 // windows per LIFE (2026-07-05 leak arc): the GC-free substrate leaks address 21 // space per window forever (measured 1.81TB VmSize on a 21-day life = the 22 // Committed_AS flood); a clean exit after ~120 windows (>=1h life) lets the 23 // supervisor respawn fresh = supervisor-as-GC. Safe BY DESIGN for this daemon: 24 // hc_guard_one passes rwin=0 (tickless respawn is expected, never crash-loop), 25 // and each life is HOURS (the 2026-06 flap was 0ms hot-exits on an empty 26 // registry -- that idle-sleep fix below is kept; state persists via clk_save). 27 28func w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 29func wn(v: i64) -> i64 { var m: i64=v; if m<0{w("-" as *u8);m=0-m} let t:*u8=sys_mmap(24); var k:i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i:i64=0; let o:*u8=sys_mmap(24); while i<k{o[i]=t[k-1-i];i=i+1} sys_write(1,o,k); return 0 } 30func cat(dst: *u8, o: i64, s: *u8) -> i64 { var x: i64=o; var i: i64=0; while s[i]!=(0 as u8){dst[x]=s[i];x=x+1;i=i+1} return x } 31func catn(dst: *u8, o: i64, v: i64) -> i64 { var x: i64=o; var m: i64=v; if m<0{dst[x]=45 as u8;x=x+1;m=0-m} if m==0{dst[x]=48 as u8;return x+1} let t:*u8=sys_mmap(24); var k:i64=0; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var j:i64=0; while j<k{dst[x]=t[k-1-j];x=x+1;j=j+1} return x } 32// SELF-heartbeat to the docroot (the clock's OWN write -- NO dispatched child). Decisive diagnostic vs clock_health.txt 33// (the dispatched sitecheck's write): heartbeat present + health absent => the clock writes fine, the NESTED DISPATCH fails. 34// THE DISPATCH COUNT WAS UNOBSERVABLE BY CONSTRUCTION (fixed 2026-08-01). hb() writes ONE line and 35// OVERWRITES, and in the main loop hb("window-end") is followed immediately by clk_save/clk_load/merge and 36// then hb("window-start") -- so the window-end beat, the ONLY place the honest dispatch count b is 37// published, survived for well under a second and could never be sampled. Measured: two 20s-interval 38// watches spanning ~30 minutes caught window-start twice and window-end ZERO times, while the tick 39// advanced normally (+870/+900 per window), proving windows were completing the whole time. 40// THE FIX: route the terminal beat to its OWN path so a later beat cannot clobber it. b is now readable 41// at any time. b==0 isolates a nested-execve failure (every child failing); b>0 with stale job outputs 42// isolates individual organs. Without this, "did the scheduler actually RUN anything" was unanswerable. 43func hbp(tag: *u8, a: i64, b: i64, c: i64, p: *u8) -> i64 { let now: i64=sys_now_realtime_sec(); let st:*u8=sys_mmap(256); var o:i64=0; o=cat(st,o,"CLOCKBEAT " as *u8); o=cat(st,o,tag); o=cat(st,o," a=" as *u8); o=catn(st,o,a); o=cat(st,o," b=" as *u8); o=catn(st,o,b); o=cat(st,o," c=" as *u8); o=catn(st,o,c); o=cat(st,o," t=" as *u8); o=catn(st,o,now); st[o]=10 as u8; o=o+1; st[o]=0 as u8; let fd:i64=sys_openat_wr(p,0x1a4); if fd>=0 { sys_write(fd,st,o); sys_close(fd) } return 0 } 44func hb(tag: *u8, a: i64, b: i64, c: i64) -> i64 { return hbp(tag, a, b, c, "sites/nishifamily/clock_heartbeat.txt" as *u8) } 45func readnum(p: *u8) -> i64 { let lp: *i64 = sys_mmap(8) as *i64; let d: *u8 = sys_read_file(p, lp); if (d as i64)==0 { return 0 } var c: i64=0; var i: i64=0; while i<lp[0] { if d[i]>=(48 as u8) { if d[i]<=(57 as u8) { c=c*10+(d[i]-(48 as u8)) as i64 } } i=i+1 } return c } 46func savenum(p: *u8, v: i64) -> i64 { let buf: *u8 = sys_mmap(24); var o: i64=0; var m: i64=v; if m==0 { buf[0]=48 as u8; o=1 } else { let t: *u8=sys_mmap(24); var k: i64=0; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} while o<k { buf[o]=t[k-1-o]; o=o+1 } } let fd: i64 = sys_openat_wr(p, 0x1a4); if fd>=0 { sys_write(fd, buf, o); sys_close(fd) } return 0 } 47 48// sleep exactly ms milliseconds (the same nanosleep idiom clk_run_tickless uses between beats; self-contained, 49// no dependency on a sys_sleep_ms wrapper) -- used to idle a full window when the registry is (transiently) empty. 50func sleep_ms(ms: i64) -> i64 { 51 let ts: *i64 = sys_mmap(16) as *i64 52 ts[0] = ms / 1000 53 ts[1] = (ms - (ms/1000)*1000) * TLN_MAGIC_1000000 54 __syscall(35, ts as i64, 0, 0, 0, 0, 0) 55 return 0 56} 57 58// PERSISTENT DAEMON (was: one-shot-per-window that leaned on the supervisor's dead->respawn to fake continuity -- 59// which collapsed to a 0ms hot-exit + respawn STORM whenever the registry loaded zero jobs = the live flap the 60// operator caught). Now main() is a real long-running process the supervisor's PID guard expects: each iteration 61// RELOADS the registry (so a job registered later is picked up with no restart), then either runs a tickless window 62// (jobs present) or idles ONE full window (empty/at-deploy). It NEVER exits on an empty registry -> the flap is gone 63// by construction, and the tickless loop still sleeps between beats so CPU stays ~0. 64func main(argc: i64, argv: *i64) -> i64 { 65 // STATE SSOT IS A SEG-STORE PLANE (2026-08-03, debts 1784828927+1784868625: planes, never tsv). 66 // `reg` is now ONLY the one-time legacy migration source: clk_load_state reads the plane first 67 // and falls back to the tsv exactly once (the very next save lands in the plane and the tsv is 68 // never written again -- its mtime freezing is the cutover proof). 69 var reg: *u8 = "clock_jobs.tsv" as *u8 // LEGACY (cwd-relative): migration source only 70 var tickf: *u8 = "clock_tick.txt" as *u8 71 let stpl: *u8 = "knowledge/store/clocksched-" as *u8 // the mutable schedule-state plane (clock-exclusive) 72 if argc >= 2 { reg = argv[1] as *u8 } 73 if argc >= 3 { tickf = argv[2] as *u8 } 74 75 let names: *u8 = sys_mmap(CLK_MAXJOBS*CLK_NAMEW); let orgs: *u8 = sys_mmap(CLK_MAXJOBS*CLK_NAMEW) 76 let iv: *i64 = sys_mmap(CLK_MAXJOBS*8) as *i64; let nd: *i64 = sys_mmap(CLK_MAXJOBS*8) as *i64 77 let np: *i64 = sys_mmap(8) as *i64 78 let out: *i64 = sys_mmap(64) as *i64 79 80 var run: i64 = 1 81 var windows: i64 = 0 82 while run == 1 { 83 np[0] = 0 84 let lsrc: i64 = clk_load_state(stpl, reg, names, orgs, iv, nd, np) 85 if lsrc == 1 { 86 // persist IMMEDIATELY so the cutover completes in seconds -- otherwise the plane only 87 // materializes at first window-end (~28 min) and a crash in that window re-migrates 88 clk_save_plane(stpl, names, orgs, iv, nd, np[0]) 89 w("nx_clock_tickless: MIGRATED legacy " as *u8); w(reg); w(" -> clocksched- plane (one-time; the tsv is never written again)\n" as *u8) 90 } 91 // WALL-CLOCK BASE (2026-08-04): new jobs must arm at now+interval in EPOCH seconds. Passing the 92 // legacy logical tick here would arm them in the past and fire a thundering herd on first sight. 93 clk_merge_store("knowledge/store/clockjobs-" as *u8, names, orgs, iv, nd, np, sys_now_realtime_sec()) // off-tsv additive job-adds (CLOBBER-PROOF: clock is READ-ONLY on the plane; adds via nx_store_put) 94 // ONE-TIME, SELF-HEALING MIGRATION: any deadline still carrying a legacy logical tick is converted 95 // to a real instant (exact -- a tick can never be a valid epoch). Runs every window and is a no-op 96 // once converted, so a row restored from an old backup repairs itself instead of firing forever. 97 let mig: i64 = clk_edf_migrate(nd, iv, np[0], sys_now_realtime_sec()) 98 if mig > 0 { w("nx_clock_tickless: MIGRATED " as *u8); wn(mig); w(" logical-tick deadlines -> wall-clock instants\n" as *u8) } 99 if np[0] == 0 { 100 hb("idle-no-jobs" as *u8, 0, 0, 0) // observable self-beat: clock alive, registry empty (heartbeat present + zero dispatch) 101 w("nx_clock_tickless: no jobs in " as *u8); w(reg); w(" -- idling one window (not exiting)\n" as *u8) 102 sleep_ms(TLN_MAXBEATS * TLN_TICK_MS) // idle a FULL window, then RELOAD -- never the 0ms hot-exit that caused the flap 103 } else { 104 let T0: i64 = sys_now_realtime_sec() 105 hb("window-start" as *u8, T0, np[0], 0) // a=start-epoch b=jobs -> proves the clock ITSELF runs + writes the docroot 106 // WALL-CLOCK EDF WINDOW (2026-08-04, debt 1785872141). REPLACES clk_run_tickless, whose logical 107 // tick advanced by SLEEP ONLY and therefore drifted behind real time by exactly the dispatch 108 // cost -- stretching every period (measured: a 6h job fired ONCE in 21h). Deadlines are now real 109 // instants and the clock is re-read after every child, so job runtime can no longer be lost. 110 clk_run_edf(orgs, iv, nd, np[0], TLN_MAXDISPATCH, TLN_WINDOW_SECS, TLN_TICK_MS, out) 111 hbp("window-end" as *u8, out[0], out[3], out[2], "sites/nishifamily/clock_dispatch.txt" as *u8) // a=DISPATCHES b=STARVED c=epoch -> a==0 isolates a nested-execve failure 112 // ADOPT THE MERGED COUNT (2026-07-31). clk_save now reload-merges externally-added rows and returns 113 // the NEW job count. Discarding that return re-appends the same row on every save: the row lands in 114 // the arrays but np[0] never learns of it, so the next save's clk_find misses it and appends a 115 // DUPLICATE. Measured immediately after shipping the merge -- srcguard appeared twice while all 30 116 // other jobs stayed unique, which is what named the cause. u2605A MERGE THAT THE CALLER DOES NOT ADOPT 117 // IS A MERGE THAT REPEATS ITSELF. 118 np[0] = clk_save_plane(stpl, names, orgs, iv, nd, np[0]); savenum(tickf, out[2]) // persist advanced deadlines + final EPOCH to the plane (resume across windows; the tsv stays frozen) 119 // HONEST WINDOW REPORT: starvation and lateness are PUBLISHED, not inferred. exec_secs is the 120 // number the old design threw away -- it is precisely the drift the logical tick used to absorb. 121 w("nx_clock_tickless: " as *u8); wn(out[0]); w(" dispatches, slept " as *u8); wn(out[1]); w("s, exec " as *u8); wn(out[5]); w("s, STARVED " as *u8); wn(out[3]); w(", max_late " as *u8); wn(out[4]); w("s, epoch=" as *u8); wn(out[2]); w("\n" as *u8) 122 } 123 // LIFE BUDGET (2026-07-05 leak arc): bounded windows per life -> clean exit -> the supervisor 124 // respawns fresh (rwin=0 = by-design respawn, never counted as crash-loop). Address-space leak 125 // per life is now BOUNDED instead of 1.81TB-per-21-days; state already persisted every window. 126 windows = windows + 1 127 if windows >= TLN_MAXWINDOWS { 128 hb("life-recycle" as *u8, windows, 0, 0) 129 w("nx_clock_tickless: window budget reached -- clean exit (supervisor respawns fresh)\n" as *u8) 130 run = 0 131 } 132 } 133 return 0 134}