code wiki / _hdl_build / nx_resmon_lib.nx

nx_resmon_lib.nx source

↩ module page · 306 lines · 19011 B

1// nx_resmon_lib.nx -- the PURE, GATEABLE core of nx_resmon (debt seq1005). 2// 3// Split out per rule 9 (single responsibility) and rule 15 (DRY): the organ owns the /proc walk and 4// the printing, this lib owns the two predicates that actually encode the policy -- 5// rm_is_leaker : what counts as a leaking process 6// rm_verdict : how measurements become GREEN / AMBER / RED 7// Keeping them here means the gate exercises the SAME code the organ runs, not a reimplementation. 8// Everything here is a pure function of its arguments (rm_field/rm_conf parse a caller-owned buffer), 9// so the gate needs no /proc, no files and no fixtures on disk. 10// license_tier: ORIGINAL 11import "nx_syscalls.nx" 12import "nx_itoa_lib.nx" // THE canonical integer emitter (runtime/ layer, so _hdl_build may import it) 13import "nx_logtail.nx" // lt_read_tail -- THE shared tail reader (runtime/ layer): see rm_trend_read_tail 14 15// ---- TAIL-ANCHORED READ OF THE BEAT LOG (2026-09-02) ------------------------------------------------- 16// rm_trend used rm_read on resmon.log, which keeps the HEAD of the file under a 1 MiB cap. The log grows 17// ~95 KB/day, so on 2026-08-30 ~20:00 it crossed 1 MiB and from that beat on the "newest" row rm_trend saw 18// was a TORN partial line at byte 1,048,575 -- no worst=, no worst_committed_kb= -- and every hourly beat 19// printed the SAME frozen `worst= newest_kb=-1 name_changed=1 verdict=UNOBSERVABLE` for 65 consecutive 20// beats while nx_seed_announce_all climbed 1.0 -> 1.5 GB in the rows the reader could no longer reach. 21// MEASURED live 2026-09-02 (`nx_resmon trend` -> exactly that line, exit 3). A HEAD-BOUNDED READ OF AN 22// APPEND-ONLY LOG REPORTS THE PAST AS THE PRESENT, AND THE ABSTENTION IT PRODUCES IS INVISIBLE BY DESIGN -- 23// this board's own axis-blind law, fired by the axis that carries the law. 24// COMPOSED, NOT RE-TYPED: lt_read_tail (runtime/nx_logtail.nx) seeks to the end, keeps the newest <=cap 25// bytes, trims the torn first record, and DECLARES the envelope (env[0]=file_bytes env[3]=truncated). It 26// lives in this lib rather than the organ so nx_resmon_gate can drive it on a planted over-cap log. 27func rm_trend_read_tail(logpath: *u8, lbuf: *u8, cap: i64, env: *i64) -> i64 { 28 return lt_read_tail(logpath, lbuf, cap, env) 29} 30 31func rm_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 32func rm_puts(s: *u8) -> i64 { sys_write(1, s, rm_slen(s)); return 0 } 33// rm_num is now a THIN ALIAS over the canonical emitter, kept as a NAME rather than an 34// IMPLEMENTATION so every existing rm_num caller needs no edit -- rule 15 without a flag day. 35func rm_num(v: i64) -> i64 { 36 // DELEGATED 2026-08-08 -- SUPERSEDES THE LAZY-STATIC HOIST I PUT HERE EARLIER TODAY. 37 // I hoisted this buffer to a static to stop the per-call mmap, then found nx_itoa_lib.nx (runtime/ 38 // layer, 2026-07-31) already IS the canonical integer emitter: ccz_cat_num allocates NOTHING and 39 // nxi_fd is the balanced fd shim, written once, explicitly 'the drop-in for every hand-rolled 40 // putn/gn/wn/pn clone'. nx_research_miner already delegates in exactly this one line. 41 // Two reasons the delegation beats my hoist, both of which I got wrong first: 42 // 1. RULE 15 -- a hoist leaves TWO implementations; a delegation leaves ONE. Better duplicate code 43 // is still duplicate code. 44 // 2. nx_mmapbal convicts an unmatched sys_mmap in a function that does not return a pointer, so 45 // the static hoist reads as a LEAK to the estate's own scanner. Delegating removes the finding 46 // instead of arguing with it. 47 // Behaviour is unchanged: both emit MSB-first decimal with a leading '-' for negatives, to fd 1. 48 nxi_out(v) 49 return 0 50} 51// read a whole small file; returns byte count (-1 if unreadable). NUL-terminates. 52func rm_read(path: *u8, buf: *u8, cap: i64) -> i64 { 53 let fd: i64 = sys_openat_rd(path) 54 if fd < 0 { return 0 - 1 } 55 var tot: i64 = 0 56 var run: i64 = 1 57 while run == 1 { 58 let n: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot - 1) 59 if n <= 0 { run = 0 } else { 60 tot = tot + n 61 if tot >= cap - 1 { run = 0 } 62 } 63 } 64 sys_close(fd) 65 buf[tot] = 0 as u8 66 return tot 67} 68// first integer on the line that STARTS with `key`; -1 if absent. Serves BOTH 69// /proc/<pid>/status ("VmSize:\t 1234 kB") and conf rows ("leak-min-kb = 262144"). 70func rm_field(buf: *u8, n: i64, key: *u8) -> i64 { 71 let kl: i64 = rm_slen(key) 72 var i: i64 = 0 73 var found: i64 = 0 - 1 74 while i + kl <= n { 75 if found < 0 { 76 var atline: i64 = 0 77 if i == 0 { atline = 1 } else { if buf[i - 1] == (10 as u8) { atline = 1 } } 78 if atline == 1 { 79 var j: i64 = 0 80 var ok: i64 = 1 81 while j < kl { if buf[i + j] != key[j] { ok = 0; j = kl } else { j = j + 1 } } 82 if ok == 1 { found = i + kl } 83 } 84 } 85 i = i + 1 86 } 87 if found < 0 { return 0 - 1 } 88 var p: i64 = found 89 var v: i64 = 0 90 var seen: i64 = 0 91 var run: i64 = 1 92 while run == 1 { 93 if p >= n { run = 0 } else { 94 let c: i64 = buf[p] as i64 95 if c == 10 { run = 0 } else { 96 if c >= 48 { 97 if c <= 57 { v = v * 10 + (c - 48); seen = 1 } else { if seen == 1 { run = 0 } } 98 } else { if seen == 1 { run = 0 } } 99 p = p + 1 100 } 101 } 102 } 103 if seen == 0 { return 0 - 1 } 104 return v 105} 106func rm_conf(cbuf: *u8, cn: i64, key: *u8, dflt: i64) -> i64 { 107 if cn <= 0 { return dflt } 108 let v: i64 = rm_field(cbuf, cn, key) 109 if v < 0 { return dflt } 110 return v 111} 112 113// ---- PREDICATE 1: a SCREEN for leak candidates -- NOT a leak verdict -------------------------------- 114// ⚠CORRECTED 2026-08-14. This predicate's original comment claimed vsz==vpk means "the address space has 115// ONLY EVER GROWN (allocate-without-free fingerprint)". That inference is FALSE. vsz==vpk says only that 116// the address space is CURRENTLY AT ITS HIGH-WATER MARK, which is equally true of a daemon that mmaps its 117// arena once at startup and never unmaps -- the leak-FREE design this estate recommends, and the one 118// nx_hub_gw's own header documents itself as implementing. ★★★★★★A LEAK IS A DERIVATIVE AND NO SNAPSHOT 119// RELATION CAN EXPRESS ONE -- growth needs two samples in time. So this is a NECESSARY-NOT-SUFFICIENT 120// SCREEN (a genuine leaker does always satisfy it), and the sufficient half lives in rm_sustained_count 121// below, fed by nx_memvel's N-window observation. MEASURED COST of the old reading, 2026-08-14: 17 122// suspects against a leak-count-red of 6, so nx_resmon was RED PERMANENTLY on this box, while the true 123// sustained-grower count measured 0 in back-to-back runs. ★A DETECTOR THAT IS PERMANENTLY RED IS ONE 124// EVERYONE LEARNS TO IGNORE. 125// SIZE IS JUDGED ON COMMITTED MEMORY (rss+swap), NEVER ON RESERVED VmData. Measured 2026-07-25 on the 126// first live run: a VmData screen reported a worst case of 40.6 GiB on a 36.9 GB box -- larger than 127// physical RAM -- because VmData counts reservations a process never faulted in. Screening on rss+swap 128// took the census from 63 suspects to 23 and the worst case to 3.02 GiB. 129func rm_is_leaker(vsz: i64, vpk: i64, rss: i64, swap: i64, min_kb: i64) -> i64 { 130 if vsz <= 0 { return 0 } 131 if vpk <= 0 { return 0 } 132 if vsz != vpk { return 0 } 133 var touched: i64 = 0 134 if rss > 0 { touched = touched + rss } 135 if swap > 0 { touched = touched + swap } 136 if touched < min_kb { return 0 } 137 return 1 138} 139 140// ---- PREDICATE 2: measurements -> severity --------------------------------------------------------- 141// 0=GREEN 1=AMBER 2=RED. RED dominates AMBER; ANY axis can raise the verdict on its own, because each 142// one independently indicates the box is in trouble. Thresholds are ALWAYS passed in (rule 11) -- this 143// function contains no policy numbers of its own, which is exactly what the gate mutates to prove it. 144// ⚠SIGNATURE CHANGED 2026-08-14: leak_observed. The two pressure axes are LEVELS, and a snapshot 145// measures a level exactly -- but the leak axis is a DERIVATIVE this organ cannot observe on its own, 146// so it must be able to say I COULD NOT LOOK. Before this, an unobservable leak axis was indistinguishable 147// from an observed zero, and the caller had no way to express the difference: the only two things it 148// could say were "no leaks" and "leaks", one of which is always a lie when there is no evidence. 149// ★★★★★I COULD NOT LOOK IS NOT IT IS FINE, AND IT IS NOT IT IS BROKEN -- an axis with no evidence must 150// contribute NOTHING to the verdict rather than silently voting GREEN. 151func rm_verdict(swused_pm: i64, avail_pm: i64, leakers: i64, leak_observed: i64, 152 sw_amber: i64, sw_red: i64, av_amber: i64, av_red: i64, 153 leak_amber: i64, leak_red: i64) -> i64 { 154 var sev: i64 = 0 155 if swused_pm >= sw_amber { sev = 1 } 156 if avail_pm <= av_amber { sev = 1 } 157 if leak_observed == 1 { if leakers >= leak_amber { sev = 1 } } 158 if swused_pm >= sw_red { sev = 2 } 159 if avail_pm <= av_red { sev = 2 } 160 if leak_observed == 1 { if leakers >= leak_red { sev = 2 } } 161 return sev 162} 163 164// ---- LINE BUILDERS -- MOVED HERE FROM nx_resmon.nx 2026-08-14 -------------------------------------- 165// Two organs now emit a status line, and the second one was about to grow its own copy of these. 166// ★EXTRACT THE FIX, DON'T RE-TYPE IT -- a helper duplicated across two files is the duplicate-ruler 167// defect wearing a utility function, and the copies diverge silently because nothing compares them. 168func rm_lcat(b: *u8, off: i64, s: *u8) -> i64 { var o: i64 = off; var j: i64 = 0; while s[j] != (0 as u8) { b[o] = s[j]; o = o + 1; j = j + 1 } return o } 169func rm_lnum(b: *u8, off: i64, v: i64) -> i64 { 170 var o: i64 = off 171 var m: i64 = v 172 if m < 0 { b[o] = 45 as u8; o = o + 1; m = 0 - m } 173 let t: *u8 = sys_mmap(28) 174 var k: i64 = 0 175 if m == 0 { t[0] = 48 as u8; k = 1 } 176 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 177 var i: i64 = 0 178 while i < k { b[o] = t[k - 1 - i]; o = o + 1; i = i + 1 } 179 sys_munmap(t, 28) 180 return o 181} 182 183// ---- PREDICATE 3: velocity -> how many processes are SUSTAINED growers ----------------------------- 184// THE SUFFICIENT HALF that PREDICATE 1 structurally cannot supply. acnt[i] is the number of observation 185// windows in which process i grew its COMMITTED memory; rounds is how many windows were observed. Only 186// k==rounds is a leak -- anything less is a process that happened to occupy a top slot in some windows 187// and not others, which is the normal churn of a busy box. Measured live 2026-08-14: the top grower was 188// synoelasticd at 3396 kB/s with grew_in=4/5, i.e. a 200 MB/min headline that is NOT a leak. 189// ★THIRD STATE: with zero observed windows the question is UNANSWERABLE, not answered "none" -- returns 190// -1 so a caller must distinguish "I could not look" from "I looked and found nothing". 191func rm_sustained_count(acnt: *i64, n: i64, rounds: i64) -> i64 { 192 if rounds <= 0 { return 0 - 1 } 193 if n < 0 { return 0 - 1 } 194 var c: i64 = 0 195 var i: i64 = 0 196 while i < n { 197 if acnt[i] >= rounds { c = c + 1 } 198 i = i + 1 199 } 200 return c 201} 202 203// ★★★★★★WHY A BLIND AXIS NEEDS A *CLASSIFIER* AND NOT JUST A FLAG: abstention is safe for the verdict 204// and invisible to the operator, so a detector that goes blind raises no alarm anywhere, by design -- 205// and "I could not look" collapses two states with OPPOSITE remedies into one word. MEASURED 206// 2026-08-21: the leak axis was UNOBSERVABLE for 7.2 h (~43 missed 600 s beats) while the pressure 207// axis went amber, and FOUR lanes reached FOUR different mechanisms, because a beat that RAN AND WAS 208// REFUSED by admission control and a beat that IS DEAD are indistinguishable from outside. nx_memvel 209// now stamps knowledge/status/memvel.refused on every refusal, which is what makes them separable. 210// 0 = NOT-BLIND -- the measurement itself is fresh; nothing to report. 211// 1 = REFUSING -- no fresh measurement, but a FRESH refusal stamp: the beat IS alive and admission 212// declined it. Remedy is to shed load; the axis recovers on its own. 213// 2 = DEAD -- neither artifact is fresh: nothing is running. Remedy is to fix the beat. 214// ★PURE BY CONSTRUCTION -- a function of already-measured numbers, so its referee needs no filesystem, 215// no fixture and no clock, and therefore cannot be fooled by any of them. This is the same reason 216// rm_sustained_count above lives here rather than in the organ: THE CLASSIFICATION IS THE DECISION. 217// ⚠A NEGATIVE AGE IS CLOCK SKEW, refused exactly as the measurement path refuses it -- accepting one 218// would let a future-stamped file read as fresh forever, the stale-fixture defect wearing a clock. 219// ⚠TOTAL: every (leak_observed, refused_age, max_age) triple returns exactly one of 0/1/2, so there is 220// no input for which a caller can receive "no answer" and quietly substitute its own. 221func rm_blind_class(leak_observed: i64, refused_age: i64, max_age: i64) -> i64 { 222 if leak_observed == 1 { return 0 } 223 if refused_age < 0 { return 2 } 224 if refused_age > max_age { return 2 } 225 return 1 226} 227 228// ---- THE ALARM AN ABSTAINING AXIS OWES (2026-08-26) ------------------------------------------------- 229// Abstention is SAFE for the verdict and INVISIBLE to the operator, so a detector that goes blind raises 230// nothing anywhere -- by design. rm_verdict is right to let UNOBSERVABLE cast no vote (guessing would be 231// worse), but that is only HALF a mechanism: the other half is an alarm on the blindness ITSELF. 232// MEASURED 2026-08-26: leak_axis=UNOBSERVABLE with memvel_age_s=1483 against max_age_s=900 while the 233// LAGGING pressure axis was RED at swap 895 permil -- the LEADING indicator dark exactly when it was most 234// wanted, which is the same pairing recorded on 2026-08-21. It is silent whenever pressure happens to be 235// fine, and that is the dangerous case, not this one. 236// WHY TWICE THE BOUND, DERIVED not picked: max_age is already sized to tolerate one FULL missed beat 237// (the live conf says so in its own words -- 900s against a 600s beat). One window stale is therefore a 238// transient miss the abstention already covers. TWO full windows cannot be a single missed beat, so it is 239// a STANDING blindness. The multiplier is named rather than inlined so the reasoning travels with it. 240const RM_BLIND_ALARM_WINDOWS: i64 = 2 241 242// PURE and TOTAL like its siblings: a function of already-measured numbers, no clock, no filesystem. 243// Returns a SEVERITY CONTRIBUTION, never a leak claim -- we do not know whether anything is leaking, and 244// saying so would be the acquittal-by-abstention defect inverted into a conviction. 245func rm_blind_sev(leak_observed: i64, leak_age: i64, max_age: i64) -> i64 { 246 if leak_observed == 1 { return 0 } // we can see; there is nothing to alarm about 247 if max_age <= 0 { return 0 } // no declared bound => cannot judge the blindness either; abstain 248 if leak_age < 0 { return 1 } // clock skew: the age is unusable, which IS a blindness 249 if leak_age >= max_age * RM_BLIND_ALARM_WINDOWS { return 1 } 250 return 0 251} 252 253// ---- AXIS D: DISK. THE AXIS THIS ORGAN NEVER HAD (2026-08-28) -------------------------------------- 254// WHY IT EXISTS. On 2026-08-28 a 100%-FULL DISK truncated a memory index to 0 bytes: open(path,"w") 255// truncates BEFORE it writes, so a full volume does not refuse a write, it DESTROYS the file. This organ 256// is "the resource axis nx_health lacks" and it measured MEMORY and SWAP only; a search for the disk 257// primitive returned matches=0 for BOTH sys_statfs and statvfs with corpus_complete=1. 258// 259// ★★★★★★THE BAR IS ABSOLUTE FREE BYTES, NOT A PERCENTAGE, AND THAT IS THE WHOLE DESIGN. A permil bar is 260// SIZE-BLIND, and the three volumes measured the day this shipped prove it in one line each: 261// /volume1 879 permil used -- 21.0 TB free -- PERFECTLY HEALTHY 262// NAS / 774 permil used -- 525 MiB free -- THE DANGEROUS ONE 263// laptop C: 912 permil used -- 81 GB free -- fine, and it is the volume that hit 100% earlier 264// A percentage threshold set anywhere useful would have ALARMED on the 21 TB volume and stayed SILENT on 265// the 525 MiB one -- exactly backwards. What ends a write is RUNNING OUT, not being 90% full, so the 266// severity is a function of bytes remaining and the permil rides along as CONTEXT only. 267// 268// PURE and TOTAL like its siblings: a function of already-measured numbers, no clock, no filesystem. 269// A NEGATIVE avail is STATFS_ERR -- could-not-look -- and contributes NOTHING. An unmeasurable volume 270// must never be scored as roomy, and must never be scored as full either: abstention is not a claim. 271func rm_disk_sev(avail_bytes: i64, amber_bytes: i64, red_bytes: i64) -> i64 { 272 if avail_bytes < 0 { return 0 } 273 if red_bytes > 0 { if avail_bytes <= red_bytes { return 2 } } 274 if amber_bytes > 0 { if avail_bytes <= amber_bytes { return 1 } } 275 return 0 276} 277 278// Composes rm_verdict_blind exactly as that composed rm_verdict: the pressure and leak axes keep their 279// arithmetic byte-for-byte and this can only ever RAISE. A run that was GREEN before can become non-GREEN 280// ONLY when a volume genuinely crosses its byte floor -- which is the entire point, and is stated plainly 281// rather than hidden behind the word "additive". 282func rm_verdict_disk(swused_pm: i64, avail_pm: i64, leakers: i64, leak_observed: i64, 283 sw_amber: i64, sw_red: i64, av_amber: i64, av_red: i64, 284 leak_amber: i64, leak_red: i64, 285 leak_age: i64, max_age: i64, 286 disk_avail_bytes: i64, disk_amber_bytes: i64, disk_red_bytes: i64) -> i64 { 287 var sev: i64 = rm_verdict_blind(swused_pm, avail_pm, leakers, leak_observed, 288 sw_amber, sw_red, av_amber, av_red, leak_amber, leak_red, 289 leak_age, max_age) 290 let d: i64 = rm_disk_sev(disk_avail_bytes, disk_amber_bytes, disk_red_bytes) 291 if d > sev { sev = d } 292 return sev 293} 294 295// Composes the incumbent rather than restating it: rm_verdict keeps its exact arithmetic and this only 296// ever RAISES, never lowers -- a blind leading axis must not be able to mask a red pressure axis. 297func rm_verdict_blind(swused_pm: i64, avail_pm: i64, leakers: i64, leak_observed: i64, 298 sw_amber: i64, sw_red: i64, av_amber: i64, av_red: i64, 299 leak_amber: i64, leak_red: i64, 300 leak_age: i64, max_age: i64) -> i64 { 301 var sev: i64 = rm_verdict(swused_pm, avail_pm, leakers, leak_observed, 302 sw_amber, sw_red, av_amber, av_red, leak_amber, leak_red) 303 let b: i64 = rm_blind_sev(leak_observed, leak_age, max_age) 304 if b > sev { sev = b } 305 return sev 306}