code wiki / _hdl_build / nx_clock_tickless.nx

nx_clock_tickless.nx source

↩ module page · 230 lines · 21391 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 } 44// EXTENDED WINDOW REPORT (2026-08-06). clk_run_edf ALREADY COMPUTES out[4]=max_lateness and 45// out[5]=exec_secs -- its own comment calls exec_secs "the number the old design silently threw away" -- 46// and then hbp published only 3 of the 6 outputs, so the two numbers that diagnose CAPACITY were thrown 47// away AGAIN at the publishing boundary. The window report could say a beat was starved but never say WHY, 48// which is exactly why netobs read as a dead job for days while its row and its elf were both fine. 49// ★A METRIC COMPUTED AND NOT PUBLISHED IS THE SAME BLIND SPOT AS A METRIC NEVER COMPUTED. 50// ADDITIVE (rule 19): a= b= c= keep their exact meanings, so every existing reader of this line still works. 51func hbx(tag: *u8, a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, g: i64, h: i64, nm: *u8, p: *u8) -> i64 { 52 let now: i64 = sys_now_realtime_sec(); let st: *u8 = sys_mmap(CLK_NAMEW + 256); var o: i64 = 0 // the slowest= field carries an ORGAN slot (+ <=243 fixed bytes); DERIVED from the slot 53 o=cat(st,o,"CLOCKBEAT " as *u8); o=cat(st,o,tag) 54 o=cat(st,o," a=" as *u8); o=catn(st,o,a) 55 o=cat(st,o," b=" as *u8); o=catn(st,o,b) 56 o=cat(st,o," c=" as *u8); o=catn(st,o,c) 57 o=cat(st,o," max_late=" as *u8); o=catn(st,o,d) 58 o=cat(st,o," exec=" as *u8); o=catn(st,o,e) 59 o=cat(st,o," jobs=" as *u8); o=catn(st,o,f) 60 o=cat(st,o," demand=" as *u8); o=catn(st,o,g) 61 o=cat(st,o," max_exec=" as *u8); o=catn(st,o,h) 62 o=cat(st,o," slowest=" as *u8); o=cat(st,o,nm) 63 o=cat(st,o," t=" as *u8); o=catn(st,o,now) 64 st[o]=10 as u8; o=o+1; st[o]=0 as u8 65 let fd: i64 = sys_openat_wr(p,0x1a4); if fd>=0 { sys_write(fd,st,o); sys_close(fd) } 66 return 0 67} 68func hb(tag: *u8, a: i64, b: i64, c: i64) -> i64 { return hbp(tag, a, b, c, "sites/nishifamily/clock_heartbeat.txt" as *u8) } 69func 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 } 70func 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 } 71 72// sleep exactly ms milliseconds (the same nanosleep idiom clk_run_tickless uses between beats; self-contained, 73// no dependency on a sys_sleep_ms wrapper) -- used to idle a full window when the registry is (transiently) empty. 74// FIRST-LINE-ONLY integer read. readnum() above parses EVERY digit anywhere in the file, which is 75// fine for a bare stamp but is a footgun for a CONFIG: a comment containing any number would silently 76// concatenate into the value, and this particular value controls how much work the scheduler does per 77// window. Stopping at the first newline lets the file carry its own documentation without changing 78// behaviour -- the same shape as nx_seat's st_reap_ttl, for the same reason. 79func readnum1(p: *u8) -> i64 { 80 let lp: *i64 = sys_mmap(8) as *i64 81 let d: *u8 = sys_read_file(p, lp) 82 if (d as i64)==0 { return 0 } 83 var c: i64=0 84 var i: i64=0 85 var go: i64=1 86 while i<lp[0] { 87 if go==1 { 88 if d[i]==(10 as u8) { go=0 } else { if d[i]>=(48 as u8) { if d[i]<=(57 as u8) { c=c*10+(d[i]-(48 as u8)) as i64 } } } 89 } 90 i=i+1 91 } 92 return c 93} 94func sleep_ms(ms: i64) -> i64 { 95 let ts: *i64 = sys_mmap(16) as *i64 96 ts[0] = ms / 1000 97 ts[1] = (ms - (ms/1000)*1000) * TLN_MAGIC_1000000 98 __syscall(35, ts as i64, 0, 0, 0, 0, 0) 99 return 0 100} 101 102// PERSISTENT DAEMON (was: one-shot-per-window that leaned on the supervisor's dead->respawn to fake continuity -- 103// which collapsed to a 0ms hot-exit + respawn STORM whenever the registry loaded zero jobs = the live flap the 104// operator caught). Now main() is a real long-running process the supervisor's PID guard expects: each iteration 105// RELOADS the registry (so a job registered later is picked up with no restart), then either runs a tickless window 106// (jobs present) or idles ONE full window (empty/at-deploy). It NEVER exits on an empty registry -> the flap is gone 107// by construction, and the tickless loop still sleeps between beats so CPU stays ~0. 108func main(argc: i64, argv: *i64) -> i64 { 109 // STATE SSOT IS A SEG-STORE PLANE (2026-08-03, debts 1784828927+1784868625: planes, never tsv). 110 // `reg` is now ONLY the one-time legacy migration source: clk_load_state reads the plane first 111 // and falls back to the tsv exactly once (the very next save lands in the plane and the tsv is 112 // never written again -- its mtime freezing is the cutover proof). 113 var reg: *u8 = "clock_jobs.tsv" as *u8 // LEGACY (cwd-relative): migration source only 114 var tickf: *u8 = "clock_tick.txt" as *u8 115 let stpl: *u8 = "knowledge/store/clocksched-" as *u8 // the mutable schedule-state plane (clock-exclusive) 116 if argc >= 2 { reg = argv[1] as *u8 } 117 if argc >= 3 { tickf = argv[2] as *u8 } 118 119 let names: *u8 = sys_mmap(CLK_MAXJOBS*CLK_NAMEW); let orgs: *u8 = sys_mmap(CLK_MAXJOBS*CLK_NAMEW) 120 let iv: *i64 = sys_mmap(CLK_MAXJOBS*8) as *i64; let nd: *i64 = sys_mmap(CLK_MAXJOBS*8) as *i64 121 let np: *i64 = sys_mmap(8) as *i64 122 // 128 BYTES = 16 SLOTS (widened from 64 = 8 on 2026-08-14). clk_run_edf now also publishes out[8] 123 // (worst-starver job index) and out[9] (periods it missed); at 64 bytes those two writes would have 124 // run off the end of the mapping. Sized with headroom so the next axis added to the window report 125 // does not silently overrun this allocation the way out[8] would have. 126 let out: *i64 = sys_mmap(128) as *i64 127 128 var run: i64 = 1 129 var windows: i64 = 0 130 while run == 1 { 131 np[0] = 0 132 let lsrc: i64 = clk_load_state(stpl, reg, names, orgs, iv, nd, np) 133 if lsrc == 1 { 134 // persist IMMEDIATELY so the cutover completes in seconds -- otherwise the plane only 135 // materializes at first window-end (~28 min) and a crash in that window re-migrates 136 clk_save_plane(stpl, names, orgs, iv, nd, np[0]) 137 w("nx_clock_tickless: MIGRATED legacy " as *u8); w(reg); w(" -> clocksched- plane (one-time; the tsv is never written again)\n" as *u8) 138 } 139 // WALL-CLOCK BASE (2026-08-04): new jobs must arm at now+interval in EPOCH seconds. Passing the 140 // legacy logical tick here would arm them in the past and fire a thundering herd on first sight. 141 let merged: i64 = clk_merge_store("knowledge/store/clockjobs-" as *u8, names, orgs, iv, nd, np, sys_now_realtime_sec()) 142 // PERSIST A RECONCILE IMMEDIATELY (2026-08-06). clk_save_plane otherwise runs ONLY at window end, 143 // so a re-period declared in clockjobs- stayed invisible in clocksched- for up to a full 1800s window. 144 // MEASURED: I re-periodded worldgen-gate 120s -> 86400s with the new nx_clockjob writer, restarted to 145 // force the merge, read the plane, saw 120 -- and nearly concluded my own writer was broken. It was 146 // not: the change was live in memory and merely unsaved. An operator without source access has NO WAY 147 // to tell those apart. Adopt the returned count (a merge the caller does not adopt repeats itself). 148 // (STAR)A CHANGE APPLIED IN MEMORY BUT NOT PERSISTED IS INDISTINGUISHABLE FROM A CHANGE THAT WAS 149 // REJECTED -- AND THE READER WILL BLAME THE TOOL, NOT THE OBSERVATION. 150 if merged > 0 { np[0] = clk_save_plane(stpl, names, orgs, iv, nd, np[0]) } // off-tsv additive job-adds (CLOBBER-PROOF: clock is READ-ONLY on the plane; adds via nx_store_put) 151 // ONE-TIME, SELF-HEALING MIGRATION: any deadline still carrying a legacy logical tick is converted 152 // to a real instant (exact -- a tick can never be a valid epoch). Runs every window and is a no-op 153 // once converted, so a row restored from an old backup repairs itself instead of firing forever. 154 let mig: i64 = clk_edf_migrate(nd, iv, np[0], sys_now_realtime_sec()) 155 if mig > 0 { w("nx_clock_tickless: MIGRATED " as *u8); wn(mig); w(" logical-tick deadlines -> wall-clock instants\n" as *u8) } 156 if np[0] == 0 { 157 hb("idle-no-jobs" as *u8, 0, 0, 0) // observable self-beat: clock alive, registry empty (heartbeat present + zero dispatch) 158 w("nx_clock_tickless: no jobs in " as *u8); w(reg); w(" -- idling one window (not exiting)\n" as *u8) 159 sleep_ms(TLN_MAXBEATS * TLN_TICK_MS) // idle a FULL window, then RELOAD -- never the 0ms hot-exit that caused the flap 160 } else { 161 let T0: i64 = sys_now_realtime_sec() 162 hb("window-start" as *u8, T0, np[0], 0) // a=start-epoch b=jobs -> proves the clock ITSELF runs + writes the docroot 163 // WALL-CLOCK EDF WINDOW (2026-08-04, debt 1785872141). REPLACES clk_run_tickless, whose logical 164 // tick advanced by SLEEP ONLY and therefore drifted behind real time by exactly the dispatch 165 // cost -- stretching every period (measured: a 6h job fired ONCE in 21h). Deadlines are now real 166 // instants and the clock is re-read after every child, so job runtime can no longer be lost. 167 // DISPATCH CEILING IS DATA-DRIVEN (2026-08-06, rule 11). MEASURED that day: a window ran 168 // a=240 b=9 exec=252s of 1800s -- 240 is EXACTLY TLN_MAXDISPATCH, so the binding constraint 169 // was a COMPILED CONSTANT while 86pct of the time budget went unused, against demand=268. 170 // The window's TIME budget is the real resource and already bounds a runaway (clk_run_edf 171 // exits on whichever of budget/ceiling comes first); the count ceiling was belt-and-braces 172 // that quietly became the limiter as the registry grew to 45 jobs. 173 // FAIL-SAFE: an absent/zero/unreadable conf keeps the compiled 240 EXACTLY as before, so a 174 // missing file can never silently change how much work the scheduler does. 175 var maxd: i64 = TLN_MAXDISPATCH 176 let mdcfg: i64 = readnum1("knowledge/status/clock_maxdispatch.conf" as *u8) 177 if mdcfg > 0 { maxd = mdcfg } 178 clk_run_edf(orgs, names, iv, nd, np[0], maxd, TLN_WINDOW_SECS, TLN_TICK_MS, out) 179 // NAME THE BINDING CONSTRAINT. Hitting the ceiling and running out of time look identical 180 // in every other number this organ prints, and telling them apart is the whole difference 181 // between "raise a constant" and "the box is too slow" -- it cost a full session to spot. 182 if out[0] >= maxd { w("nx_clock_tickless: DISPATCH-CEILING BOUND -- completed " as *u8); wn(out[0]); w(" = the ceiling (" as *u8); wn(maxd); w("), exec " as *u8); wn(out[5]); w("s of " as *u8); wn(TLN_WINDOW_SECS); w("s. The COUNT limit bound this window, not time; raise knowledge/status/clock_maxdispatch.conf\n" as *u8) } 183 let demand: i64 = clk_demand_per_window(iv, np[0], TLN_WINDOW_SECS) 184 // NAME THE HOG, DO NOT MAKE THE READER INFER IT. out[7] is the job index that owned the worst 185 // single dispatch; -1 means nothing was dispatched at all (which is itself the answer). 186 var slow: *u8 = "none" as *u8 187 if out[7] >= 0 { slow = clk_slot(orgs, out[7]) } 188 hbx("window-end" as *u8, out[0], out[3], out[2], out[4], out[5], np[0], demand, out[6], slow, "sites/nishifamily/clock_dispatch.txt" as *u8) // a=DISPATCHES b=STARVED c=epoch (unchanged) + max_late/exec/jobs/demand 189 // OVERSUBSCRIPTION IS A CAPACITY FACT, NOT A SCHEDULING BUG. When the window cannot complete what 190 // the registry demands, NO ordering saves anyone -- EDF just picks who loses. Say it LOUDLY and name 191 // the ratio, because the alternative is precisely what happened for days: a healthy registry row, a 192 // healthy promoted elf, a beat silently running ~5x slow, and a remediation that sent every reader 193 // off to re-verify the row and the elf -- the two things that were never wrong. 194 if demand > out[0] { w("nx_clock_tickless: OVERSUBSCRIBED demand=" as *u8); wn(demand); w(" dispatches/window vs completed=" as *u8); wn(out[0]); w(" (exec " as *u8); wn(out[5]); w("s of " as *u8); wn(TLN_WINDOW_SECS); w("s) -- EDF is choosing WHO starves, not WHETHER. Lengthen periods or raise capacity.\n" as *u8) } 195 // ADOPT THE MERGED COUNT (2026-07-31). clk_save now reload-merges externally-added rows and returns 196 // the NEW job count. Discarding that return re-appends the same row on every save: the row lands in 197 // the arrays but np[0] never learns of it, so the next save's clk_find misses it and appends a 198 // DUPLICATE. Measured immediately after shipping the merge -- srcguard appeared twice while all 30 199 // other jobs stayed unique, which is what named the cause. u2605A MERGE THAT THE CALLER DOES NOT ADOPT 200 // IS A MERGE THAT REPEATS ITSELF. 201 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) 202 // HONEST WINDOW REPORT: starvation and lateness are PUBLISHED, not inferred. exec_secs is the 203 // number the old design threw away -- it is precisely the drift the logical tick used to absorb. 204 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) 205 // NAME THE STARVED JOB (2026-08-14). The line above has printed a bare STARVED count since it 206 // was written; a reader seeing "STARVED 3" had no way to learn WHICH jobs missed their period, 207 // which is the only fact that decides the remedy (lengthen THAT period vs raise capacity). 208 // Printed as a SEPARATE line on purpose: clock_dispatch.txt's CLOCKBEAT format is parsed by 209 // other readers, and widening a published record to answer a new question is how a format 210 // silently breaks its consumers. Only emitted when something actually starved, so a healthy 211 // window stays exactly as quiet as before. 212 if out[3] > 0 { w("nx_clock_tickless: WORST-STARVER " as *u8); w(clk_slot(orgs, out[8])); w(" missed " as *u8); wn(out[9]); w(" period(s) -- its declared interval is unschedulable against this registry; lengthen THAT period or raise capacity\n" as *u8) } 213 // STORM-DEFERRED (2026-09-02): out[10] = heavy beats clk_run_edf declined to fork because the shared 214 // I/O-storm ruler (nx_ioadmit_lib, the build gate's own witness) reported zero spawn budget. Each one 215 // was re-armed a fraction of its period ahead, so a quiet window prints nothing new -- the line only 216 // appears when the clock actually stood aside, which is the whole point of announcing it. 217 if out[10] > 0 { w("nx_clock_tickless: STORM-DEFERRED " as *u8); wn(out[10]); w(" heavy dispatch(es) re-armed (knowledge/status/clock_heavy.conf; the box was at its D-state storm line, the same conjunct /api/build refuses on)\n" as *u8) } 218 } 219 // LIFE BUDGET (2026-07-05 leak arc): bounded windows per life -> clean exit -> the supervisor 220 // respawns fresh (rwin=0 = by-design respawn, never counted as crash-loop). Address-space leak 221 // per life is now BOUNDED instead of 1.81TB-per-21-days; state already persisted every window. 222 windows = windows + 1 223 if windows >= TLN_MAXWINDOWS { 224 hb("life-recycle" as *u8, windows, 0, 0) 225 w("nx_clock_tickless: window budget reached -- clean exit (supervisor respawns fresh)\n" as *u8) 226 run = 0 227 } 228 } 229 return 0 230}