code wiki / (root) / nx_ioadmit_lib.nx

nx_ioadmit_lib.nx source

↩ module page · 886 lines · 43663 B

1// nx_ioadmit_lib.nx -- THE I/O-STORM ADMISSION RULER, importable: ONE ruler for every process that is 2// about to ADD uninterruptible I/O to a shared array (torrent workers, crawlers, bulk writers) -- the same 3// witness nx_build_admit already uses to refuse a compiler fork, so a spawner and the build gate can 4// never disagree about whether the box is in an I/O storm. 5// 6// WHY (measured 2026-08-19): the box sat at load 12-17 on 8 CPUs for days with procs_blocked=8, and 5 of 7// the 8 blocked pids were nx_torrent_get workers -- the torrent daemon resumed EVERY active torrent at 8// once (no budget, no resource awareness), and that alone pushed procs_blocked to ncpu, where the build 9// admission's I/O-storm witness correctly refused every lane's builds. The cure is not a higher ceiling; 10// it is that an I/O spawner asks the same question the build gate asks BEFORE it forks. 11// 12// THE RULER (unchanged from nx_build_admit.ba_verdict, 2026-08-17): procs_blocked >= per_cpu x ncpu is an 13// I/O storm. ioa_spawn_budget answers "how many MORE blocked processes may I add and stay under that line, 14// leaving reserve slots for everyone else" -- a number DERIVED from the box, never chosen. A third state 15// is first-class: /proc/stat unreadable or truncated -> IOA_UNREADABLE, never a guess (the caller owns 16// its fallback and must NAME it). 17// license_tier: ORIGINAL No hw writes (Rule 26). lib (no main) 18import "nx_syscalls.nx" 19import "nx_resmon_lib.nx" // rm_read (bounded read that reports a full cap), rm_field (line-anchored int), rm_conf 20import "nx_itoa_lib.nx" // nxi_buf: the canonical integer emitter (builds /proc/<pid>/stat paths from a numeric pid) 21 22const IOA_STAT: *u8 = "/proc/stat" 23// /proc files report size 0 to stat/lseek, so sys_read_file CANNOT size them; a bound is legitimate here 24// and its truncation ANNOUNCES: a read that fills the cap returns IOA_UNREADABLE, never a prefix -- 25// procs_blocked is the LAST line of /proc/stat, after the intr line, so a prefix would silently lose it. 26const IOA_STAT_CAP: i64 = 65536 27const IOA_UNREADABLE: i64 = 0 - 1 28// The same bracket nx_build_admit ships (BA_BLOCKED_PER_CPU = 1): the 2026-07-30 healthy disk-wait case 29// had blocked 1-6 on 8 CPUs (below the line), the 2026-08-16 outage ~9 (above), the 2026-08-19 torrent 30// storm exactly 8 (at). Tighten with new samples and say so; never silently. 31const IOA_BLOCKED_PER_CPU: i64 = 1 32// Never be the process that crosses the line: leave this many blocked slots for every other writer 33// (store_put, fsync paths, the build gate's own reads). One slot is the smallest non-zero reserve; it 34// becomes a conf key the day a measurement asks for more. 35const IOA_RESERVE_SLOTS: i64 = 1 36// /proc/<pid>/stat comm: the kernel truncates comm to TASK_COMM_LEN-1 = 15 bytes, so a longer name is 37// matched on its first 15 bytes (nx_torrent_get.sov.elf -> "nx_torrent_get."). 38const IOA_COMM_MAX: i64 = 15 39const IOA_DIRBUF: i64 = 65536 // ONE getdents64 transfer; the walk loops until it returns 0 40const IOA_STATBUF: i64 = 1024 // one /proc/<pid>/stat line; comm sits inside the first 64 bytes 41const IOA_PATHBUF: i64 = 64 // "/proc/<pid>/stat" 42const IOA_RECLEN_OFF: i64 = 16 // linux_dirent64: d_reclen at 16, d_name at 19 43const IOA_NAME_OFF: i64 = 19 44 45func ioa_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 46// count "cpuN " lines in /proc/stat (the aggregate "cpu " line has a SPACE at index 3, per-cpu lines a DIGIT) 47func ioa_ncpu(buf: *u8, n: i64) -> i64 { 48 var c: i64 = 0 49 var i: i64 = 0 50 while i < n { 51 var bol: i64 = 0 52 if i == 0 { bol = 1 } else { if buf[i-1] == (10 as u8) { bol = 1 } } 53 if bol == 1 { if i + 4 <= n { if buf[i] == (99 as u8) { if buf[i+1] == (112 as u8) { if buf[i+2] == (117 as u8) { 54 let d: i64 = buf[i+3] as i64 55 if d >= 48 { if d <= 57 { c = c + 1 } } 56 } } } } } 57 i = i + 1 58 } 59 return c 60} 61// read /proc/stat whole; out[0]=ncpu out[1]=procs_blocked. returns 0, or IOA_UNREADABLE when the file 62// cannot be read, FILLS the cap (truncation), or lacks either field -- out[] carries IOA_UNREADABLE then. 63func ioa_measure(out: *i64) -> i64 { 64 let buf: *u8 = sys_mmap(IOA_STAT_CAP) 65 let n: i64 = rm_read(IOA_STAT, buf, IOA_STAT_CAP) 66 out[0] = IOA_UNREADABLE 67 out[1] = IOA_UNREADABLE 68 // ONE EXIT, ONE munmap (2026-09-02). Every early return above used to leave the 64 KiB /proc/stat 69 // buffer mapped: harmless in a oneshot like nx_build_admit, a LEAK in any long-lived caller -- and 70 // the clock dispatcher now consults this ruler before every heavy dispatch for the life of the 71 // daemon (nx_clock_tickless, 120 windows). A RULER THAT IS SAFE ONLY IN A PROCESS THAT EXITS IS 72 // NOT A SHARED RULER. The decision logic is byte-for-byte the same; only the exit path changed. 73 var rc: i64 = IOA_UNREADABLE 74 if n > 0 { if n < IOA_STAT_CAP - 1 { 75 let ncpu: i64 = ioa_ncpu(buf, n) 76 let blk: i64 = rm_field(buf, n, "procs_blocked" as *u8) 77 if ncpu > 0 { if blk >= 0 { 78 out[0] = ncpu 79 out[1] = blk 80 rc = 0 81 } } 82 } } 83 sys_munmap(buf, IOA_STAT_CAP) 84 return rc 85} 86// PURE: how many more I/O-bound processes may be added right now without reaching the storm line. 87// ncpu<=0 or procs_blk<0 = unobservable -> IOA_UNREADABLE (the caller decides its own fallback; this 88// function never invents one). Otherwise max(0, per_cpu*ncpu - procs_blk - reserve). 89func ioa_spawn_budget(ncpu: i64, procs_blk: i64, per_cpu: i64, reserve: i64) -> i64 { 90 if ncpu <= 0 { return IOA_UNREADABLE } 91 if procs_blk < 0 { return IOA_UNREADABLE } 92 var b: i64 = per_cpu * ncpu - procs_blk - reserve 93 if b < 0 { b = 0 } 94 return b 95} 96// ---- comm counting ----------------------------------------------------------------------------------- 97// MEASURED 2026-08-19, first live sweep: `live_workers=63` against max_workers=2. A worker forks peer 98// handlers WITHOUT exec, and fork keeps the parent's comm -- so "every process whose comm matches" counted 99// 2 workers plus 61 children, and the conf budget read 0 forever while any worker lived. The count a 100// spawner needs is ROOT processes of that comm: comm matches AND the PARENT's comm does not (a worker's 101// parent is the daemon or init after its double-fork; a handler's parent is the worker). 102// *A NAME THAT IS INHERITED ACROSS fork() COUNTS A TREE, NOT A POPULATION -- cut at the parent edge. 103const IOA_ROOTS_ONLY: i64 = 1 104const IOA_ALL_PROCS: i64 = 0 105// parse one /proc/<pid>/stat buffer: out[0]=comm start, out[1]=comm length, out[2]=ppid (or -1). 106// Returns 0, or -1 when the buffer does not carry "(comm)". comm is the span between the FIRST '(' and 107// the LAST ')' (a comm may itself hold ')'); then " S ppid ...": one space, the state, one space, digits. 108func ioa_stat_parse(sbuf: *u8, sn: i64, out: *i64) -> i64 { 109 var lp: i64 = 0 - 1 110 var rp: i64 = 0 - 1 111 var k: i64 = 0 112 while k < sn { 113 if lp < 0 { if (sbuf[k] as i64) == 40 { lp = k } } 114 if (sbuf[k] as i64) == 41 { rp = k } 115 k = k + 1 116 } 117 out[2] = 0 - 1 118 if lp < 0 { return 0 - 1 } 119 if rp <= lp { return 0 - 1 } 120 out[0] = lp + 1 121 out[1] = rp - lp - 1 122 // after ')': space, state char, space, then the ppid digits -- find the position after the 2nd space 123 var p: i64 = rp + 1 124 var sp: i64 = 0 125 var dig: i64 = 0 - 1 126 while p < sn { 127 if (sbuf[p] as i64) == 32 { sp = sp + 1; if sp == 2 { dig = p + 1; p = sn } } 128 p = p + 1 129 } 130 if dig < 0 { return 0 } 131 var v: i64 = 0 132 var seen: i64 = 0 133 var q: i64 = dig 134 while q < sn { 135 let c: i64 = sbuf[q] as i64 136 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); seen = 1; q = q + 1 } else { q = sn } } else { q = sn } 137 } 138 if seen == 1 { out[2] = v } 139 return 0 140} 141// read /proc/<pid>/stat for a NUMERIC pid into sbuf; returns bytes read or -1. 142func ioa_read_pid_stat(pid: i64, sbuf: *u8, path: *u8) -> i64 { 143 var po: i64 = 0 144 let pre: *u8 = "/proc/" as *u8 145 var pi: i64 = 0 146 while pre[pi] != (0 as u8) { path[po] = pre[pi]; po = po + 1; pi = pi + 1 } 147 po = nxi_buf(path, po, pid) 148 let suf: *u8 = "/stat" as *u8 149 var si: i64 = 0 150 while suf[si] != (0 as u8) { path[po] = suf[si]; po = po + 1; si = si + 1 } 151 path[po] = 0 as u8 152 let sfd: i64 = sys_openat_rd(path) 153 if sfd < 0 { return 0 - 1 } 154 let sn: i64 = sys_read(sfd, sbuf, IOA_STATBUF) 155 sys_close(sfd) 156 return sn 157} 158// does comm[cs..cs+cl) equal the first nl bytes of name (and cl == nl)? 159func ioa_comm_eq(sbuf: *u8, cs: i64, cl: i64, name: *u8, nl: i64) -> i64 { 160 if cl != nl { return 0 } 161 var j: i64 = 0 162 while j < nl { if sbuf[cs + j] != name[j] { return 0 } j = j + 1 } 163 return 1 164} 165// how many live processes carry this comm (the first IOA_COMM_MAX bytes of `name`); -1 if /proc is 166// unreadable. mode IOA_ROOTS_ONLY counts only processes whose PARENT does not carry the comm (the 167// population of workers); IOA_ALL_PROCS counts the whole tree (workers + their forked handlers). 168// Walks /proc with getdents64 UNTIL IT RETURNS 0 -- one call is not a directory listing. 169func ioa_count_comm_mode(name: *u8, mode: i64) -> i64 { 170 var nl: i64 = ioa_slen(name) 171 if nl > IOA_COMM_MAX { nl = IOA_COMM_MAX } 172 let fd: i64 = sys_openat_rd("/proc" as *u8) 173 if fd < 0 { return 0 - 1 } 174 let dbuf: *u8 = sys_mmap(IOA_DIRBUF) 175 let sbuf: *u8 = sys_mmap(IOA_STATBUF) 176 let pbuf: *u8 = sys_mmap(IOA_STATBUF) 177 let path: *u8 = sys_mmap(IOA_PATHBUF) 178 let st: *i64 = sys_mmap(32) as *i64 179 let pst: *i64 = sys_mmap(32) as *i64 180 var cnt: i64 = 0 181 var more: i64 = 1 182 while more == 1 { 183 let n: i64 = sys_getdents64(fd, dbuf, IOA_DIRBUF) 184 if n <= 0 { more = 0 } else { 185 var off: i64 = 0 186 while off < n { 187 let base: i64 = dbuf as i64 188 let rec: *u8 = (base + off) as *u8 189 let reclen: i64 = (rec[IOA_RECLEN_OFF] as i64) + ((rec[IOA_RECLEN_OFF+1] as i64) << 8) 190 let nm: *u8 = (base + off + IOA_NAME_OFF) as *u8 191 var isnum: i64 = 1 192 if nm[0] == (0 as u8) { isnum = 0 } 193 var pidv: i64 = 0 194 var q: i64 = 0 195 while nm[q] != (0 as u8) { 196 let c: i64 = nm[q] as i64 197 if c < 48 { isnum = 0 } 198 if c > 57 { isnum = 0 } 199 if isnum == 1 { pidv = pidv * 10 + (c - 48) } 200 q = q + 1 201 } 202 if isnum == 1 { 203 let sn: i64 = ioa_read_pid_stat(pidv, sbuf, path) 204 if sn > 0 { if ioa_stat_parse(sbuf, sn, st) == 0 { 205 if ioa_comm_eq(sbuf, st[0], st[1], name, nl) == 1 { 206 var root: i64 = 1 207 if mode == IOA_ROOTS_ONLY { if st[2] > 0 { 208 let pn: i64 = ioa_read_pid_stat(st[2], pbuf, path) 209 if pn > 0 { if ioa_stat_parse(pbuf, pn, pst) == 0 { 210 if ioa_comm_eq(pbuf, pst[0], pst[1], name, nl) == 1 { root = 0 } 211 } } 212 } } 213 if root == 1 { cnt = cnt + 1 } 214 } 215 } } 216 } 217 if reclen <= 0 { off = n } else { off = off + reclen } 218 } 219 } 220 } 221 sys_close(fd) 222 return cnt 223} 224// the two named readings. A SPAWNER wants roots (how many workers exist); a tree census wants all. 225func ioa_count_comm(name: *u8) -> i64 { return ioa_count_comm_mode(name, IOA_ALL_PROCS) } 226func ioa_count_comm_roots(name: *u8) -> i64 { return ioa_count_comm_mode(name, IOA_ROOTS_ONLY) } 227 228// ---- THE WRITEBACK-CONGESTION AXIS (2026-09-03) ------------------------------------------------------- 229// WHY, MEASURED THIS SESSION (load 20.98 on 8 CPUs, every build/write/grep lane taking 503s): procs_blocked 230// is the ONLY congestion input this ruler has, and it is one of the NOISIEST fields in /proc. Two reads of 231// /proc/stat 20 s apart on the same box gave 6 and then 0; the procchurn journal shows it swinging 0..8 232// between consecutive beats. A SINGLE SAMPLE OF A BURSTY FIELD DECIDES ADMISSION BY COIN FLIP. 233// Meanwhile /proc/meminfo carried Dirty=644820 kB and Writeback=19340 kB -- a slow-moving INTEGRAL of 234// exactly the write congestion that makes a write return 503, free to read on every call, and an input to 235// NEITHER governor (build_admit.conf reads load and MemAvailable; heavyio.conf reads load and blocked). 236// procs_blocked says SOMEONE IS BLOCKED RIGHT NOW. Dirty says THE ARRAY IS N MEGABYTES BEHIND. 237// Only the second is a level you can steer on, and only the second survives being sampled once. 238// 239// SCOPE, STATED PLAINLY: this axis is DECLARED, MEASURED AND COUNTED -- IT IS NOT ARMED. Its bars ship at 240// 0 in knowledge/ioadmit.conf and ioa_congestion returns CLEAR while either bar is <= 0, so nothing this 241// function returns can refuse any lane today. That is deliberate: this box has ONE storm sample and no 242// healthy distribution, and an uncalibrated classifier must report numbers, never verdicts. Arming it on a 243// guess would put a permanently-red detector on the write path -- the one place nobody can afford to learn 244// to ignore a detector. nx_procchurn now writes dirty_kb= and writeback_kb= on every beat; when that log 245// holds a GREEN distribution, derive the bars the way nx_loadceil derived max_centiload from resmon.log 246// and arm them THEN, in the conf, with the sample count written beside them. 247const IOA_MEMINFO: *u8 = "/proc/meminfo" 248const IOA_MEMINFO_CAP: i64 = 65536 249// Named states, never a bare boolean; UNREADABLE is first-class so a blind axis ABSTAINS, never acquits. 250const IOA_CONG_CLEAR: i64 = 0 251const IOA_CONG_RISING: i64 = 1 252const IOA_CONG_STORM: i64 = 2 253const IOA_CONG_UNREADABLE: i64 = 0 - 1 254 255// read /proc/meminfo whole; out[0]=Dirty kB, out[1]=Writeback kB. Returns 0, or IOA_UNREADABLE when the 256// file cannot be read, FILLS the cap (a prefix could silently lose either key), or lacks either field -- 257// out[] carries IOA_UNREADABLE then, exactly as ioa_measure does for /proc/stat. 258// THE KEYS CARRY THEIR COLONS ON PURPOSE. rm_field matches the line that STARTS with the key, and 259// /proc/meminfo contains BOTH "Writeback:" and "WritebackTmp:" -- a bare "Writeback" key is a prefix of 260// the wrong line and would silently read whichever the kernel happens to print first. 261func ioa_dirty(out: *i64) -> i64 { 262 let buf: *u8 = sys_mmap(IOA_MEMINFO_CAP) 263 let n: i64 = rm_read(IOA_MEMINFO, buf, IOA_MEMINFO_CAP) 264 out[0] = IOA_UNREADABLE 265 out[1] = IOA_UNREADABLE 266 var rc: i64 = IOA_UNREADABLE 267 if n > 0 { if n < IOA_MEMINFO_CAP - 1 { 268 let d: i64 = rm_field(buf, n, "Dirty:" as *u8) 269 let w: i64 = rm_field(buf, n, "Writeback:" as *u8) 270 if d >= 0 { if w >= 0 { 271 out[0] = d 272 out[1] = w 273 rc = 0 274 } } 275 } } 276 sys_munmap(buf, IOA_MEMINFO_CAP) 277 return rc 278} 279// PURE: classify write congestion from the two readings and two bars. A negative reading is UNOBSERVABLE 280// and this function never invents a fallback -- the caller owns its own and must NAME it. 281// A bar of <= 0 means UNCALIBRATED: return CLEAR, so an unarmed axis is INERT rather than permissive- 282// looking-like-armed, and the caller prints the raw kB beside it. 283func ioa_congestion(dirty_kb: i64, wb_kb: i64, warn_kb: i64, storm_kb: i64) -> i64 { 284 if dirty_kb < 0 { return IOA_CONG_UNREADABLE } 285 if wb_kb < 0 { return IOA_CONG_UNREADABLE } 286 if warn_kb <= 0 { return IOA_CONG_CLEAR } 287 if storm_kb <= 0 { return IOA_CONG_CLEAR } 288 let total: i64 = dirty_kb + wb_kb 289 if total >= storm_kb { return IOA_CONG_STORM } 290 if total >= warn_kb { return IOA_CONG_RISING } 291 return IOA_CONG_CLEAR 292} 293// ONE vocabulary for every consumer: two organs printing two spellings of the same state is the 294// duplicate-ruler defect wearing prose, and the gate rollup already has eleven spellings of success. 295func ioa_cong_name(state: i64) -> *u8 { 296 if state == IOA_CONG_STORM { return "STORM" as *u8 } 297 if state == IOA_CONG_RISING { return "RISING" as *u8 } 298 if state == IOA_CONG_CLEAR { return "CLEAR" as *u8 } 299 return "UNREADABLE" as *u8 300} 301 302// ---- THE BLOCK-DEVICE AXIS (2026-09-03, /compare/observability rung OB1) ----------------------------- 303// WHY: diagnosing the 2026-09-03 I/O storm required hand-reading /proc/diskstats over ssh, and the single 304// number that settled the whole investigation -- md4 writing 14.2 MB/s with 2,157 I/Os IN FLIGHT while the 305// CPU sat 85-90 percent idle -- was invisible to every organ in the estate. nx_raidwatch reads md DEGRADED 306// state; nothing read throughput or queue depth. A seat could see THAT the box was blocked and never on WHAT. 307// 308// *A LEVEL IS NOT A RATE, AND THIS SPLIT IS THE POINT.* ioa_diskstats returns the kernel's CUMULATIVE 309// counters, which is the only honest thing one sample can give. Turning them into a throughput lives in a 310// SEPARATE PURE function that takes a DELTA and an elapsed time -- so a caller cannot publish a counter as 311// a rate by accident, which is exactly how a monotonically rising number becomes a fake megabytes-per-second. 312const IOA_DISKSTATS: *u8 = "/proc/diskstats" 313// /proc reports size 0 to stat, so a bound is legitimate and it ANNOUNCES: filling the cap returns 314// UNREADABLE, never a prefix. A truncated diskstats would silently drop whichever devices sort last, and on 315// this box md4 -- the one that matters -- sorts after eight sata members. 316const IOA_DISKSTATS_CAP: i64 = 262144 317const IOA_DS_SCRATCH_BYTES: i64 = 16 318// The 11 counters that follow the device name, and 0-BASED indices into them named for what they are. 319const IOA_DS_FIELDS: i64 = 11 320const IOA_DS_SECTORS_READ: i64 = 2 321const IOA_DS_SECTORS_WRITTEN: i64 = 6 322const IOA_DS_IOS_IN_FLIGHT: i64 = 8 323const IOA_DS_MS_DOING_IO: i64 = 9 324const IOA_SECTOR_BYTES: i64 = 512 325const IOA_MS_PER_S: i64 = 1000 326const IOA_BYTES_PER_KB: i64 = 1024 327// Character codes, named so the scanner reads as text rather than as arithmetic. 328const IOA_CH_SPACE: i64 = 32 329const IOA_CH_NL: i64 = 10 330const IOA_CH_D0: i64 = 48 331const IOA_CH_D9: i64 = 57 332 333// advance past spaces; returns the first non-space position at or after p (or n) 334func ioa_ds_skip_sp(buf: *u8, n: i64, p: i64) -> i64 { 335 var i: i64 = p 336 var go: i64 = 1 337 while go == 1 { 338 if i >= n { go = 0 } else { 339 if (buf[i] as i64) == IOA_CH_SPACE { i = i + 1 } else { go = 0 } 340 } 341 } 342 return i 343} 344// read one decimal integer at p into out[0]; returns the position after it, or p unchanged if no digits. 345// Returning p unchanged is what lets the caller detect a malformed line instead of consuming a zero. 346func ioa_ds_int(buf: *u8, n: i64, p: i64, out: *i64) -> i64 { 347 var i: i64 = p 348 var v: i64 = 0 349 var seen: i64 = 0 350 var go: i64 = 1 351 while go == 1 { 352 if i >= n { go = 0 } else { 353 let c: i64 = buf[i] as i64 354 if c >= IOA_CH_D0 { 355 if c <= IOA_CH_D9 { v = v * 10 + (c - IOA_CH_D0); seen = 1; i = i + 1 } else { go = 0 } 356 } else { go = 0 } 357 } 358 } 359 out[0] = v 360 if seen == 0 { return p } 361 return i 362} 363// does the space-terminated token at p equal name EXACTLY? returns the token end, or -1. 364// EXACTLY matters: a prefix match would make "md4" also match "md40", the same shadowing defect the 365// meminfo keys carry their colon to avoid. 366func ioa_ds_tok_eq(buf: *u8, n: i64, p: i64, name: *u8) -> i64 { 367 var i: i64 = p 368 var j: i64 = 0 369 var ok: i64 = 1 370 var go: i64 = 1 371 while go == 1 { 372 if i >= n { go = 0 } else { 373 if (buf[i] as i64) == IOA_CH_SPACE { go = 0 } else { 374 if name[j] == (0 as u8) { ok = 0; go = 0 } else { 375 if buf[i] != name[j] { ok = 0; go = 0 } else { i = i + 1; j = j + 1 } 376 } 377 } 378 } 379 } 380 if ok == 0 { return 0 - 1 } 381 if name[j] != (0 as u8) { return 0 - 1 } 382 return i 383} 384// Read the counters for ONE named device (e.g. "md4", "sata1"). 385// out[0]=sectors_read out[1]=sectors_written out[2]=ios_in_flight out[3]=ms_doing_io 386// Returns 0, or IOA_UNREADABLE when the file cannot be read, FILLS the cap, or the device is not present. 387// *A DEVICE THAT IS NOT THERE IS NOT A ZERO.* Returning 0 for an absent device would let a typo publish a 388// perfectly healthy-looking idle array, so absence takes the third state like every other axis here. 389// PURE over a BUFFER, split from the reader on purpose. A gate can then prove this parser on bytes whose 390// answer is KNOWN, instead of on whatever this host happens to have mounted -- the estate's standing 391// pattern for every /proc reader, and the only way a parser tooth is portable. 392func ioa_ds_parse(buf: *u8, n: i64, dev: *u8, out: *i64) -> i64 { 393 out[0] = IOA_UNREADABLE 394 out[1] = IOA_UNREADABLE 395 out[2] = IOA_UNREADABLE 396 out[3] = IOA_UNREADABLE 397 var rc: i64 = IOA_UNREADABLE 398 if n > 0 { 399 let sc: *i64 = sys_mmap(IOA_DS_SCRATCH_BYTES) as *i64 400 var i: i64 = 0 401 var go: i64 = 1 402 while go == 1 { 403 if i >= n { go = 0 } else { 404 var p: i64 = ioa_ds_skip_sp(buf, n, i) 405 p = ioa_ds_int(buf, n, p, sc) 406 p = ioa_ds_skip_sp(buf, n, p) 407 p = ioa_ds_int(buf, n, p, sc) 408 p = ioa_ds_skip_sp(buf, n, p) 409 let te: i64 = ioa_ds_tok_eq(buf, n, p, dev) 410 if te > 0 { 411 var q: i64 = te 412 var k: i64 = 0 413 while k < IOA_DS_FIELDS { 414 q = ioa_ds_skip_sp(buf, n, q) 415 q = ioa_ds_int(buf, n, q, sc) 416 if k == IOA_DS_SECTORS_READ { out[0] = sc[0] } 417 if k == IOA_DS_SECTORS_WRITTEN { out[1] = sc[0] } 418 if k == IOA_DS_IOS_IN_FLIGHT { out[2] = sc[0] } 419 if k == IOA_DS_MS_DOING_IO { out[3] = sc[0] } 420 k = k + 1 421 } 422 rc = 0 423 go = 0 424 } 425 if go == 1 { 426 var e: i64 = i 427 var g2: i64 = 1 428 while g2 == 1 { 429 if e >= n { g2 = 0 } else { 430 if (buf[e] as i64) == IOA_CH_NL { g2 = 0 } else { e = e + 1 } 431 } 432 } 433 i = e + 1 434 } 435 } 436 } 437 sys_munmap(sc, IOA_DS_SCRATCH_BYTES) 438 } 439 return rc 440} 441// The thin reader: owns the file and the cap, and delegates every DECISION to the pure parser above. 442// A read that FILLS the cap is UNREADABLE, never a prefix -- a truncated diskstats would silently drop 443// whichever devices sort last, and on this box md4, the one that matters, sorts after eight sata members. 444func ioa_diskstats(dev: *u8, out: *i64) -> i64 { 445 let buf: *u8 = sys_mmap(IOA_DISKSTATS_CAP) 446 let n: i64 = rm_read(IOA_DISKSTATS, buf, IOA_DISKSTATS_CAP) 447 var rc: i64 = IOA_UNREADABLE 448 if n > 0 { if n < IOA_DISKSTATS_CAP - 1 { rc = ioa_ds_parse(buf, n, dev, out) } } 449 if rc != 0 { 450 out[0] = IOA_UNREADABLE 451 out[1] = IOA_UNREADABLE 452 out[2] = IOA_UNREADABLE 453 out[3] = IOA_UNREADABLE 454 } 455 sys_munmap(buf, IOA_DISKSTATS_CAP) 456 return rc 457} 458// PURE: kilobytes per second from a SECTOR DELTA and an elapsed time. 459// The signature is the guard: there is no way to call this with a single cumulative reading, so a counter 460// can never be published as a throughput. A non-positive interval, or a negative delta (counter wrap, or a 461// mis-ordered sample pair), is UNOBSERVABLE -- never a number, never a zero. 462func ioa_sector_rate_kbs(sectors_delta: i64, elapsed_ms: i64) -> i64 { 463 if elapsed_ms <= 0 { return IOA_UNREADABLE } 464 if sectors_delta < 0 { return IOA_UNREADABLE } 465 let kb: i64 = sectors_delta * IOA_SECTOR_BYTES / IOA_BYTES_PER_KB 466 return kb * IOA_MS_PER_S / elapsed_ms 467} 468 469// ---- THE SMOOTHED WITNESS (2026-09-03, loadgov/admission-witness-single-sample) --------------------- 470// WHY, MEASURED: admission reads ONE instantaneous sample of procs_blocked and refuses on it. Twelve 471// samples two seconds apart on this box read 14,10,5,10,9,5,5,5,5,4,2,8 -- a SEVEN-FOLD swing inside 24 472// seconds, with FIVE OF TWELVE at or above the refusal line (blocked_max = ncpu = 8) while the median sat 473// at 5-6, comfortably under it. procs_running behaved the same: a refusal read 23, a control seconds later 474// read 1, and a paired before/detector/after control caught a genuine 13 -> 9 -> 7 decay. 475// ONE SAMPLE CANNOT DISTINGUISH A TWO-SECOND BURST FROM A SUSTAINED STORM, so admission on one reading is 476// a coin flip -- measured at roughly 42 percent refuse on a box whose median was never over the bar. The 477// median of K SPACED samples ignores a burst and still reads high when every sample is high, which is 478// precisely the discrimination admission needs and the only thing it currently lacks. 479// 480// THE SPACING IS THE WHOLE POINT, NOT AN IMPLEMENTATION DETAIL: K back-to-back reads land microseconds 481// apart and smooth NOTHING. sys_nanosleep is ABSENT-PROVEN in this estate (23,544 files, 482// corpus_complete=1) and so is sys_kill; sys_poll with no descriptors is the portable sleep we do have. 483// 484// SHIPPED AS A SEPARATE FUNCTION, NOT AS A CHANGE TO ioa_measure. Every existing caller keeps the exact 485// arithmetic it has today, so no lane that is admitted now can start being refused by this edit. ARMING it 486// -- pointing ba_verdict at the median instead of the sample -- CHANGES WHETHER BUILDS RUN and is a 487// deliberate second step with its own gate, not a side effect of adding the capability. 488const IOA_MEDIAN_MAX_K: i64 = 15 489const IOA_MEDIAN_SLOTS: i64 = 128 // IOA_MEDIAN_MAX_K i64 slots with headroom 490const IOA_MEAS_SLOTS: i64 = 32 // the out[] ioa_measure fills 491 492// The portable sleep: poll with no descriptors blocks for the timeout and nothing else. 493func ioa_sleep_ms(ms: i64) -> i64 { return sys_poll(0 as *u8, 0, ms) } 494 495// PURE: the median of k values, WITHOUT SORTING. For each candidate count how many samples are strictly 496// less and how many equal; the median is the value where less <= k/2 and less+equal > k/2. 497// THE ABSENCE OF A SORT IS DELIBERATE: this estate has already been bitten by a cursor-clobbering 498// insertion sort that corrupted the very array it had just ordered (nx_loadceil, corrected 2026-08-25, 499// where the inner loop exited via j = 0 - 1 and the next line wrote a[j+1]). k is at most 15 here, so the 500// O(k squared) count is free and cannot have that defect by construction. 501func ioa_median(vals: *i64, k: i64) -> i64 { 502 if k <= 0 { return IOA_UNREADABLE } 503 let half: i64 = k / 2 504 var res: i64 = IOA_UNREADABLE 505 var i: i64 = 0 506 while i < k { 507 var less: i64 = 0 508 var eq: i64 = 0 509 var j: i64 = 0 510 while j < k { 511 if vals[j] < vals[i] { less = less + 1 } 512 if vals[j] == vals[i] { eq = eq + 1 } 513 j = j + 1 514 } 515 if less <= half { if less + eq > half { res = vals[i] } } 516 i = i + 1 517 } 518 return res 519} 520 521// Take k samples of /proc/stat spaced gap_ms apart; out[0]=ncpu out[1]=MEDIAN procs_blocked out[2]=samples 522// actually obtained. Returns 0, or IOA_UNREADABLE when k is out of range or not one sample succeeded. 523// out[2] PUBLISHES THE COVERAGE: a run where some reads failed still answers, and says how thin the answer 524// is, rather than presenting a median of two as though it were a median of five. 525// ---- OWNERSHIP ATTRIBUTION (2026-09-03, /compare/observability rung OB3) ------------------------------- 526// THE QUESTION NO APM ANSWERS: what share of this box is NOT OURS? New Relic, Datadog and the Prometheus 527// node exporter all attribute load to the services they instrument and are SILENT about the neighbours 528// actually eating the machine. That silence is not cosmetic here -- it is why our own admission ring is a 529// ONE-SIDED TREATY: heavyio.conf names 12 producers, ALL Nishi, and derives width from LIVE LOAD, so 530// foreign churn shrinks OUR width while nothing shrinks theirs. 531// MEASURED THE DAY THIS SHIPPED: the dominant fork sources were synoscgi 37, postgres 18, nginx 9 against 532// nx_tools_api 10 and the torrent lane 6 -- the WHOLE Nishi estate a minority -- while DSM ran its web 533// stack at nice -10, above every lane of ours. Admission was refusing us on load we did not create. 534// *A GOVERNOR THAT CANNOT SAY WHOSE LOAD IT IS CAN ONLY EVER THROTTLE THE ONE TENANT THAT ASKED IT TO.* 535// This attributes BOTH the population and the BLOCKED roster -- the roster is what admission actually 536// decides on, so attributing only the population would answer a question nobody asks. 537const IOA_OWN_SLOTS: i64 = 64 538const IOA_STATE_OFF: i64 = 2 // "(comm) S ..." -- the state char sits 2 bytes past the closing paren 539const IOA_CH_D: i64 = 68 // 'D', uninterruptible sleep: the state admission counts 540 541// does comm[cs..cs+cl) START with the first nl bytes of name? (ioa_comm_eq demands equal length; ownership 542// is a PREFIX question -- every organ here is nx_something and the kernel truncates comm at 15 bytes) 543func ioa_comm_starts(sbuf: *u8, cs: i64, cl: i64, name: *u8, nl: i64) -> i64 { 544 if cl < nl { return 0 } 545 var j: i64 = 0 546 while j < nl { if sbuf[cs + j] != name[j] { return 0 } j = j + 1 } 547 return 1 548} 549 550// Census /proc by OWNERSHIP, where ownership is the comm prefix ("nx_" for this estate). 551// out[0]=procs out[1]=ours out[2]=foreign out[3]=blocked out[4]=blocked_ours out[5]=blocked_foreign 552// Returns 0, or IOA_UNREADABLE if /proc cannot be opened -- in which case out[] is left at UNREADABLE and 553// the caller ABSTAINS. A census that silently reports zero foreign processes because it could not look is 554// the most flattering possible lie about whose fault the load is. 555// Both partitions RECONCILE BY CONSTRUCTION: ours+foreign==procs and blocked_ours+blocked_foreign==blocked, 556// because every counted pid increments exactly one bucket of each pair. 557func ioa_ownership_census(prefix: *u8, out: *i64) -> i64 { 558 var nl: i64 = ioa_slen(prefix) 559 if nl > IOA_COMM_MAX { nl = IOA_COMM_MAX } 560 var z: i64 = 0 561 while z < 6 { out[z] = IOA_UNREADABLE; z = z + 1 } 562 let fd: i64 = sys_openat_rd("/proc" as *u8) 563 if fd < 0 { return IOA_UNREADABLE } 564 let dbuf: *u8 = sys_mmap(IOA_DIRBUF) 565 let sbuf: *u8 = sys_mmap(IOA_STATBUF) 566 let path: *u8 = sys_mmap(IOA_PATHBUF) 567 let st: *i64 = sys_mmap(IOA_OWN_SLOTS) as *i64 568 var procs: i64 = 0 569 var ours: i64 = 0 570 var blocked: i64 = 0 571 var blocked_ours: i64 = 0 572 var more: i64 = 1 573 while more == 1 { 574 let n: i64 = sys_getdents64(fd, dbuf, IOA_DIRBUF) 575 if n <= 0 { more = 0 } else { 576 var off: i64 = 0 577 while off < n { 578 let base: i64 = dbuf as i64 579 let rec: *u8 = (base + off) as *u8 580 let reclen: i64 = (rec[IOA_RECLEN_OFF] as i64) + ((rec[IOA_RECLEN_OFF+1] as i64) << 8) 581 let nm: *u8 = (base + off + IOA_NAME_OFF) as *u8 582 var isnum: i64 = 1 583 if nm[0] == (0 as u8) { isnum = 0 } 584 var pidv: i64 = 0 585 var q: i64 = 0 586 while nm[q] != (0 as u8) { 587 let c: i64 = nm[q] as i64 588 if c < 48 { isnum = 0 } 589 if c > 57 { isnum = 0 } 590 if isnum == 1 { pidv = pidv * 10 + (c - 48) } 591 q = q + 1 592 } 593 if isnum == 1 { 594 let sn: i64 = ioa_read_pid_stat(pidv, sbuf, path) 595 if sn > 0 { if ioa_stat_parse(sbuf, sn, st) == 0 { 596 procs = procs + 1 597 var mine: i64 = 0 598 if ioa_comm_starts(sbuf, st[0], st[1], prefix, nl) == 1 { mine = 1 } 599 if mine == 1 { ours = ours + 1 } 600 // state char: ioa_stat_parse leaves comm at st[0]..st[0]+st[1]; the closing paren is 601 // the byte after, and the state follows one space later. 602 let spos: i64 = st[0] + st[1] + IOA_STATE_OFF 603 if spos < sn { if (sbuf[spos] as i64) == IOA_CH_D { 604 blocked = blocked + 1 605 if mine == 1 { blocked_ours = blocked_ours + 1 } 606 } } 607 } } 608 } 609 if reclen <= 0 { off = n } else { off = off + reclen } 610 } 611 } 612 } 613 sys_close(fd) 614 out[0] = procs 615 out[1] = ours 616 out[2] = procs - ours 617 out[3] = blocked 618 out[4] = blocked_ours 619 out[5] = blocked - blocked_ours 620 return 0 621} 622 623const IOA_PERMIL: i64 = 1000 624// PURE: permil of a part in a whole, with the third state. whole<=0 is UNOBSERVABLE, never 0 permil -- 625// "nothing is foreign" and "I could not count" must never render as the same number, and the second is the 626// reading a governor must refuse to act on. 627func ioa_share_permil(part: i64, whole: i64) -> i64 { 628 if whole <= 0 { return IOA_UNREADABLE } 629 if part < 0 { return IOA_UNREADABLE } 630 return part * IOA_PERMIL / whole 631} 632 633func ioa_measure_median(out: *i64, k: i64, gap_ms: i64) -> i64 { 634 out[0] = IOA_UNREADABLE 635 out[1] = IOA_UNREADABLE 636 out[2] = 0 637 if k <= 0 { return IOA_UNREADABLE } 638 if k > IOA_MEDIAN_MAX_K { return IOA_UNREADABLE } 639 let blk: *i64 = sys_mmap(IOA_MEDIAN_SLOTS) as *i64 640 let m: *i64 = sys_mmap(IOA_MEAS_SLOTS) as *i64 641 var got: i64 = 0 642 var ncpu: i64 = IOA_UNREADABLE 643 var i: i64 = 0 644 while i < k { 645 if ioa_measure(m) == 0 { 646 blk[got] = m[1] 647 ncpu = m[0] 648 got = got + 1 649 } 650 if i + 1 < k { ioa_sleep_ms(gap_ms) } 651 i = i + 1 652 } 653 var rc: i64 = IOA_UNREADABLE 654 out[2] = got 655 if got > 0 { 656 out[0] = ncpu 657 out[1] = ioa_median(blk, got) 658 rc = 0 659 } 660 sys_munmap(blk, IOA_MEDIAN_SLOTS) 661 sys_munmap(m, IOA_MEAS_SLOTS) 662 return rc 663} 664 665// ---- OB9: THE DEVICE-MAPPER CACHE LAYER (added 2026-09-03) -------------------------------------- 666// Cache mode, the dirty LEVEL, and THE REDUNDANCY STATE OF THE CACHE DEVICE ITSELF, joined into one 667// verdict -- the rung's own words. Found the hard way: /volume1 ran on an unmirrored write-back cache 668// and NO ORGAN COULD SEE IT, so a day of refused builds was diagnosed only by hand-reading /proc. 669// MEASURED ABSENCE BEFORE WRITING: `ioa_dmcache` and the `ioadm_` prefix were both ABSENT-PROVEN across 670// 23,592 sources, corpus_complete=1. 671// 672// IT READS LEVELS AND REFUSES TO READ COUNTERS AS LEVELS. 673// `flashcache_stats` carries `dirty_writeback_kb` and `dirty_write_hits`. Both are LIFETIME COUNTERS 674// sitting among reads/writes/hits. Read as levels they report tens of GB stranded on a degraded device 675// -- alarming, actionable and WRONG. This rung's own description made that error, and so did the seat 676// that corrected it, hours apart. The LEVEL is `dirty_blocks` in `cache_info`, and it reads 0. 677// 678// `mode` IS NOT THE DISCRIMINATOR. `cache_info` keeps printing `mode=WRITE_BACK` after the cache has 679// been flushed and disabled -- it records how the device was CONFIGURED, not whether it is caching now. 680// The live signal is `flashcache_progress: status=uncacheable...`. A verdict built on `mode` reads GREEN 681// through the whole outage; the paired gate proves this by requiring two fixtures with BYTE-IDENTICAL 682// cache_info to return DIFFERENT verdicts. 683// 684// `root` is the /proc prefix so a gate can drive fixture trees and never the live host -- on a box that 685// is currently IN the RED state, a gate reading real /proc is indistinguishable from one hardcoded. 686// out[0] caching (1 yes, 0 disabled) out[1] dirty_blocks LEVEL out[2] cached_blocks 687// out[3] total_blocks out[4] degraded_mirrors 688// returns IOADM_GREEN | IOADM_RED | IOADM_AMBER | IOADM_UNOBS. 689// UNOBSERVABLE IS NOT GREEN: an absent cache means this host has none OR this reader is wrong, and 690// neither acquits. 691// VERIFIED 2026-09-03 standalone (nx_ioadm_probe) against the same fixtures as nx_cachewatch_gate: 692// live-host tree -> 1 caching=0 degraded=1 | healthy -> 0 caching=1 degraded=0 | absent -> 3, slots -1. 693// The laptop copy of THIS lib is an 11,303-byte fossil against 34,323 here, so the hunk was compiled and 694// run standalone rather than in place -- editing the fossil and pushing it would have deleted OB0/OB1. 695const IOADM_GREEN: i64 = 0 696const IOADM_RED: i64 = 1 697const IOADM_AMBER: i64 = 2 698const IOADM_UNOBS: i64 = 3 699const IOADM_BUF: i64 = 65536 700const IOADM_NAME: i64 = 512 701const IOADM_PATH: i64 = 1024 702const IOADM_RECLEN_OFF: i64 = 16 703const IOADM_NAME_OFF: i64 = 19 704const IOADM_GETDENTS: i64 = 217 705const IOADM_OPENDIR: i64 = 0x10000 706const IOADM_AT_FDCWD: i64 = 0 - 100 707const IOADM_PLUS: i64 = 43 708const IOADM_DOT: i64 = 46 709const IOADM_LBRACK: i64 = 91 710const IOADM_SLASH: i64 = 47 711const IOADM_RBRACK: i64 = 93 712const IOADM_D0: i64 = 48 713const IOADM_D9: i64 = 57 714const IOADM_OUT_SLOTS: i64 = 5 715 716func ioadm_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 717func ioadm_cat(dst: *u8, a: *u8, b: *u8, c: *u8) -> i64 { 718 var o: i64 = 0 719 var i: i64 = 0 720 while a[i] != (0 as u8) { dst[o] = a[i]; o = o + 1; i = i + 1 } 721 i = 0 722 while b[i] != (0 as u8) { dst[o] = b[i]; o = o + 1; i = i + 1 } 723 i = 0 724 while c[i] != (0 as u8) { dst[o] = c[i]; o = o + 1; i = i + 1 } 725 dst[o] = 0 as u8 726 return o 727} 728func ioadm_read(path: *u8, buf: *u8, cap: i64) -> i64 { 729 let fd: i64 = sys_openat_rd(path) 730 if fd < 0 { return 0 - 1 } 731 var got: i64 = 0 732 var go: i64 = 1 733 while go == 1 { 734 if got >= cap { go = 0 } else { 735 let r: i64 = sys_read(fd, (buf as i64 + got) as *u8, cap - got) 736 if r <= 0 { go = 0 } else { got = got + r } 737 } 738 } 739 sys_close(fd) 740 return got 741} 742// index just past `needle`, or -1. Exits on a FLAG, never by clobbering the cursor. 743func ioadm_find(buf: *u8, n: i64, needle: *u8) -> i64 { 744 let m: i64 = ioadm_slen(needle) 745 if m == 0 { return 0 - 1 } 746 var i: i64 = 0 747 var hit: i64 = 0 - 1 748 while i + m <= n { 749 var j: i64 = 0 750 var same: i64 = 1 751 while j < m { 752 if buf[i + j] != needle[j] { same = 0; j = m } else { j = j + 1 } 753 } 754 if same == 1 { if hit < 0 { hit = i + m } } 755 i = i + 1 756 } 757 return hit 758} 759func ioadm_int_after(buf: *u8, n: i64, key: *u8) -> i64 { 760 let p: i64 = ioadm_find(buf, n, key) 761 if p < 0 { return 0 - 1 } 762 var i: i64 = p 763 var v: i64 = 0 764 var got: i64 = 0 765 var go: i64 = 1 766 while go == 1 { 767 if i >= n { go = 0 } else { 768 let c: i64 = buf[i] as i64 769 if c < IOADM_D0 { go = 0 } else { 770 if c > IOADM_D9 { go = 0 } else { v = v * 10 + (c - IOADM_D0); got = 1; i = i + 1 } 771 } 772 } 773 } 774 if got == 0 { return 0 - 1 } 775 return v 776} 777// A raid1 line prints [N/M]; M < N means a member is missing. Counting the SHAPE needs no device names, 778// so this cannot go stale when a disk is renamed or re-lettered. 779func ioadm_degraded(buf: *u8, n: i64) -> i64 { 780 var i: i64 = 0 781 var deg: i64 = 0 782 while i + 5 < n { 783 if buf[i] == (IOADM_LBRACK as u8) { 784 let d1: i64 = buf[i + 1] as i64 785 let sl: i64 = buf[i + 2] as i64 786 let d2: i64 = buf[i + 3] as i64 787 let rb: i64 = buf[i + 4] as i64 788 if sl == IOADM_SLASH { 789 if rb == IOADM_RBRACK { 790 if d1 >= IOADM_D0 { if d1 <= IOADM_D9 { if d2 >= IOADM_D0 { if d2 <= IOADM_D9 { 791 if d2 < d1 { deg = deg + 1 } 792 } } } } 793 } 794 } 795 } 796 i = i + 1 797 } 798 return deg 799} 800// The instance directory encodes the volume group and volume, so it is HOST-SPECIFIC and must never be 801// a constant here. It is the entry carrying a '+'; the plain files beside it never do. 802func ioadm_instance(root: *u8, out: *u8) -> i64 { 803 let dpath: *u8 = sys_mmap(IOADM_PATH) 804 ioadm_cat(dpath, root, "/flashcache" as *u8, "" as *u8) 805 let dfd: i64 = __syscall(257, IOADM_AT_FDCWD, dpath, IOADM_OPENDIR, 0, 0, 0) 806 if dfd < 0 { return 0 } 807 let buf: *u8 = sys_mmap(IOADM_BUF) 808 let nm: *u8 = sys_mmap(IOADM_NAME) 809 var found: i64 = 0 810 var go: i64 = 1 811 // ONE getdents64 CALL IS NOT A DIRECTORY LISTING -- loop until it returns 0. 812 while go == 1 { 813 let nread: i64 = __syscall(IOADM_GETDENTS, dfd, buf, IOADM_BUF, 0, 0, 0) 814 if nread <= 0 { go = 0 } else { 815 var pos: i64 = 0 816 while pos < nread { 817 let reclen: i64 = (buf[pos + IOADM_RECLEN_OFF] as i64) | ((buf[pos + IOADM_RECLEN_OFF + 1] as i64) << 8) 818 var nl: i64 = 0 819 while buf[pos + IOADM_NAME_OFF + nl] != (0 as u8) { 820 nm[nl] = buf[pos + IOADM_NAME_OFF + nl] 821 nl = nl + 1 822 } 823 nm[nl] = 0 as u8 824 if found == 0 { 825 if nl > 0 { 826 if nm[0] != (IOADM_DOT as u8) { 827 var k: i64 = 0 828 var plus: i64 = 0 829 while k < nl { if nm[k] == (IOADM_PLUS as u8) { plus = 1 } k = k + 1 } 830 if plus == 1 { 831 var c: i64 = 0 832 while c <= nl { out[c] = nm[c]; c = c + 1 } 833 found = 1 834 } 835 } 836 } 837 } 838 if reclen <= 0 { pos = nread } else { pos = pos + reclen } 839 } 840 } 841 } 842 sys_close(dfd) 843 return found 844} 845 846func ioa_dmcache(root: *u8, out: *i64) -> i64 { 847 var i: i64 = 0 848 while i < IOADM_OUT_SLOTS { out[i] = 0 - 1; i = i + 1 } 849 850 let mdbuf: *u8 = sys_mmap(IOADM_BUF + 16) 851 let mdpath: *u8 = sys_mmap(IOADM_PATH) 852 ioadm_cat(mdpath, root, "/mdstat" as *u8, "" as *u8) 853 let mdn: i64 = ioadm_read(mdpath, mdbuf, IOADM_BUF) 854 if mdn > 0 { out[4] = ioadm_degraded(mdbuf, mdn) } 855 856 let cname: *u8 = sys_mmap(IOADM_NAME) 857 if ioadm_instance(root, cname) == 0 { return IOADM_UNOBS } 858 859 let pre: *u8 = sys_mmap(IOADM_PATH) 860 ioadm_cat(pre, root, "/flashcache/" as *u8, cname) 861 862 let ci: *u8 = sys_mmap(IOADM_BUF + 16) 863 let cipath: *u8 = sys_mmap(IOADM_PATH) 864 ioadm_cat(cipath, pre, "/cache_info" as *u8, "" as *u8) 865 let cin: i64 = ioadm_read(cipath, ci, IOADM_BUF) 866 if cin <= 0 { return IOADM_UNOBS } 867 868 // LEVELS ONLY -- the verdict may use nothing else. See the counter note above. 869 out[1] = ioadm_int_after(ci, cin, "dirty_blocks=" as *u8) 870 out[2] = ioadm_int_after(ci, cin, "cached_blocks=" as *u8) 871 out[3] = ioadm_int_after(ci, cin, "total_blocks=" as *u8) 872 if out[1] < 0 { return IOADM_UNOBS } 873 874 let pg: *u8 = sys_mmap(IOADM_BUF + 16) 875 let pgpath: *u8 = sys_mmap(IOADM_PATH) 876 ioadm_cat(pgpath, pre, "/flashcache_progress" as *u8, "" as *u8) 877 let pgn: i64 = ioadm_read(pgpath, pg, IOADM_BUF) 878 if pgn <= 0 { return IOADM_UNOBS } 879 880 out[0] = 1 881 if ioadm_find(pg, pgn, "uncacheable" as *u8) >= 0 { out[0] = 0 } 882 883 if out[0] == 0 { return IOADM_RED } 884 if out[4] > 0 { return IOADM_AMBER } 885 return IOADM_GREEN 886}