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}