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}