code wiki / _hdl_build / nx_procchurn.nx
nx_procchurn.nx source
↩ module page · 206 lines · 11273 B
1// nx_procchurn.nx -- the PROCESS-CHURN axis the health plane never had (debt seq1317 / seq1318).
2//
3// WHY THIS EXISTS: 2026-07-30, nx_health said {overall:OK, degraded:0, down:0} with all 14 services UP
4// while the NAS was forking 78.3 processes/sec sustained, burning 231 permil of an 8-core box in the
5// KERNEL against 98 permil in userspace (a 2.36x kernel:user inversion) and taking 91718 context
6// switches/sec. nx_resmon owns MEMORY, nx_netobs owns NETWORK -- the process-churn axis had NO organ, so
7// the largest CPU consumer on the platform was invisible to every instrument we owned and was found by
8// hand. This is that instrument.
9// LAW: liveness is not performance. A board that only asks "is it up" cannot see the machine burning.
10//
11// WHAT IT MEASURES (two /proc/stat samples bracketing a real window -- deltas, never since-boot totals,
12// because a since-boot average hides a storm that started an hour ago):
13// forks/sec -- `processes` delta. THE headline: fork+exec is the most expensive thing a kernel does.
14// ctxsw/sec -- `ctxt` delta. Rises with churn AND with thrash; corroborates, never diagnoses alone.
15// intr/sec -- `intr` delta, first field. Device pressure, reported for context (not thresholded).
16// kernel:user -- system:user+nice jiffies as permil. 1000 = parity. THE inversion detector.
17// busy/idle -- share of measured capacity, self-normalising (no HZ and no core count needed --
18// every share is a fraction of the SAME jiffy total, so the arithmetic cannot drift
19// when the box changes size. That is what makes this instrument hardware-portable).
20//
21// Policy lives in nx_procchurn_lib.nx so the gate exercises the SAME code (rule 9 + rule 15); thresholds
22// are data-driven from knowledge/status/procchurn.conf (rule 11). Read-only. No hw writes (Rule 26).
23// Exit code IS the verdict so a caller can gate on it: 0=GREEN 1=AMBER 2=RED 3=UNMEASURED.
24// license_tier: ORIGINAL expect_exit: 0
25import "nx_procchurn_lib.nx"
26
27const PC_STATBUF: i64 = 8192
28const PC_CONFBUF: i64 = 4096
29const PC_FIELD_BYTES: i64 = 256
30const PC_NCPU_FIELDS: i64 = 10
31const PC_DEF_WINDOW_MS: i64 = 3000
32const PC_MIN_WINDOW_MS: i64 = 1000
33const PC_MAX_WINDOW_MS: i64 = 10000
34const PC_US_PER_MS: i64 = 1000
35const PC_DEF_F_AMBER: i64 = 20
36const PC_DEF_F_RED: i64 = 50
37const PC_DEF_R_AMBER: i64 = 1000
38const PC_DEF_R_RED: i64 = 1500
39const PC_DEF_C_AMBER: i64 = 20000
40const PC_DEF_C_RED: i64 = 50000
41const PC_EXIT_UNMEASURED: i64 = 3
42const PC_JBUF: i64 = 512
43const PC_MODE_644: i64 = 420
44
45// APPEND one frame to the trend journal. Append-only and never truncating: history is sacred (rule 13),
46// and the burst structure this organ measures is only visible ACROSS runs. Failure to journal is NOT
47// fatal -- a read-only or full filesystem must not cost us the live measurement -- but it IS reported,
48// because a silently-missing journal would make the trend look flat instead of absent.
49func pc_jappend(path: *u8, buf: *u8, n: i64) -> i64 {
50 let fd: i64 = sys_openat_append(path, PC_MODE_644)
51 if fd < 0 { return 0 - 1 }
52 let w: i64 = sys_write(fd, buf, n)
53 sys_close(fd)
54 return w
55}
56
57// read one scalar counter line ("processes 4153147") -- returns the value or -1 FAIL-CLOSED.
58func pc_scalar(buf: *u8, n: i64, key: *u8, out: *i64) -> i64 {
59 let c: i64 = pc_line_ints(buf, n, key, out, 1)
60 if c < 1 { return 0 - 1 }
61 return out[0]
62}
63
64func main() -> i64 {
65 let cbuf: *u8 = sys_mmap(PC_CONFBUF)
66 let cn: i64 = rm_read("knowledge/status/procchurn.conf" as *u8, cbuf, PC_CONFBUF)
67 var win_ms: i64 = rm_conf(cbuf, cn, "window-ms" as *u8, PC_DEF_WINDOW_MS)
68 if win_ms < PC_MIN_WINDOW_MS { win_ms = PC_MIN_WINDOW_MS }
69 if win_ms > PC_MAX_WINDOW_MS { win_ms = PC_MAX_WINDOW_MS }
70 let f_amber: i64 = rm_conf(cbuf, cn, "forks-per-sec-amber" as *u8, PC_DEF_F_AMBER)
71 let f_red: i64 = rm_conf(cbuf, cn, "forks-per-sec-red" as *u8, PC_DEF_F_RED)
72 let r_amber: i64 = rm_conf(cbuf, cn, "sysuser-permil-amber" as *u8, PC_DEF_R_AMBER)
73 let r_red: i64 = rm_conf(cbuf, cn, "sysuser-permil-red" as *u8, PC_DEF_R_RED)
74 let c_amber: i64 = rm_conf(cbuf, cn, "ctxsw-per-sec-amber" as *u8, PC_DEF_C_AMBER)
75 let c_red: i64 = rm_conf(cbuf, cn, "ctxsw-per-sec-red" as *u8, PC_DEF_C_RED)
76
77 let b1: *u8 = sys_mmap(PC_STATBUF)
78 let b2: *u8 = sys_mmap(PC_STATBUF)
79 let c1: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
80 let c2: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
81 let sc: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
82
83 // ---- SAMPLE 1 -> window -> SAMPLE 2. The window is measured, never assumed: sys_sleep_ms can
84 // ---- oversleep on a loaded box, and using the REQUESTED window would inflate every rate.
85 let t1: i64 = sys_now_us()
86 let n1: i64 = rm_read("/proc/stat" as *u8, b1, PC_STATBUF)
87 let p1: i64 = pc_scalar(b1, n1, "processes " as *u8, sc)
88 let x1: i64 = pc_scalar(b1, n1, "ctxt " as *u8, sc)
89 let i1: i64 = pc_scalar(b1, n1, "intr " as *u8, sc)
90 let nf1: i64 = pc_line_ints(b1, n1, "cpu " as *u8, c1, PC_NCPU_FIELDS)
91 sys_sleep_ms(win_ms)
92 let t2: i64 = sys_now_us()
93 let n2: i64 = rm_read("/proc/stat" as *u8, b2, PC_STATBUF)
94 let p2: i64 = pc_scalar(b2, n2, "processes " as *u8, sc)
95 let x2: i64 = pc_scalar(b2, n2, "ctxt " as *u8, sc)
96 let i2: i64 = pc_scalar(b2, n2, "intr " as *u8, sc)
97 let nf2: i64 = pc_line_ints(b2, n2, "cpu " as *u8, c2, PC_NCPU_FIELDS)
98 let elapsed_ms: i64 = (t2 - t1) / PC_US_PER_MS
99
100 var forks_ps: i64 = 0 - 1
101 if p1 >= 0 { if p2 >= 0 { forks_ps = pc_rate(p2 - p1, elapsed_ms) } }
102 var ctxsw_ps: i64 = 0 - 1
103 if x1 >= 0 { if x2 >= 0 { ctxsw_ps = pc_rate(x2 - x1, elapsed_ms) } }
104 var intr_ps: i64 = 0 - 1
105 if i1 >= 0 { if i2 >= 0 { intr_ps = pc_rate(i2 - i1, elapsed_ms) } }
106
107 var cpu_ok: i64 = 0
108 if nf1 >= PC_NCPU_FIELDS { if nf2 >= PC_NCPU_FIELDS { cpu_ok = 1 } }
109 var usr_d: i64 = 0
110 var sys_d: i64 = 0
111 var irq_d: i64 = 0
112 var idle_d: i64 = 0
113 var iow_d: i64 = 0
114 var tot_d: i64 = 0
115 if cpu_ok == 1 {
116 var k: i64 = 0
117 while k < PC_NCPU_FIELDS { tot_d = tot_d + (c2[k] - c1[k]); k = k + 1 }
118 usr_d = (c2[0] - c1[0]) + (c2[1] - c1[1])
119 sys_d = c2[2] - c1[2]
120 idle_d = c2[3] - c1[3]
121 iow_d = c2[4] - c1[4]
122 irq_d = (c2[5] - c1[5]) + (c2[6] - c1[6])
123 }
124 var ratio_pm: i64 = 0 - 1
125 if cpu_ok == 1 { ratio_pm = pc_ratio_permil(sys_d, usr_d) }
126 var usr_pm: i64 = 0 - 1
127 var sys_pm: i64 = 0 - 1
128 var irq_pm: i64 = 0 - 1
129 var busy_pm: i64 = 0 - 1
130 if tot_d > 0 {
131 usr_pm = (usr_d * 1000) / tot_d
132 sys_pm = (sys_d * 1000) / tot_d
133 irq_pm = (irq_d * 1000) / tot_d
134 busy_pm = ((tot_d - idle_d - iow_d) * 1000) / tot_d
135 }
136
137 // FAIL-CLOSED: a negative (unmeasurable) axis compares BELOW every threshold and would otherwise be
138 // published as a confident GREEN. That is the partial-as-complete defect the operator banned, so an
139 // incomplete sample gets its OWN exit code and prints no verdict at all.
140 var measured: i64 = 1
141 if forks_ps < 0 { measured = 0 }
142 if ctxsw_ps < 0 { measured = 0 }
143 if ratio_pm < 0 { measured = 0 }
144 if cpu_ok == 0 { measured = 0 }
145 if tot_d <= 0 { measured = 0 }
146
147 rm_puts("=== nx_procchurn -- the process-churn axis nx_health lacks (seq1317) ===\n" as *u8)
148 rm_puts("window_ms=" as *u8); rm_num(elapsed_ms)
149 rm_puts(" (requested " as *u8); rm_num(win_ms); rm_puts(", MEASURED not assumed)\n" as *u8)
150 if measured == 0 {
151 rm_puts("verdict=UNMEASURED sev=" as *u8); rm_num(PC_EXIT_UNMEASURED)
152 rm_puts(" why=a /proc/stat counter was absent or went backwards (wrap/reboot); NO verdict is" as *u8)
153 rm_puts(" published off a partial sample\n" as *u8)
154 return PC_EXIT_UNMEASURED
155 }
156 rm_puts("forks_per_sec=" as *u8); rm_num(forks_ps)
157 rm_puts(" (amber>=" as *u8); rm_num(f_amber); rm_puts(" red>=" as *u8); rm_num(f_red); rm_puts(")\n" as *u8)
158 rm_puts("ctxsw_per_sec=" as *u8); rm_num(ctxsw_ps)
159 rm_puts(" (amber>=" as *u8); rm_num(c_amber); rm_puts(" red>=" as *u8); rm_num(c_red); rm_puts(")\n" as *u8)
160 rm_puts("intr_per_sec=" as *u8); rm_num(intr_ps); rm_puts(" (context only, not thresholded)\n" as *u8)
161 rm_puts("kernel_user_permil=" as *u8); rm_num(ratio_pm)
162 rm_puts(" (1000=parity; amber>=" as *u8); rm_num(r_amber); rm_puts(" red>=" as *u8); rm_num(r_red); rm_puts(")\n" as *u8)
163 rm_puts("cpu_share_permil: user=" as *u8); rm_num(usr_pm)
164 rm_puts(" system=" as *u8); rm_num(sys_pm)
165 rm_puts(" irq_softirq=" as *u8); rm_num(irq_pm)
166 rm_puts(" busy=" as *u8); rm_num(busy_pm); rm_puts("\n" as *u8)
167 rm_puts("jiffies_delta: user+nice=" as *u8); rm_num(usr_d)
168 rm_puts(" system=" as *u8); rm_num(sys_d)
169 rm_puts(" total=" as *u8); rm_num(tot_d); rm_puts("\n" as *u8)
170
171 // QUEUE COMPOSITION (seq1555) -- the number that separates an I/O-blocked queue from a compute one,
172 // and it is FREE: /proc/stat already carries procs_running and procs_blocked, so this costs ZERO extra
173 // syscalls and needs NO admission gate (unlike a /proc walk, which is refused exactly when a deep
174 // queue makes it most interesting -- the priority-lane trap).
175 // WHY IT MATTERS: loadavg counts TASK_UNINTERRUPTIBLE as well as runnable, so a load of 14.94 against
176 // a 50.7pct-idle CPU says the QUEUE is deep but never says WHAT it waits on. iowait cannot answer
177 // either -- Linux only accrues it while a CPU is otherwise IDLE, so a D-state task on a busy core is
178 // invisible to it. procs_blocked counts those tasks DIRECTLY.
179 // READ IT AS: blocked >> running => the queue is I/O/lock-bound, and adding CPU would change nothing.
180 var blocked: i64 = 0 - 1
181 var running: i64 = 0 - 1
182 if pc_line_ints(b2, n2, "procs_blocked " as *u8, sc, 1) > 0 { blocked = sc[0] }
183 if pc_line_ints(b2, n2, "procs_running " as *u8, sc, 1) > 0 { running = sc[0] }
184 rm_puts("queue: procs_running=" as *u8); rm_num(running)
185 rm_puts(" procs_blocked=" as *u8); rm_num(blocked)
186 rm_puts(" (blocked >> running = I/O or lock bound, NOT compute; more CPU would not help)\n" as *u8)
187
188 let sev: i64 = pc_verdict(forks_ps, ratio_pm, ctxsw_ps, f_amber, f_red, r_amber, r_red, c_amber, c_red)
189
190 // ---- TREND: one append-only frame per run. A single verdict is a snapshot; the storm is bursty,
191 // ---- so only the journal can show whether a fix actually landed.
192 let jb: *u8 = sys_mmap(PC_JBUF)
193 let jn: i64 = pc_frame(jb, sys_now_realtime_sec(), elapsed_ms, forks_ps, ctxsw_ps, intr_ps,
194 ratio_pm, usr_pm, sys_pm, busy_pm, sev, running, blocked)
195 let jw: i64 = pc_jappend("knowledge/status/procchurn.jrnl" as *u8, jb, jn)
196 rm_puts("journal=knowledge/status/procchurn.jrnl " as *u8)
197 if jw < 0 { rm_puts("APPEND-FAILED (measurement still valid; the TREND is what was lost)\n" as *u8) }
198 if jw >= 0 { rm_puts("frame_bytes=" as *u8); rm_num(jw); rm_puts("\n" as *u8) }
199
200 rm_puts("verdict=" as *u8)
201 if sev == 0 { rm_puts("GREEN" as *u8) }
202 if sev == 1 { rm_puts("AMBER" as *u8) }
203 if sev == 2 { rm_puts("RED" as *u8) }
204 rm_puts(" sev=" as *u8); rm_num(sev); rm_puts("\n" as *u8)
205 return sev
206}