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}