code wiki / _hdl_build / nx_ctxtop_lib.nx

nx_ctxtop_lib.nx source

↩ module page · 135 lines · 6065 B

1// nx_ctxtop_lib.nx -- PURE core of nx_ctxtop: per-process context-switch ATTRIBUTION. 2// 3// WHY: nx_procchurn proved the box takes 81k-93k context switches/sec in EVERY sample across 45 minutes 4// (1.6-1.9x the RED line) -- the one CONSTANT in the perf lane. But a box-level rate names no culprit, 5// and the fork-rate hypothesis was DISPROVEN as the driver (seq1340: forks swing 15->179->10 while ctxsw 6// stays flat, so the switches are NOT mostly fork/exec). /proc/<pid>/status carries 7// voluntary_ctxt_switches and nonvoluntary_ctxt_switches PER PROCESS, so the producer is directly 8// measurable instead of guessed. LAW (rising-debt-means-find-the-producer): a rate with no owner is a 9// symptom, not a diagnosis. 10// 11// ★THE VOLUNTARY/NONVOLUNTARY SPLIT IS THE DIAGNOSIS, not a detail: 12// voluntary = the process BLOCKED on its own (read/poll/sleep) -- an I/O or poll-loop design, 13// fixable by batching, longer polls, or event-driven waits. 14// nonvoluntary = the SCHEDULER preempted it -- CPU contention / too many runnable threads, 15// fixed by reducing concurrency, not by batching. 16// The same total means opposite remedies, so a tool reporting only the sum would send the fix the wrong 17// way. This lib keeps them separate all the way to the output. 18// 19// Rule 15: rate arithmetic is REUSED from nx_procchurn_lib (pc_rate), generic IO/print from 20// nx_resmon_lib -- no new copies of the emit family (D001/seq389: 9495 duplicate bodies). 21// license_tier: ORIGINAL Read-only. No hw writes (Rule 26). 22import "nx_procchurn_lib.nx" 23 24const CT_NOTFOUND: i64 = 0 - 1 25 26// Linear find of a pid in a parallel-array sample. Returns index or -1. O(n) per lookup, O(n^2) overall 27// at ~1000 pids = ~1e6 compares -- deliberately simple: a hash table here would be unprovable complexity 28// for a one-shot diagnostic, and the cost is far below one scheduling quantum. 29func ct_find(pids: *i64, n: i64, pid: i64) -> i64 { 30 var i: i64 = 0 31 while i < n { 32 if pids[i] == pid { return i } 33 i = i + 1 34 } 35 return CT_NOTFOUND 36} 37 38// Insert (pid,rate) into a DESCENDING top-k kept in parallel arrays. Returns the new used count. 39// FAIL-CLOSED ON TRUNCATION BY DESIGN: when the table is full the SMALLEST entry is evicted, never the 40// incoming one -- a top-k that silently drops the biggest producer because it arrived last is exactly 41// the silent-truncation class (seq1319/L011). Ties keep the incumbent (stable, so output does not 42// churn between runs on equal rates). 43func ct_topk_insert(pids: *i64, rates: *i64, k: i64, used: i64, pid: i64, rate: i64) -> i64 { 44 if k <= 0 { return 0 } 45 if rate < 0 { return used } 46 // find insertion point: first slot whose rate is strictly smaller 47 var pos: i64 = used 48 var i: i64 = 0 49 var found: i64 = 0 50 while i < used { 51 if found == 0 { 52 if rates[i] < rate { pos = i; found = 1 } 53 } 54 i = i + 1 55 } 56 if pos >= k { return used } 57 var nu: i64 = used 58 if nu < k { nu = nu + 1 } 59 // shift down from the end toward pos (drops the smallest when the table was full) 60 var j: i64 = nu - 1 61 while j > pos { 62 pids[j] = pids[j - 1] 63 rates[j] = rates[j - 1] 64 j = j - 1 65 } 66 pids[pos] = pid 67 rates[pos] = rate 68 return nu 69} 70 71// Share of a total, in permil. Guards a zero/absent total rather than dividing by it. 72func ct_share_permil(part: i64, total: i64) -> i64 { 73 if total <= 0 { return 0 - 1 } 74 if part < 0 { return 0 - 1 } 75 return (part * 1000) / total 76} 77 78// ---- ADMISSION CONTROL: an instrument must not become the load it measures ---- 79// WHY (seq341, and nearly re-learned 2026-07-30): nx_ctxtop and nx_memvel each walk ALL of /proc and 80// openat+read+close every /proc/<pid>/status -- ~2700 syscalls per nx_memvel run at 5 rounds x ~450 81// procs. That is the SAME O(processes) syscall storm seq1318 indicts the supervisor for doing ~25x per 82// poll. Running it on an already-saturated box is how seq341's loadgen tipped the NAS into an outage. 83// ★LAW: A DIAGNOSTIC THAT CANNOT REFUSE TO RUN IS A LOAD GENERATOR WITH GOOD INTENTIONS. 84// The check is deliberately CHEAP (one small file read, no fork) so the guard can never cost more than 85// the thing it guards. 86const CT_LOADAVG_SCALE: i64 = 100 87const CT_ASCII_DOT: i64 = 46 88 89// Parse the FIRST field of /proc/loadavg ("4.02 3.75 ...") into centi-load (402). -1 if unparseable. 90// Two decimals is the kernel's fixed format; a third digit is ignored rather than silently scaling by 10. 91func ct_load_centi(buf: *u8, n: i64) -> i64 { 92 if n <= 0 { return 0 - 1 } 93 var i: i64 = 0 94 var whole: i64 = 0 95 var seen: i64 = 0 96 while i < n { 97 let c: i64 = buf[i] as i64 98 var isdig: i64 = 0 99 if c >= 48 { if c <= 57 { isdig = 1 } } 100 if isdig == 1 { whole = whole * 10 + (c - 48); seen = 1; i = i + 1 } 101 if isdig == 0 { i = n } 102 } 103 if seen == 0 { return 0 - 1 } 104 // resume at the '.' to collect exactly two fractional digits 105 var p: i64 = 0 106 while p < n { if buf[p] == (CT_ASCII_DOT as u8) { p = n } else { p = p + 1 } } 107 var frac: i64 = 0 108 var got: i64 = 0 109 var q: i64 = 0 110 while q < n { 111 if buf[q] == (CT_ASCII_DOT as u8) { 112 var k: i64 = q + 1 113 while got < 2 { 114 if k >= n { got = 2 } else { 115 let d: i64 = buf[k] as i64 116 if d >= 48 { if d <= 57 { frac = frac * 10 + (d - 48); got = got + 1 } } 117 if d < 48 { got = 2 } 118 if d > 57 { got = 2 } 119 k = k + 1 120 } 121 } 122 q = n 123 } else { q = q + 1 } 124 } 125 return whole * CT_LOADAVG_SCALE + frac 126} 127 128// 1 = admitted, 0 = REFUSED. Fail-CLOSED on an unreadable loadavg: if we cannot tell how loaded the box 129// is, we do not get to add to it. 130func ct_admit(load_centi: i64, max_centi: i64) -> i64 { 131 if load_centi < 0 { return 0 } 132 if max_centi <= 0 { return 0 } 133 if load_centi > max_centi { return 0 } 134 return 1 135}