code wiki / _hdl_build / nx_ctxtop.nx
nx_ctxtop.nx source
↩ module page · 229 lines · 11792 B
1// nx_ctxtop.nx -- WHO is producing the context switches. The producer-finder for seq1340.
2//
3// nx_procchurn measured the box at 81k-93k ctxsw/sec in every sample (the one CONSTANT in the perf lane)
4// and DISPROVED forks as the driver -- forks swing 15->179->10 while ctxsw stays flat. A box-level rate
5// names no culprit, so this walks /proc/<pid>/status twice and attributes the switches per process.
6// LAW (rising-debt-means-find-the-producer): a rate with no owner is a symptom, not a diagnosis.
7//
8// ★Reports voluntary and nonvoluntary SEPARATELY because they demand OPPOSITE fixes: voluntary = the
9// process blocked itself (poll/read/sleep) -> batch, lengthen polls, go event-driven; nonvoluntary = the
10// scheduler preempted it -> reduce concurrency. A tool printing only the sum sends the fix the wrong way.
11// Exit code = 0 when the sample completed, 3 UNMEASURED (fail-closed, never a confident empty answer).
12// license_tier: ORIGINAL Read-only. No hw writes (Rule 26). expect_exit: 0
13import "nx_ctxtop_lib.nx"
14import "nx_proc_ctl.nx"
15
16const CT_MAXP: i64 = 2048
17const CT_TABLE_BYTES: i64 = 16384
18const CT_DIRBUF: i64 = 65536
19const CT_STATBUF: i64 = 4096
20const CT_TOPK: i64 = 8
21const CT_TOPK_BYTES: i64 = 128
22const CT_WINDOW_MS: i64 = 3000
23const CT_US_PER_MS: i64 = 1000
24const CT_NAMEOFF: i64 = 6
25const CT_EXIT_UNMEASURED: i64 = 3
26const CT_SLASH: i64 = 47
27const CT_EXIT_REFUSED: i64 = 4
28const CT_ADMIT_DEFAULT: i64 = 800
29const CT_CONFBUF: i64 = 4096
30const CT_LOADBUF: i64 = 128
31
32// Walk /proc, recording pid + both ctxsw counters. Returns the number of processes captured.
33// Reports its own capping honestly (L011: partial-as-complete is a bug) via the returned count vs CT_MAXP.
34func ct_scan(pids: *i64, vols: *i64, nvs: *i64, dbuf: *u8, path: *u8, sbuf: *u8) -> i64 {
35 var cnt: i64 = 0
36 // SECOND dirent buffer: the task/ walk is NESTED inside the /proc walk, so it cannot reuse dbuf --
37 // doing so would corrupt the outer getdents cursor mid-iteration and silently skip processes.
38 let tbuf: *u8 = sys_mmap(CT_DIRBUF)
39 let fd: i64 = sys_openat_rd("/proc" as *u8)
40 if fd < 0 { return 0 }
41 var run: i64 = 1
42 while run == 1 {
43 let n: i64 = sys_getdents64(fd, dbuf, CT_DIRBUF)
44 if n <= 0 { run = 0 } else {
45 var off: i64 = 0
46 while off < n {
47 let rec: *u8 = ((dbuf as i64) + off) as *u8
48 let reclen: i64 = dirent_reclen(rec)
49 if reclen <= 0 { off = n } else {
50 let nm: *u8 = dirent_name(rec)
51 if nm[0] >= (48 as u8) { if nm[0] <= (57 as u8) { if cnt < CT_MAXP {
52 var p: i64 = 0
53 let pre: *u8 = "/proc/" as *u8
54 var a: i64 = 0
55 while pre[a] != (0 as u8) { path[p] = pre[a]; p = p + 1; a = a + 1 }
56 var pidv: i64 = 0
57 a = 0
58 while nm[a] != (0 as u8) { path[p] = nm[a]; pidv = pidv * 10 + ((nm[a] as i64) - 48); p = p + 1; a = a + 1 }
59 let suf: *u8 = "/status" as *u8
60 a = 0
61 while suf[a] != (0 as u8) { path[p] = suf[a]; p = p + 1; a = a + 1 }
62 path[p] = 0 as u8
63 // THREAD-LEVEL WALK (seq1364 gap closed). Reading only /proc/<pid>/status counts
64 // the MAIN THREAD's counters and silently ignores every sibling thread: the box
65 // carries 1809 threads across ~809 processes, so a process-only walk attributed
66 // 40480 of a box-level 116-130k ctxsw/s and left TWO THIRDS unexplained -- while
67 // looking like a complete top-8. Every task has /proc/<pid>/task/<pid>, so walking
68 // task/ ALONE covers the main thread too: no double-count, full coverage.
69 // LAW: A PER-PROCESS COUNTER ON A MULTI-THREADED SYSTEM IS A FLOOR WEARING THE
70 // COSTUME OF A TOTAL.
71 var tp: i64 = p - 7
72 let tsuf: *u8 = "/task" as *u8
73 var ta: i64 = 0
74 while tsuf[ta] != (0 as u8) { path[tp] = tsuf[ta]; tp = tp + 1; ta = ta + 1 }
75 path[tp] = 0 as u8
76 let tfd: i64 = sys_openat_rd(path)
77 if tfd >= 0 {
78 var trun: i64 = 1
79 while trun == 1 {
80 let tn: i64 = sys_getdents64(tfd, tbuf, CT_DIRBUF)
81 if tn <= 0 { trun = 0 } else {
82 var toff: i64 = 0
83 while toff < tn {
84 let trec: *u8 = ((tbuf as i64) + toff) as *u8
85 let treclen: i64 = dirent_reclen(trec)
86 if treclen <= 0 { toff = tn } else {
87 let tnm: *u8 = dirent_name(trec)
88 if tnm[0] >= (48 as u8) { if tnm[0] <= (57 as u8) { if cnt < CT_MAXP {
89 var q: i64 = tp
90 path[q] = CT_SLASH as u8; q = q + 1
91 var tidv: i64 = 0
92 var tb: i64 = 0
93 while tnm[tb] != (0 as u8) { path[q] = tnm[tb]; tidv = tidv * 10 + ((tnm[tb] as i64) - 48); q = q + 1; tb = tb + 1 }
94 let ssuf: *u8 = "/status" as *u8
95 tb = 0
96 while ssuf[tb] != (0 as u8) { path[q] = ssuf[tb]; q = q + 1; tb = tb + 1 }
97 path[q] = 0 as u8
98 let sn2: i64 = rm_read(path, sbuf, CT_STATBUF)
99 if sn2 > 0 {
100 let v2: i64 = rm_field(sbuf, sn2, "voluntary_ctxt_switches:" as *u8)
101 let w2: i64 = rm_field(sbuf, sn2, "nonvoluntary_ctxt_switches:" as *u8)
102 if v2 >= 0 { if w2 >= 0 {
103 pids[cnt] = tidv
104 vols[cnt] = v2
105 nvs[cnt] = w2
106 cnt = cnt + 1
107 } }
108 }
109 } } }
110 toff = toff + treclen
111 }
112 }
113 }
114 }
115 sys_close(tfd)
116 }
117 } } }
118 off = off + reclen
119 }
120 }
121 }
122 }
123 sys_close(fd)
124 return cnt
125}
126
127// print "/proc/<pid>/status" Name: field (comm starts at byte 6, ends at the first newline)
128func ct_putname(pid: i64, path: *u8, sbuf: *u8) -> i64 {
129 var p: i64 = 0
130 let pre: *u8 = "/proc/" as *u8
131 var a: i64 = 0
132 while pre[a] != (0 as u8) { path[p] = pre[a]; p = p + 1; a = a + 1 }
133 p = pc_catn(path, p, pid)
134 let suf: *u8 = "/status" as *u8
135 a = 0
136 while suf[a] != (0 as u8) { path[p] = suf[a]; p = p + 1; a = a + 1 }
137 path[p] = 0 as u8
138 let sn: i64 = rm_read(path, sbuf, CT_STATBUF)
139 if sn <= 0 { rm_puts("<gone>" as *u8); return 0 }
140 var z: i64 = CT_NAMEOFF
141 let out: *u8 = sys_mmap(64)
142 var w: i64 = 0
143 while z < sn { if sbuf[z] == (10 as u8) { z = sn } else { if w < 62 { out[w] = sbuf[z]; w = w + 1 } z = z + 1 } }
144 out[w] = 0 as u8
145 rm_puts(out)
146 sys_munmap(out, 64)
147 return 0
148}
149
150func main() -> i64 {
151 // ADMISSION CONTROL (seq341 / seq1389): this organ walks ALL of /proc and stats every
152 // /proc/<pid>/status -- an O(processes) syscall storm, the same shape seq1318 indicts the supervisor
153 // for. Running it on an already-saturated box is how a diagnostic becomes an outage. Refuse instead.
154 // ★LAW: A DIAGNOSTIC THAT CANNOT REFUSE TO RUN IS A LOAD GENERATOR WITH GOOD INTENTIONS.
155 let cbuf: *u8 = sys_mmap(CT_CONFBUF)
156 let cn: i64 = rm_read("knowledge/status/procchurn.conf" as *u8, cbuf, CT_CONFBUF)
157 let maxload: i64 = rm_conf(cbuf, cn, "admit-max-load-centi" as *u8, CT_ADMIT_DEFAULT)
158 let lbuf: *u8 = sys_mmap(CT_LOADBUF)
159 let ln: i64 = rm_read("/proc/loadavg" as *u8, lbuf, CT_LOADBUF)
160 let loadc: i64 = ct_load_centi(lbuf, ln)
161 if ct_admit(loadc, maxload) == 0 {
162 rm_puts("NX-CTXTOP verdict=REFUSED load_centi=" as *u8); rm_num(loadc)
163 rm_puts(" max=" as *u8); rm_num(maxload)
164 rm_puts(" why=host already saturated; a /proc-walking diagnostic must not add to it (seq341)\n" as *u8)
165 return CT_EXIT_REFUSED
166 }
167 let p1: *i64 = sys_mmap(CT_TABLE_BYTES) as *i64
168 let v1: *i64 = sys_mmap(CT_TABLE_BYTES) as *i64
169 let w1: *i64 = sys_mmap(CT_TABLE_BYTES) as *i64
170 let p2: *i64 = sys_mmap(CT_TABLE_BYTES) as *i64
171 let v2: *i64 = sys_mmap(CT_TABLE_BYTES) as *i64
172 let w2: *i64 = sys_mmap(CT_TABLE_BYTES) as *i64
173 let dbuf: *u8 = sys_mmap(CT_DIRBUF)
174 let path: *u8 = sys_mmap(256)
175 let sbuf: *u8 = sys_mmap(CT_STATBUF)
176
177 let t1: i64 = sys_now_us()
178 let n1: i64 = ct_scan(p1, v1, w1, dbuf, path, sbuf)
179 sys_sleep_ms(CT_WINDOW_MS)
180 let t2: i64 = sys_now_us()
181 let n2: i64 = ct_scan(p2, v2, w2, dbuf, path, sbuf)
182 let el: i64 = (t2 - t1) / CT_US_PER_MS
183
184 if n1 <= 0 { rm_puts("NX-CTXTOP verdict=UNMEASURED why=/proc walk returned nothing\n" as *u8); return CT_EXIT_UNMEASURED }
185 if el <= 0 { rm_puts("NX-CTXTOP verdict=UNMEASURED why=zero-length window\n" as *u8); return CT_EXIT_UNMEASURED }
186
187 let tp: *i64 = sys_mmap(CT_TOPK_BYTES) as *i64
188 let tr: *i64 = sys_mmap(CT_TOPK_BYTES) as *i64
189 var used: i64 = 0
190 var tot: i64 = 0
191 var totv: i64 = 0
192 var totn: i64 = 0
193 var i: i64 = 0
194 while i < n2 {
195 let j: i64 = ct_find(p1, n1, p2[i])
196 if j >= 0 {
197 let dv: i64 = v2[i] - v1[j]
198 let dn: i64 = w2[i] - w1[j]
199 if dv >= 0 { if dn >= 0 {
200 let r: i64 = pc_rate(dv + dn, el)
201 if r > 0 {
202 tot = tot + r
203 totv = totv + pc_rate(dv, el)
204 totn = totn + pc_rate(dn, el)
205 used = ct_topk_insert(tp, tr, CT_TOPK, used, p2[i], r)
206 }
207 } }
208 }
209 i = i + 1
210 }
211
212 rm_puts("NX-CTXTOP window_ms=" as *u8); rm_num(el)
213 rm_puts(" procs=" as *u8); rm_num(n2)
214 rm_puts(" attributed_ctxsw_per_sec=" as *u8); rm_num(tot)
215 rm_puts(" vol=" as *u8); rm_num(totv)
216 rm_puts(" nonvol=" as *u8); rm_num(totn); rm_puts("\n" as *u8)
217 var k: i64 = 0
218 while k < used {
219 rm_puts(" #" as *u8); rm_num(k + 1)
220 rm_puts(" pid=" as *u8); rm_num(tp[k])
221 rm_puts(" ctxsw/s=" as *u8); rm_num(tr[k])
222 rm_puts(" share=" as *u8); rm_num(ct_share_permil(tr[k], tot)); rm_puts("permil name=" as *u8)
223 ct_putname(tp[k], path, sbuf)
224 rm_puts("\n" as *u8)
225 k = k + 1
226 }
227 if n2 >= CT_MAXP { rm_puts(" WARNING scan CAPPED at " as *u8); rm_num(CT_MAXP); rm_puts(" pids -- coverage INCOMPLETE, totals are a floor\n" as *u8) }
228 return 0
229}