code wiki / _hdl_build / nx_procchurn_lib.nx

nx_procchurn_lib.nx source

↩ module page · 474 lines · 22888 B

1// nx_procchurn_lib.nx -- the PURE, GATEABLE core of nx_procchurn (perf lane, debt seq1317/seq1318). 2// 3// WHY THIS EXISTS: on 2026-07-30 the NAS measured 78.3 forks/sec SUSTAINED with kernel time at 231 permil 4// of an 8-core host against 98 permil userspace -- the kernel doing 2.36x the work of the application -- 5// while nx_health reported {overall:OK, degraded:0, down:0} and all 14 services read UP. nx_resmon owns the 6// MEMORY axis and nx_netobs owns the NETWORK axis; NOTHING owned the PROCESS-CHURN axis, so the single 7// largest CPU consumer on the platform had no instrument at all and had to be found by hand from /proc. 8// LAW: liveness is not performance -- a board that only asks \"is it up\" cannot see the machine burning. 9// 10// Split per rule 9 (single responsibility) + rule 15 (DRY): the organ owns the sampling and the printing, 11// this lib owns the four pure functions that encode the policy, so the gate exercises the SAME code the 12// organ runs rather than a reimplementation. Rule 15 again: the generic helpers (puts/num/read/conf) are 13// IMPORTED from nx_resmon_lib.nx, NOT copied -- D001/seq389 counts 9495 duplicate function bodies and the 14// emit family (gw x427, g_puts x325) is the worst group; a new organ must not add to it. 15// license_tier: ORIGINAL Read-only. No hw writes (Rule 26). 16import "nx_resmon_lib.nx" 17import "nx_ioadmit_lib.nx" // ioa_read_pid_stat + ioa_stat_parse: THE estate /proc/<pid>/stat parser (comm spans first-paren to LAST-paren). Imported rather than reimplemented; there is no cycle -- ioadmit imports syscalls, resmon and itoa only. 18 19// system:user sentinel for zero userspace time -- the kernel is infinitely dominant, not \"at parity\". 20// Deliberately ABOVE any plausible real ratio so it always trips red instead of reading as a quiet 0. 21const PC_RATIO_SAT: i64 = 1000000 22const PC_ASCII_0: i64 = 48 23const PC_ASCII_9: i64 = 57 24const PC_ASCII_NL: i64 = 10 25const PC_PERMIL: i64 = 1000 26 27// Parse up to `maxv` integers from the line that STARTS with `key`, into out[]. Returns how many integers 28// were FOUND on that line (may exceed maxv -- only the first maxv are stored), or -1 if NO line starts 29// with key. FAIL-CLOSED: -1 is unmistakable and can never be mistaken for a real measurement of 0. 30// PREFIX DISCIPLINE IS THE WHOLE POINT: the caller passes \"cpu \" WITH the trailing space. Without it the 31// scan would match \"cpu0\" and report ONE CORE's jiffies as the whole box -- a wrong answer that looks 32// entirely plausible. The gate proves this specific confusion cannot happen. 33func pc_line_ints(buf: *u8, n: i64, key: *u8, out: *i64, maxv: i64) -> i64 { 34 let kl: i64 = rm_slen(key) 35 var i: i64 = 0 36 var found: i64 = 0 - 1 37 while i + kl <= n { 38 if found < 0 { 39 var atline: i64 = 0 40 if i == 0 { atline = 1 } 41 if i > 0 { if buf[i - 1] == (PC_ASCII_NL as u8) { atline = 1 } } 42 if atline == 1 { 43 var j: i64 = 0 44 var ok: i64 = 1 45 while j < kl { if buf[i + j] != key[j] { ok = 0; j = kl } else { j = j + 1 } } 46 if ok == 1 { found = i + kl } 47 } 48 } 49 i = i + 1 50 } 51 if found < 0 { return 0 - 1 } 52 var p: i64 = found 53 var cnt: i64 = 0 54 var cur: i64 = 0 55 var seen: i64 = 0 56 var run: i64 = 1 57 while run == 1 { 58 var c: i64 = PC_ASCII_NL 59 if p < n { c = buf[p] as i64 } 60 var isdig: i64 = 0 61 if c >= PC_ASCII_0 { if c <= PC_ASCII_9 { isdig = 1 } } 62 if isdig == 1 { cur = cur * 10 + (c - PC_ASCII_0); seen = 1 } 63 if isdig == 0 { 64 if seen == 1 { 65 if cnt < maxv { out[cnt] = cur } 66 cnt = cnt + 1 67 cur = 0 68 seen = 0 69 } 70 } 71 if c == PC_ASCII_NL { run = 0 } 72 if p >= n { run = 0 } 73 p = p + 1 74 } 75 return cnt 76} 77 78// delta over an elapsed window -> per-second rate. FAIL-CLOSED on BOTH nonsense inputs: 79// elapsed<=0 two samples taken at the same instant (division by zero / meaningless window) 80// delta<0 the counter went BACKWARDS = a 32-bit wrap or a reboot between samples 81// Returning -1 instead of a number is precisely what stops a wrap being published as a plausible rate. 82func pc_rate(delta: i64, elapsed_ms: i64) -> i64 { 83 if elapsed_ms <= 0 { return 0 - 1 } 84 if delta < 0 { return 0 - 1 } 85 return (delta * PC_PERMIL) / elapsed_ms 86} 87 88// system jiffies : user jiffies, as permil. 1000 = parity. >1000 = the kernel is doing more work than 89// the application, which on a service host is the churn fingerprint. usr==0 -> PC_RATIO_SAT. 90func pc_ratio_permil(sys_j: i64, usr_j: i64) -> i64 { 91 if sys_j < 0 { return 0 - 1 } 92 if usr_j < 0 { return 0 - 1 } 93 if usr_j == 0 { return PC_RATIO_SAT } 94 return (sys_j * PC_PERMIL) / usr_j 95} 96 97// measurements -> severity. 0=GREEN 1=AMBER 2=RED. RED dominates AMBER, and ANY axis raises the verdict 98// on its own because each one independently indicates churn. Contains NO policy numbers of its own 99// (rule 11) -- every threshold is passed in, which is exactly what the gate mutates to prove it is not 100// vacuous. Callers MUST screen out negative (unmeasurable) inputs first: a -1 compares below every 101// threshold and would otherwise read as a confident GREEN, which is the partial-as-complete defect. 102func pc_verdict(forks_ps: i64, ratio_pm: i64, ctxsw_ps: i64, 103 f_amber: i64, f_red: i64, r_amber: i64, r_red: i64, 104 c_amber: i64, c_red: i64) -> i64 { 105 var sev: i64 = 0 106 if forks_ps >= f_amber { sev = 1 } 107 if ratio_pm >= r_amber { sev = 1 } 108 if ctxsw_ps >= c_amber { sev = 1 } 109 if forks_ps >= f_red { sev = 2 } 110 if ratio_pm >= r_red { sev = 2 } 111 if ctxsw_ps >= c_red { sev = 2 } 112 return sev 113} 114 115const PC_NUMSCRATCH: i64 = 32 116const PC_ASCII_MINUS: i64 = 45 117 118// ---- journal formatting: PURE, so the gate proves the ON-DISK frame without touching a filesystem ---- 119// Rule 15 note: cat/catn are re-implemented in nearly every organ (D001/seq389 measured gw x427, 120// g_puts x325 -- 9495 duplicate function bodies). They live HERE rather than inline in the organ so the 121// instrument and its gate share ONE copy, and the next organ on this plane imports them instead of 122// adding copy 428. 123func pc_cat(d: *u8, o: i64, s: *u8) -> i64 { 124 var i: i64 = 0 125 while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } 126 return o + i 127} 128func pc_catn(d: *u8, o: i64, v: i64) -> i64 { 129 let t: *u8 = sys_mmap(PC_NUMSCRATCH) 130 var m: i64 = v 131 var p: i64 = o 132 if m < 0 { d[p] = PC_ASCII_MINUS as u8; p = p + 1; m = 0 - m } 133 var k: i64 = 0 134 if m == 0 { t[0] = PC_ASCII_0 as u8; k = 1 } 135 while m > 0 { t[k] = (PC_ASCII_0 + (m - (m / 10) * 10)) as u8; m = m / 10; k = k + 1 } 136 var i: i64 = 0 137 while i < k { d[p + i] = t[k - 1 - i]; i = i + 1 } 138 sys_munmap(t, PC_NUMSCRATCH) 139 return p + k 140} 141 142// ONE append-only journal frame -- the trend line. PURE: caller owns d, returns the byte length. 143// WHY A JOURNAL AND NOT JUST A VERDICT: a single verdict is a snapshot, and the fork storm this organ 144// exists to catch is BURSTY. Measured the same hour on 2026-07-30: 85/s since-boot average over 13.5h, 145// 78/s over a 117s window, 18/s inside one 3s window -- ALL THREE HONEST, all different timescales. 146// One sample can therefore neither confirm nor refute the condition; the journal carries the movement, 147// exactly as nx_law_warden's does, and a fix is proven landed by the TREND, never by a single run. 148// QUEUE COMPOSITION appended (seq1555): running/blocked are the ONLY fields here that say WHAT a deep 149// queue is waiting on. loadavg counts D-state as well as runnable, and iowait is only accrued while a 150// CPU is otherwise IDLE -- so neither can distinguish an I/O-blocked queue from a compute one, and 151// blocked>>running settles it. They are FREE (already in /proc/stat) and must be in the FRAME, not just 152// on stdout, or the 200-char plan snippet truncates them: an instrument that cannot deliver its findings 153// has not delivered them. 154func pc_frame(d: *u8, ts: i64, win_ms: i64, forks_ps: i64, ctxsw_ps: i64, intr_ps: i64, 155 ku_pm: i64, usr_pm: i64, sys_pm: i64, busy_pm: i64, sev: i64, 156 running: i64, blocked: i64) -> i64 { 157 var o: i64 = 0 158 o = pc_cat(d, o, "ts=" as *u8) 159 o = pc_catn(d, o, ts) 160 o = pc_cat(d, o, " window_ms=" as *u8) 161 o = pc_catn(d, o, win_ms) 162 o = pc_cat(d, o, " forks_ps=" as *u8) 163 o = pc_catn(d, o, forks_ps) 164 o = pc_cat(d, o, " ctxsw_ps=" as *u8) 165 o = pc_catn(d, o, ctxsw_ps) 166 o = pc_cat(d, o, " intr_ps=" as *u8) 167 o = pc_catn(d, o, intr_ps) 168 o = pc_cat(d, o, " ku_permil=" as *u8) 169 o = pc_catn(d, o, ku_pm) 170 o = pc_cat(d, o, " usr_permil=" as *u8) 171 o = pc_catn(d, o, usr_pm) 172 o = pc_cat(d, o, " sys_permil=" as *u8) 173 o = pc_catn(d, o, sys_pm) 174 o = pc_cat(d, o, " busy_permil=" as *u8) 175 o = pc_catn(d, o, busy_pm) 176 o = pc_cat(d, o, " sev=" as *u8) 177 o = pc_catn(d, o, sev) 178 o = pc_cat(d, o, " running=" as *u8) 179 o = pc_catn(d, o, running) 180 o = pc_cat(d, o, " blocked=" as *u8) 181 o = pc_catn(d, o, blocked) 182 o = pc_cat(d, o, "\n" as *u8) 183 return o 184} 185 186// ==== OB5: THE SEVERITY CONSUMER -- the ACTUATOR half of a loop that has only ever had a sensor ===== 187// WHY THIS EXISTS: this organ has written `sev=` on EVERY journal frame since 2026-07-30 and NOTHING 188// HAS EVER READ IT. 935 KB of correct severity sat on disk while the storm it describes kept recurring. 189// A severity no actuator consumes cannot change an outcome, so the sensor was, in the only sense that 190// matters, not wired at all. That gap is not a missing threshold -- it is a missing READER. 191// 192// IT READS THE FIELD OFF THE JOURNAL, NEVER OUT OF THE PRODUCER"S LIVE VARIABLE. A consumer that reads 193// the in-memory value proves the arithmetic and proves nothing about the WIRE, and the wire is exactly 194// where a producer and a consumer that are each correct in isolation still disagree (the 6-fields-on- 195// one-line defect, 2026-08-14: every field after the first parsed as -1 and the consumer reported 196// UNOBSERVABLE while holding a perfectly valid file). 197// 198// IT ABSTAINS RATHER THAN ACQUITS. Stale, absent, unparseable, clock-skewed and UNARMED all return 199// UNOBSERVABLE, never PROCEED -- because telling a caller "go ahead" at the moment you could not look 200// is the most flattering possible lie, and it is indistinguishable from having no guard at all. 201const PC_ACT_PROCEED: i64 = 0 202const PC_ACT_SLOW: i64 = 1 203const PC_ACT_DEFER: i64 = 2 204const PC_ACT_UNOBSERVABLE: i64 = 3 205const PC_SEVKEY_LEN: i64 = 5 206const PC_TSKEY_LEN: i64 = 3 207 208func pc_isdig(c: i64) -> i64 { 209 if c < PC_ASCII_0 { return 0 } 210 if c > PC_ASCII_9 { return 0 } 211 return 1 212} 213 214// substring search bounded to [s, e). Returns the offset or -1. Bounded on BOTH ends on purpose: the 215// caller anchors a key to a line, and an unbounded search would happily find last frame"s key in the 216// frame before it and report a stale value as current. 217func pc_find_in(buf: *u8, s: i64, e: i64, key: *u8) -> i64 { 218 var kl: i64 = 0 219 while key[kl] != (0 as u8) { kl = kl + 1 } 220 if kl == 0 { return 0 - 1 } 221 var i: i64 = s 222 while i + kl <= e { 223 var j: i64 = 0 224 var bad: i64 = 0 225 while j < kl { 226 if buf[i + j] != key[j] { bad = 1 } 227 j = j + 1 228 } 229 if bad == 0 { return i } 230 i = i + 1 231 } 232 return 0 - 1 233} 234 235// parse a decimal at p, bounded by e. -1 when there is no digit at all -- FAIL-CLOSED, never a silent 0, 236// because a 0 here would read as GREEN. The stop flag is deliberately NOT the cursor: writing the 237// loop-exit sentinel into the search cursor erases the answer (measured 4x in one day, seq/2026-08-07). 238func pc_int_at(buf: *u8, p: i64, e: i64) -> i64 { 239 var i: i64 = p 240 var neg: i64 = 0 241 if i < e { if buf[i] == (PC_ASCII_MINUS as u8) { neg = 1; i = i + 1 } } 242 var v: i64 = 0 243 var any: i64 = 0 244 var stop: i64 = 0 245 while stop == 0 { 246 if i >= e { stop = 1 } 247 if stop == 0 { if pc_isdig(buf[i] as i64) == 0 { stop = 1 } } 248 if stop == 0 { 249 v = v * 10 + ((buf[i] as i64) - PC_ASCII_0) 250 any = 1 251 i = i + 1 252 } 253 } 254 if any == 0 { return 0 - 1 } 255 if neg == 1 { return 0 - v } 256 return v 257} 258 259// LAST frame off a journal buffer. out[0]=sev out[1]=ts. Returns 1 READ / 0 NO-FRAME / -1 UNPARSEABLE, 260// three states because "the file is empty" and "the file is corrupt" demand opposite remedies and a 261// single negative word would have the reader guess the alarming one. 262// TWO ANCHORING DECISIONS, each one a defect that has already been paid for elsewhere in this estate: 263// * the severity key carries its LEADING SPACE (" sev=") so a future prevsev= or maxsev= cannot 264// shadow it -- the same reason nx_ioadmit_lib reads "Dirty:" and "Writeback:" WITH their colons, 265// since WritebackTmp: would otherwise answer for Writeback. 266// * ts= is anchored to LINE START rather than searched, because an unanchored "ts=" also matches the 267// tail of a hypothetical starts= or lasts= and would return a number from the wrong field. 268func pc_sev_last(buf: *u8, n: i64, out: *i64) -> i64 { 269 out[0] = 0 - 1 270 out[1] = 0 - 1 271 if n <= 0 { return 0 } 272 var e: i64 = n 273 var d1: i64 = 0 274 while d1 == 0 { 275 if e <= 0 { d1 = 1 } 276 if d1 == 0 { if buf[e - 1] != (PC_ASCII_NL as u8) { d1 = 1 } } 277 if d1 == 0 { e = e - 1 } 278 } 279 if e <= 0 { return 0 } 280 var s: i64 = e 281 var d2: i64 = 0 282 while d2 == 0 { 283 if s <= 0 { d2 = 1 } 284 if d2 == 0 { if buf[s - 1] == (PC_ASCII_NL as u8) { d2 = 1 } } 285 if d2 == 0 { s = s - 1 } 286 } 287 let sp: i64 = pc_find_in(buf, s, e, " sev=" as *u8) 288 if sp < 0 { return 0 - 1 } 289 let sv: i64 = pc_int_at(buf, sp + PC_SEVKEY_LEN, e) 290 if sv < 0 { return 0 - 1 } 291 var tv: i64 = 0 - 1 292 if pc_find_in(buf, s, s + PC_TSKEY_LEN, "ts=" as *u8) == s { tv = pc_int_at(buf, s + PC_TSKEY_LEN, e) } 293 if tv < 0 { return 0 - 1 } 294 out[0] = sv 295 out[1] = tv 296 return 1 297} 298 299// severity + freshness -> ACTION. Contains no policy numbers of its own (rule 11): the staleness bar is 300// passed in, which is exactly what the gate mutates to prove this is not vacuous. 301// max_age_s <= 0 means NO BAR HAS BEEN DECLARED, and an unarmed consumer abstains. It deliberately does 302// NOT default to PROCEED: an unarmed guard that returns PROCEED is byte-for-byte indistinguishable from 303// no guard at all, and it fails in the flattering direction, so nobody investigates. 304func pc_sev_consumer(sev: i64, age_s: i64, max_age_s: i64) -> i64 { 305 if max_age_s <= 0 { return PC_ACT_UNOBSERVABLE } 306 if age_s < 0 { return PC_ACT_UNOBSERVABLE } 307 if age_s > max_age_s { return PC_ACT_UNOBSERVABLE } 308 if sev < 0 { return PC_ACT_UNOBSERVABLE } 309 if sev == 0 { return PC_ACT_PROCEED } 310 if sev == 1 { return PC_ACT_SLOW } 311 return PC_ACT_DEFER 312} 313 314func pc_act_name(a: i64) -> *u8 { 315 if a == PC_ACT_PROCEED { return "PROCEED" as *u8 } 316 if a == PC_ACT_SLOW { return "SLOW" as *u8 } 317 if a == PC_ACT_DEFER { return "DEFER" as *u8 } 318 return "UNOBSERVABLE" as *u8 319} 320 321// ==== OB4: FORK SOURCES BY PARENT ================================================================== 322// THE MEASUREMENT THIS ANSWERS, AND THE ONE IT REFUSES TO PRETEND TO ANSWER. /proc/stat processes is a 323// CUMULATIVE FORK COUNTER and gives the true rate. A pid diff between two samples gives ATTRIBUTION but 324// counts only SURVIVORS, and the short-lived majority IS the churn: measured on this host, a pid diff 325// attributed 2.2 forks per second where the kernel counter said 14.5. So these are not two estimates of 326// one quantity -- they are two different quantities, and the only honest way to publish the attributed 327// one is BESIDE the kernel counter, as a declared LOWER BOUND with its coverage stated. 328// A SURVIVOR CENSUS PUBLISHED AS A FORK RATE UNDERSTATES BY WHATEVER FRACTION DIED INSIDE THE WINDOW, 329// AND IT UNDERSTATES SILENTLY, BECAUSE NOTHING IN THE SAMPLE RECORDS WHAT IT DID NOT SEE. 330// 331// THERE IS EXACTLY ONE /proc/<pid>/stat PARSER IN THIS ESTATE AND THIS FILE DOES NOT ADD A SECOND. 332// ioa_stat_parse spans comm from the FIRST '(' to the LAST ')' -- the only correct rule when a comm may 333// itself contain spaces and parentheses -- and ioa_read_pid_stat owns the path build and the read. 334// ⚠ THE FIRST DRAFT OF THIS BLOCK SHIPPED A pc_ppid_of THAT READ /proc/<pid>/status AND PARSED PPid: 335// ITSELF. It would have worked. It was still the duplicate-ruler defect, written in the same session 336// that recorded the law against it, and it was caught only by reading the incumbent lib before building. 337// CHECK-BEFORE-BUILD APPLIES TO THE FUNCTION YOU ARE ABOUT TO WRITE, NOT ONLY TO THE ORGAN. 338// 339// DUPLICATE-ENUMERATOR DEBT, NAMED RATHER THAN LEFT SILENT: nx_ctxtop_lib carries ct_find, which is 340// pc_pid_seen under another name, and ioa_ownership_census and ioa_count_comm_mode each walk /proc with 341// the enumeration baked into their classification so neither can lend it out. This lib cannot compose 342// ct_find, because nx_ctxtop_lib IMPORTS THIS FILE and the cycle is not resolvable -- consolidation has 343// to flow DOWNWARD into here. pc_pids is that landing place: a pid enumerator with nothing else in it. 344const PC_PATHBUF: i64 = 64 345const PC_DIRBUF: i64 = 65536 346const PC_DIRENT_NAME_OFF: i64 = 19 347const PC_ST_PPID: i64 = 2 // ioa_stat_parse out layout: 0=comm start, 1=comm length, 2=ppid 348 349// Is every byte of this NUL-terminated name a digit? /proc holds named entries beside the pids, and a 350// walk that does not screen them hands "self" or "meminfo" to a decimal parser and gets a confident 0. 351func pc_name_is_pid(s: *u8) -> i64 { 352 if s[0] == (0 as u8) { return 0 } 353 var i: i64 = 0 354 var ok: i64 = 1 355 while s[i] != (0 as u8) { 356 if pc_isdig(s[i] as i64) == 0 { ok = 0 } 357 i = i + 1 358 } 359 return ok 360} 361 362func pc_name_to_i64(s: *u8) -> i64 { 363 var v: i64 = 0 364 var i: i64 = 0 365 while s[i] != (0 as u8) { v = v * 10 + ((s[i] as i64) - PC_ASCII_0); i = i + 1 } 366 return v 367} 368 369// Enumerate the pids under /proc into out. Returns the count, or -1 if /proc cannot be opened. 370// ONE getdents64 CALL IS NOT A DIRECTORY LISTING -- it returns as much as fits, and a caller that does 371// not loop until it returns 0 silently reads a big directory as a prefix and publishes that as a total. 372// The cap is a DECLARED bound: filling it sets out_full, so the caller can say FLOOR rather than TOTAL. 373func pc_pids(out: *i64, cap: i64, out_full: *i64) -> i64 { 374 out_full[0] = 0 375 let fd: i64 = sys_openat_rd("/proc" as *u8) 376 if fd < 0 { return 0 - 1 } 377 let db: *u8 = sys_mmap(PC_DIRBUF) 378 var n: i64 = 0 379 var go: i64 = 1 380 while go == 1 { 381 let nr: i64 = sys_getdents64(fd, db, PC_DIRBUF) 382 if nr <= 0 { go = 0 } 383 if nr > 0 { 384 var pos: i64 = 0 385 while pos < nr { 386 let rec: *u8 = ((db as i64) + pos) as *u8 387 let rl: i64 = dirent_reclen(rec) 388 if rl <= 0 { pos = nr } 389 if rl > 0 { 390 let nm: *u8 = ((rec as i64) + PC_DIRENT_NAME_OFF) as *u8 391 if pc_name_is_pid(nm) == 1 { 392 if n < cap { out[n] = pc_name_to_i64(nm); n = n + 1 } 393 if n >= cap { out_full[0] = 1 } 394 } 395 pos = pos + rl 396 } 397 } 398 } 399 } 400 sys_munmap(db, PC_DIRBUF) 401 sys_close(fd) 402 return n 403} 404 405// Membership. O(n*m) over ~800 pids is under a millisecond and needs no allocation; the alternative is a 406// sort, and a sort is where this estate has repeatedly written its loop-exit sentinel into its cursor. 407func pc_pid_seen(pids: *i64, n: i64, pid: i64) -> i64 { 408 var i: i64 = 0 409 while i < n { if pids[i] == pid { return 1 } i = i + 1 } 410 return 0 411} 412 413// A NAMED ACCESSOR for the ppid slot of the incumbent parser -- not a second parser. sbuf must be at 414// least IOA_STATBUF bytes, because ioa_read_pid_stat reads that many. Returns -1 when the process is 415// already gone (the common case in a fork storm) and never a fabricated 0, which is a real pid. 416func pc_ppid_of(pid: i64, sbuf: *u8, pathb: *u8, out: *i64) -> i64 { 417 let n: i64 = ioa_read_pid_stat(pid, sbuf, pathb) 418 if n <= 0 { return 0 - 1 } 419 if ioa_stat_parse(sbuf, n, out) < 0 { return 0 - 1 } 420 return out[PC_ST_PPID] 421} 422 423// PURE tally: fold one parent into the (ppid, count) table. Returns the new distinct-parent count, or 424// the unchanged count when the table is full. Kept free of every syscall so the gate can prove the 425// arithmetic on crafted input without a /proc, which is what makes it testable at all. 426func pc_tally_parent(ppids: *i64, counts: *i64, np: i64, maxp: i64, ppid: i64) -> i64 { 427 var i: i64 = 0 428 while i < np { 429 if ppids[i] == ppid { counts[i] = counts[i] + 1; return np } 430 i = i + 1 431 } 432 if np >= maxp { return np } 433 ppids[np] = ppid 434 counts[np] = 1 435 return np + 1 436} 437 438// Attribute the pids present in cur but not in prev to their parents. 439// out[0]=survivors_new out[1]=attributed out[2]=distinct_parents out[3]=vanished_before_read 440// A pid that exits between the walk and the stat read lands in out[3] and NOT in out[1]: "we saw it and 441// could not attribute it" and "there was nothing to attribute" have opposite remedies and must never 442// share a counter. The parts sum -- out[1] + out[3] == out[0] -- and the gate asserts exactly that. 443func pc_fork_by_parent(prev: *i64, prev_n: i64, cur: *i64, cur_n: i64, 444 ppids: *i64, counts: *i64, maxp: i64, 445 sbuf: *u8, pathb: *u8, stout: *i64, out: *i64) -> i64 { 446 out[0] = 0 447 out[1] = 0 448 out[2] = 0 449 out[3] = 0 450 var np: i64 = 0 451 var i: i64 = 0 452 while i < cur_n { 453 if pc_pid_seen(prev, prev_n, cur[i]) == 0 { 454 out[0] = out[0] + 1 455 let pp: i64 = pc_ppid_of(cur[i], sbuf, pathb, stout) 456 if pp < 0 { out[3] = out[3] + 1 } 457 if pp >= 0 { np = pc_tally_parent(ppids, counts, np, maxp, pp); out[1] = out[1] + 1 } 458 } 459 i = i + 1 460 } 461 out[2] = np 462 return np 463} 464 465// Coverage of the attributed census against the kernel counter, in permil. 466// ABSTAINS (-1) when the kernel counter is unusable, because a 0 here would read as "nothing forked" 467// rather than "we could not compare", and those are opposite findings. 468// It deliberately does NOT clamp at 1000: a value above 1000 means the two measurements disagree about 469// the window they cover, and clamping would hide precisely that. 470func pc_attrib_permil(attributed: i64, kernel_forks: i64) -> i64 { 471 if kernel_forks <= 0 { return 0 - 1 } 472 if attributed < 0 { return 0 - 1 } 473 return (attributed * PC_PERMIL) / kernel_forks 474}