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}