code wiki / _hdl_build / nx_procchurn_lib.nx
nx_procchurn_lib.nx source
↩ module page · 183 lines · 8958 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"
17
18// system:user sentinel for zero userspace time -- the kernel is infinitely dominant, not \"at parity\".
19// Deliberately ABOVE any plausible real ratio so it always trips red instead of reading as a quiet 0.
20const PC_RATIO_SAT: i64 = 1000000
21const PC_ASCII_0: i64 = 48
22const PC_ASCII_9: i64 = 57
23const PC_ASCII_NL: i64 = 10
24const PC_PERMIL: i64 = 1000
25
26// Parse up to `maxv` integers from the line that STARTS with `key`, into out[]. Returns how many integers
27// were FOUND on that line (may exceed maxv -- only the first maxv are stored), or -1 if NO line starts
28// with key. FAIL-CLOSED: -1 is unmistakable and can never be mistaken for a real measurement of 0.
29// PREFIX DISCIPLINE IS THE WHOLE POINT: the caller passes \"cpu \" WITH the trailing space. Without it the
30// scan would match \"cpu0\" and report ONE CORE's jiffies as the whole box -- a wrong answer that looks
31// entirely plausible. The gate proves this specific confusion cannot happen.
32func pc_line_ints(buf: *u8, n: i64, key: *u8, out: *i64, maxv: i64) -> i64 {
33 let kl: i64 = rm_slen(key)
34 var i: i64 = 0
35 var found: i64 = 0 - 1
36 while i + kl <= n {
37 if found < 0 {
38 var atline: i64 = 0
39 if i == 0 { atline = 1 }
40 if i > 0 { if buf[i - 1] == (PC_ASCII_NL as u8) { atline = 1 } }
41 if atline == 1 {
42 var j: i64 = 0
43 var ok: i64 = 1
44 while j < kl { if buf[i + j] != key[j] { ok = 0; j = kl } else { j = j + 1 } }
45 if ok == 1 { found = i + kl }
46 }
47 }
48 i = i + 1
49 }
50 if found < 0 { return 0 - 1 }
51 var p: i64 = found
52 var cnt: i64 = 0
53 var cur: i64 = 0
54 var seen: i64 = 0
55 var run: i64 = 1
56 while run == 1 {
57 var c: i64 = PC_ASCII_NL
58 if p < n { c = buf[p] as i64 }
59 var isdig: i64 = 0
60 if c >= PC_ASCII_0 { if c <= PC_ASCII_9 { isdig = 1 } }
61 if isdig == 1 { cur = cur * 10 + (c - PC_ASCII_0); seen = 1 }
62 if isdig == 0 {
63 if seen == 1 {
64 if cnt < maxv { out[cnt] = cur }
65 cnt = cnt + 1
66 cur = 0
67 seen = 0
68 }
69 }
70 if c == PC_ASCII_NL { run = 0 }
71 if p >= n { run = 0 }
72 p = p + 1
73 }
74 return cnt
75}
76
77// delta over an elapsed window -> per-second rate. FAIL-CLOSED on BOTH nonsense inputs:
78// elapsed<=0 two samples taken at the same instant (division by zero / meaningless window)
79// delta<0 the counter went BACKWARDS = a 32-bit wrap or a reboot between samples
80// Returning -1 instead of a number is precisely what stops a wrap being published as a plausible rate.
81func pc_rate(delta: i64, elapsed_ms: i64) -> i64 {
82 if elapsed_ms <= 0 { return 0 - 1 }
83 if delta < 0 { return 0 - 1 }
84 return (delta * PC_PERMIL) / elapsed_ms
85}
86
87// system jiffies : user jiffies, as permil. 1000 = parity. >1000 = the kernel is doing more work than
88// the application, which on a service host is the churn fingerprint. usr==0 -> PC_RATIO_SAT.
89func pc_ratio_permil(sys_j: i64, usr_j: i64) -> i64 {
90 if sys_j < 0 { return 0 - 1 }
91 if usr_j < 0 { return 0 - 1 }
92 if usr_j == 0 { return PC_RATIO_SAT }
93 return (sys_j * PC_PERMIL) / usr_j
94}
95
96// measurements -> severity. 0=GREEN 1=AMBER 2=RED. RED dominates AMBER, and ANY axis raises the verdict
97// on its own because each one independently indicates churn. Contains NO policy numbers of its own
98// (rule 11) -- every threshold is passed in, which is exactly what the gate mutates to prove it is not
99// vacuous. Callers MUST screen out negative (unmeasurable) inputs first: a -1 compares below every
100// threshold and would otherwise read as a confident GREEN, which is the partial-as-complete defect.
101func pc_verdict(forks_ps: i64, ratio_pm: i64, ctxsw_ps: i64,
102 f_amber: i64, f_red: i64, r_amber: i64, r_red: i64,
103 c_amber: i64, c_red: i64) -> i64 {
104 var sev: i64 = 0
105 if forks_ps >= f_amber { sev = 1 }
106 if ratio_pm >= r_amber { sev = 1 }
107 if ctxsw_ps >= c_amber { sev = 1 }
108 if forks_ps >= f_red { sev = 2 }
109 if ratio_pm >= r_red { sev = 2 }
110 if ctxsw_ps >= c_red { sev = 2 }
111 return sev
112}
113
114const PC_NUMSCRATCH: i64 = 32
115const PC_ASCII_MINUS: i64 = 45
116
117// ---- journal formatting: PURE, so the gate proves the ON-DISK frame without touching a filesystem ----
118// Rule 15 note: cat/catn are re-implemented in nearly every organ (D001/seq389 measured gw x427,
119// g_puts x325 -- 9495 duplicate function bodies). They live HERE rather than inline in the organ so the
120// instrument and its gate share ONE copy, and the next organ on this plane imports them instead of
121// adding copy 428.
122func pc_cat(d: *u8, o: i64, s: *u8) -> i64 {
123 var i: i64 = 0
124 while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 }
125 return o + i
126}
127func pc_catn(d: *u8, o: i64, v: i64) -> i64 {
128 let t: *u8 = sys_mmap(PC_NUMSCRATCH)
129 var m: i64 = v
130 var p: i64 = o
131 if m < 0 { d[p] = PC_ASCII_MINUS as u8; p = p + 1; m = 0 - m }
132 var k: i64 = 0
133 if m == 0 { t[0] = PC_ASCII_0 as u8; k = 1 }
134 while m > 0 { t[k] = (PC_ASCII_0 + (m - (m / 10) * 10)) as u8; m = m / 10; k = k + 1 }
135 var i: i64 = 0
136 while i < k { d[p + i] = t[k - 1 - i]; i = i + 1 }
137 sys_munmap(t, PC_NUMSCRATCH)
138 return p + k
139}
140
141// ONE append-only journal frame -- the trend line. PURE: caller owns d, returns the byte length.
142// WHY A JOURNAL AND NOT JUST A VERDICT: a single verdict is a snapshot, and the fork storm this organ
143// exists to catch is BURSTY. Measured the same hour on 2026-07-30: 85/s since-boot average over 13.5h,
144// 78/s over a 117s window, 18/s inside one 3s window -- ALL THREE HONEST, all different timescales.
145// One sample can therefore neither confirm nor refute the condition; the journal carries the movement,
146// exactly as nx_law_warden's does, and a fix is proven landed by the TREND, never by a single run.
147// QUEUE COMPOSITION appended (seq1555): running/blocked are the ONLY fields here that say WHAT a deep
148// queue is waiting on. loadavg counts D-state as well as runnable, and iowait is only accrued while a
149// CPU is otherwise IDLE -- so neither can distinguish an I/O-blocked queue from a compute one, and
150// blocked>>running settles it. They are FREE (already in /proc/stat) and must be in the FRAME, not just
151// on stdout, or the 200-char plan snippet truncates them: an instrument that cannot deliver its findings
152// has not delivered them.
153func pc_frame(d: *u8, ts: i64, win_ms: i64, forks_ps: i64, ctxsw_ps: i64, intr_ps: i64,
154 ku_pm: i64, usr_pm: i64, sys_pm: i64, busy_pm: i64, sev: i64,
155 running: i64, blocked: i64) -> i64 {
156 var o: i64 = 0
157 o = pc_cat(d, o, "ts=" as *u8)
158 o = pc_catn(d, o, ts)
159 o = pc_cat(d, o, " window_ms=" as *u8)
160 o = pc_catn(d, o, win_ms)
161 o = pc_cat(d, o, " forks_ps=" as *u8)
162 o = pc_catn(d, o, forks_ps)
163 o = pc_cat(d, o, " ctxsw_ps=" as *u8)
164 o = pc_catn(d, o, ctxsw_ps)
165 o = pc_cat(d, o, " intr_ps=" as *u8)
166 o = pc_catn(d, o, intr_ps)
167 o = pc_cat(d, o, " ku_permil=" as *u8)
168 o = pc_catn(d, o, ku_pm)
169 o = pc_cat(d, o, " usr_permil=" as *u8)
170 o = pc_catn(d, o, usr_pm)
171 o = pc_cat(d, o, " sys_permil=" as *u8)
172 o = pc_catn(d, o, sys_pm)
173 o = pc_cat(d, o, " busy_permil=" as *u8)
174 o = pc_catn(d, o, busy_pm)
175 o = pc_cat(d, o, " sev=" as *u8)
176 o = pc_catn(d, o, sev)
177 o = pc_cat(d, o, " running=" as *u8)
178 o = pc_catn(d, o, running)
179 o = pc_cat(d, o, " blocked=" as *u8)
180 o = pc_catn(d, o, blocked)
181 o = pc_cat(d, o, "\n" as *u8)
182 return o
183}