code wiki / _hdl_build / nx_resmon_lib.nx
nx_resmon_lib.nx source
↩ module page · 306 lines · 19011 B
1// nx_resmon_lib.nx -- the PURE, GATEABLE core of nx_resmon (debt seq1005).
2//
3// Split out per rule 9 (single responsibility) and rule 15 (DRY): the organ owns the /proc walk and
4// the printing, this lib owns the two predicates that actually encode the policy --
5// rm_is_leaker : what counts as a leaking process
6// rm_verdict : how measurements become GREEN / AMBER / RED
7// Keeping them here means the gate exercises the SAME code the organ runs, not a reimplementation.
8// Everything here is a pure function of its arguments (rm_field/rm_conf parse a caller-owned buffer),
9// so the gate needs no /proc, no files and no fixtures on disk.
10// license_tier: ORIGINAL
11import "nx_syscalls.nx"
12import "nx_itoa_lib.nx" // THE canonical integer emitter (runtime/ layer, so _hdl_build may import it)
13import "nx_logtail.nx" // lt_read_tail -- THE shared tail reader (runtime/ layer): see rm_trend_read_tail
14
15// ---- TAIL-ANCHORED READ OF THE BEAT LOG (2026-09-02) -------------------------------------------------
16// rm_trend used rm_read on resmon.log, which keeps the HEAD of the file under a 1 MiB cap. The log grows
17// ~95 KB/day, so on 2026-08-30 ~20:00 it crossed 1 MiB and from that beat on the "newest" row rm_trend saw
18// was a TORN partial line at byte 1,048,575 -- no worst=, no worst_committed_kb= -- and every hourly beat
19// printed the SAME frozen `worst= newest_kb=-1 name_changed=1 verdict=UNOBSERVABLE` for 65 consecutive
20// beats while nx_seed_announce_all climbed 1.0 -> 1.5 GB in the rows the reader could no longer reach.
21// MEASURED live 2026-09-02 (`nx_resmon trend` -> exactly that line, exit 3). A HEAD-BOUNDED READ OF AN
22// APPEND-ONLY LOG REPORTS THE PAST AS THE PRESENT, AND THE ABSTENTION IT PRODUCES IS INVISIBLE BY DESIGN --
23// this board's own axis-blind law, fired by the axis that carries the law.
24// COMPOSED, NOT RE-TYPED: lt_read_tail (runtime/nx_logtail.nx) seeks to the end, keeps the newest <=cap
25// bytes, trims the torn first record, and DECLARES the envelope (env[0]=file_bytes env[3]=truncated). It
26// lives in this lib rather than the organ so nx_resmon_gate can drive it on a planted over-cap log.
27func rm_trend_read_tail(logpath: *u8, lbuf: *u8, cap: i64, env: *i64) -> i64 {
28 return lt_read_tail(logpath, lbuf, cap, env)
29}
30
31func rm_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
32func rm_puts(s: *u8) -> i64 { sys_write(1, s, rm_slen(s)); return 0 }
33// rm_num is now a THIN ALIAS over the canonical emitter, kept as a NAME rather than an
34// IMPLEMENTATION so every existing rm_num caller needs no edit -- rule 15 without a flag day.
35func rm_num(v: i64) -> i64 {
36 // DELEGATED 2026-08-08 -- SUPERSEDES THE LAZY-STATIC HOIST I PUT HERE EARLIER TODAY.
37 // I hoisted this buffer to a static to stop the per-call mmap, then found nx_itoa_lib.nx (runtime/
38 // layer, 2026-07-31) already IS the canonical integer emitter: ccz_cat_num allocates NOTHING and
39 // nxi_fd is the balanced fd shim, written once, explicitly 'the drop-in for every hand-rolled
40 // putn/gn/wn/pn clone'. nx_research_miner already delegates in exactly this one line.
41 // Two reasons the delegation beats my hoist, both of which I got wrong first:
42 // 1. RULE 15 -- a hoist leaves TWO implementations; a delegation leaves ONE. Better duplicate code
43 // is still duplicate code.
44 // 2. nx_mmapbal convicts an unmatched sys_mmap in a function that does not return a pointer, so
45 // the static hoist reads as a LEAK to the estate's own scanner. Delegating removes the finding
46 // instead of arguing with it.
47 // Behaviour is unchanged: both emit MSB-first decimal with a leading '-' for negatives, to fd 1.
48 nxi_out(v)
49 return 0
50}
51// read a whole small file; returns byte count (-1 if unreadable). NUL-terminates.
52func rm_read(path: *u8, buf: *u8, cap: i64) -> i64 {
53 let fd: i64 = sys_openat_rd(path)
54 if fd < 0 { return 0 - 1 }
55 var tot: i64 = 0
56 var run: i64 = 1
57 while run == 1 {
58 let n: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot - 1)
59 if n <= 0 { run = 0 } else {
60 tot = tot + n
61 if tot >= cap - 1 { run = 0 }
62 }
63 }
64 sys_close(fd)
65 buf[tot] = 0 as u8
66 return tot
67}
68// first integer on the line that STARTS with `key`; -1 if absent. Serves BOTH
69// /proc/<pid>/status ("VmSize:\t 1234 kB") and conf rows ("leak-min-kb = 262144").
70func rm_field(buf: *u8, n: i64, key: *u8) -> i64 {
71 let kl: i64 = rm_slen(key)
72 var i: i64 = 0
73 var found: i64 = 0 - 1
74 while i + kl <= n {
75 if found < 0 {
76 var atline: i64 = 0
77 if i == 0 { atline = 1 } else { if buf[i - 1] == (10 as u8) { atline = 1 } }
78 if atline == 1 {
79 var j: i64 = 0
80 var ok: i64 = 1
81 while j < kl { if buf[i + j] != key[j] { ok = 0; j = kl } else { j = j + 1 } }
82 if ok == 1 { found = i + kl }
83 }
84 }
85 i = i + 1
86 }
87 if found < 0 { return 0 - 1 }
88 var p: i64 = found
89 var v: i64 = 0
90 var seen: i64 = 0
91 var run: i64 = 1
92 while run == 1 {
93 if p >= n { run = 0 } else {
94 let c: i64 = buf[p] as i64
95 if c == 10 { run = 0 } else {
96 if c >= 48 {
97 if c <= 57 { v = v * 10 + (c - 48); seen = 1 } else { if seen == 1 { run = 0 } }
98 } else { if seen == 1 { run = 0 } }
99 p = p + 1
100 }
101 }
102 }
103 if seen == 0 { return 0 - 1 }
104 return v
105}
106func rm_conf(cbuf: *u8, cn: i64, key: *u8, dflt: i64) -> i64 {
107 if cn <= 0 { return dflt }
108 let v: i64 = rm_field(cbuf, cn, key)
109 if v < 0 { return dflt }
110 return v
111}
112
113// ---- PREDICATE 1: a SCREEN for leak candidates -- NOT a leak verdict --------------------------------
114// ⚠CORRECTED 2026-08-14. This predicate's original comment claimed vsz==vpk means "the address space has
115// ONLY EVER GROWN (allocate-without-free fingerprint)". That inference is FALSE. vsz==vpk says only that
116// the address space is CURRENTLY AT ITS HIGH-WATER MARK, which is equally true of a daemon that mmaps its
117// arena once at startup and never unmaps -- the leak-FREE design this estate recommends, and the one
118// nx_hub_gw's own header documents itself as implementing. ★★★★★★A LEAK IS A DERIVATIVE AND NO SNAPSHOT
119// RELATION CAN EXPRESS ONE -- growth needs two samples in time. So this is a NECESSARY-NOT-SUFFICIENT
120// SCREEN (a genuine leaker does always satisfy it), and the sufficient half lives in rm_sustained_count
121// below, fed by nx_memvel's N-window observation. MEASURED COST of the old reading, 2026-08-14: 17
122// suspects against a leak-count-red of 6, so nx_resmon was RED PERMANENTLY on this box, while the true
123// sustained-grower count measured 0 in back-to-back runs. ★A DETECTOR THAT IS PERMANENTLY RED IS ONE
124// EVERYONE LEARNS TO IGNORE.
125// SIZE IS JUDGED ON COMMITTED MEMORY (rss+swap), NEVER ON RESERVED VmData. Measured 2026-07-25 on the
126// first live run: a VmData screen reported a worst case of 40.6 GiB on a 36.9 GB box -- larger than
127// physical RAM -- because VmData counts reservations a process never faulted in. Screening on rss+swap
128// took the census from 63 suspects to 23 and the worst case to 3.02 GiB.
129func rm_is_leaker(vsz: i64, vpk: i64, rss: i64, swap: i64, min_kb: i64) -> i64 {
130 if vsz <= 0 { return 0 }
131 if vpk <= 0 { return 0 }
132 if vsz != vpk { return 0 }
133 var touched: i64 = 0
134 if rss > 0 { touched = touched + rss }
135 if swap > 0 { touched = touched + swap }
136 if touched < min_kb { return 0 }
137 return 1
138}
139
140// ---- PREDICATE 2: measurements -> severity ---------------------------------------------------------
141// 0=GREEN 1=AMBER 2=RED. RED dominates AMBER; ANY axis can raise the verdict on its own, because each
142// one independently indicates the box is in trouble. Thresholds are ALWAYS passed in (rule 11) -- this
143// function contains no policy numbers of its own, which is exactly what the gate mutates to prove it.
144// ⚠SIGNATURE CHANGED 2026-08-14: leak_observed. The two pressure axes are LEVELS, and a snapshot
145// measures a level exactly -- but the leak axis is a DERIVATIVE this organ cannot observe on its own,
146// so it must be able to say I COULD NOT LOOK. Before this, an unobservable leak axis was indistinguishable
147// from an observed zero, and the caller had no way to express the difference: the only two things it
148// could say were "no leaks" and "leaks", one of which is always a lie when there is no evidence.
149// ★★★★★I COULD NOT LOOK IS NOT IT IS FINE, AND IT IS NOT IT IS BROKEN -- an axis with no evidence must
150// contribute NOTHING to the verdict rather than silently voting GREEN.
151func rm_verdict(swused_pm: i64, avail_pm: i64, leakers: i64, leak_observed: i64,
152 sw_amber: i64, sw_red: i64, av_amber: i64, av_red: i64,
153 leak_amber: i64, leak_red: i64) -> i64 {
154 var sev: i64 = 0
155 if swused_pm >= sw_amber { sev = 1 }
156 if avail_pm <= av_amber { sev = 1 }
157 if leak_observed == 1 { if leakers >= leak_amber { sev = 1 } }
158 if swused_pm >= sw_red { sev = 2 }
159 if avail_pm <= av_red { sev = 2 }
160 if leak_observed == 1 { if leakers >= leak_red { sev = 2 } }
161 return sev
162}
163
164// ---- LINE BUILDERS -- MOVED HERE FROM nx_resmon.nx 2026-08-14 --------------------------------------
165// Two organs now emit a status line, and the second one was about to grow its own copy of these.
166// ★EXTRACT THE FIX, DON'T RE-TYPE IT -- a helper duplicated across two files is the duplicate-ruler
167// defect wearing a utility function, and the copies diverge silently because nothing compares them.
168func rm_lcat(b: *u8, off: i64, s: *u8) -> i64 { var o: i64 = off; var j: i64 = 0; while s[j] != (0 as u8) { b[o] = s[j]; o = o + 1; j = j + 1 } return o }
169func rm_lnum(b: *u8, off: i64, v: i64) -> i64 {
170 var o: i64 = off
171 var m: i64 = v
172 if m < 0 { b[o] = 45 as u8; o = o + 1; m = 0 - m }
173 let t: *u8 = sys_mmap(28)
174 var k: i64 = 0
175 if m == 0 { t[0] = 48 as u8; k = 1 }
176 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
177 var i: i64 = 0
178 while i < k { b[o] = t[k - 1 - i]; o = o + 1; i = i + 1 }
179 sys_munmap(t, 28)
180 return o
181}
182
183// ---- PREDICATE 3: velocity -> how many processes are SUSTAINED growers -----------------------------
184// THE SUFFICIENT HALF that PREDICATE 1 structurally cannot supply. acnt[i] is the number of observation
185// windows in which process i grew its COMMITTED memory; rounds is how many windows were observed. Only
186// k==rounds is a leak -- anything less is a process that happened to occupy a top slot in some windows
187// and not others, which is the normal churn of a busy box. Measured live 2026-08-14: the top grower was
188// synoelasticd at 3396 kB/s with grew_in=4/5, i.e. a 200 MB/min headline that is NOT a leak.
189// ★THIRD STATE: with zero observed windows the question is UNANSWERABLE, not answered "none" -- returns
190// -1 so a caller must distinguish "I could not look" from "I looked and found nothing".
191func rm_sustained_count(acnt: *i64, n: i64, rounds: i64) -> i64 {
192 if rounds <= 0 { return 0 - 1 }
193 if n < 0 { return 0 - 1 }
194 var c: i64 = 0
195 var i: i64 = 0
196 while i < n {
197 if acnt[i] >= rounds { c = c + 1 }
198 i = i + 1
199 }
200 return c
201}
202
203// ★★★★★★WHY A BLIND AXIS NEEDS A *CLASSIFIER* AND NOT JUST A FLAG: abstention is safe for the verdict
204// and invisible to the operator, so a detector that goes blind raises no alarm anywhere, by design --
205// and "I could not look" collapses two states with OPPOSITE remedies into one word. MEASURED
206// 2026-08-21: the leak axis was UNOBSERVABLE for 7.2 h (~43 missed 600 s beats) while the pressure
207// axis went amber, and FOUR lanes reached FOUR different mechanisms, because a beat that RAN AND WAS
208// REFUSED by admission control and a beat that IS DEAD are indistinguishable from outside. nx_memvel
209// now stamps knowledge/status/memvel.refused on every refusal, which is what makes them separable.
210// 0 = NOT-BLIND -- the measurement itself is fresh; nothing to report.
211// 1 = REFUSING -- no fresh measurement, but a FRESH refusal stamp: the beat IS alive and admission
212// declined it. Remedy is to shed load; the axis recovers on its own.
213// 2 = DEAD -- neither artifact is fresh: nothing is running. Remedy is to fix the beat.
214// ★PURE BY CONSTRUCTION -- a function of already-measured numbers, so its referee needs no filesystem,
215// no fixture and no clock, and therefore cannot be fooled by any of them. This is the same reason
216// rm_sustained_count above lives here rather than in the organ: THE CLASSIFICATION IS THE DECISION.
217// ⚠A NEGATIVE AGE IS CLOCK SKEW, refused exactly as the measurement path refuses it -- accepting one
218// would let a future-stamped file read as fresh forever, the stale-fixture defect wearing a clock.
219// ⚠TOTAL: every (leak_observed, refused_age, max_age) triple returns exactly one of 0/1/2, so there is
220// no input for which a caller can receive "no answer" and quietly substitute its own.
221func rm_blind_class(leak_observed: i64, refused_age: i64, max_age: i64) -> i64 {
222 if leak_observed == 1 { return 0 }
223 if refused_age < 0 { return 2 }
224 if refused_age > max_age { return 2 }
225 return 1
226}
227
228// ---- THE ALARM AN ABSTAINING AXIS OWES (2026-08-26) -------------------------------------------------
229// Abstention is SAFE for the verdict and INVISIBLE to the operator, so a detector that goes blind raises
230// nothing anywhere -- by design. rm_verdict is right to let UNOBSERVABLE cast no vote (guessing would be
231// worse), but that is only HALF a mechanism: the other half is an alarm on the blindness ITSELF.
232// MEASURED 2026-08-26: leak_axis=UNOBSERVABLE with memvel_age_s=1483 against max_age_s=900 while the
233// LAGGING pressure axis was RED at swap 895 permil -- the LEADING indicator dark exactly when it was most
234// wanted, which is the same pairing recorded on 2026-08-21. It is silent whenever pressure happens to be
235// fine, and that is the dangerous case, not this one.
236// WHY TWICE THE BOUND, DERIVED not picked: max_age is already sized to tolerate one FULL missed beat
237// (the live conf says so in its own words -- 900s against a 600s beat). One window stale is therefore a
238// transient miss the abstention already covers. TWO full windows cannot be a single missed beat, so it is
239// a STANDING blindness. The multiplier is named rather than inlined so the reasoning travels with it.
240const RM_BLIND_ALARM_WINDOWS: i64 = 2
241
242// PURE and TOTAL like its siblings: a function of already-measured numbers, no clock, no filesystem.
243// Returns a SEVERITY CONTRIBUTION, never a leak claim -- we do not know whether anything is leaking, and
244// saying so would be the acquittal-by-abstention defect inverted into a conviction.
245func rm_blind_sev(leak_observed: i64, leak_age: i64, max_age: i64) -> i64 {
246 if leak_observed == 1 { return 0 } // we can see; there is nothing to alarm about
247 if max_age <= 0 { return 0 } // no declared bound => cannot judge the blindness either; abstain
248 if leak_age < 0 { return 1 } // clock skew: the age is unusable, which IS a blindness
249 if leak_age >= max_age * RM_BLIND_ALARM_WINDOWS { return 1 }
250 return 0
251}
252
253// ---- AXIS D: DISK. THE AXIS THIS ORGAN NEVER HAD (2026-08-28) --------------------------------------
254// WHY IT EXISTS. On 2026-08-28 a 100%-FULL DISK truncated a memory index to 0 bytes: open(path,"w")
255// truncates BEFORE it writes, so a full volume does not refuse a write, it DESTROYS the file. This organ
256// is "the resource axis nx_health lacks" and it measured MEMORY and SWAP only; a search for the disk
257// primitive returned matches=0 for BOTH sys_statfs and statvfs with corpus_complete=1.
258//
259// ★★★★★★THE BAR IS ABSOLUTE FREE BYTES, NOT A PERCENTAGE, AND THAT IS THE WHOLE DESIGN. A permil bar is
260// SIZE-BLIND, and the three volumes measured the day this shipped prove it in one line each:
261// /volume1 879 permil used -- 21.0 TB free -- PERFECTLY HEALTHY
262// NAS / 774 permil used -- 525 MiB free -- THE DANGEROUS ONE
263// laptop C: 912 permil used -- 81 GB free -- fine, and it is the volume that hit 100% earlier
264// A percentage threshold set anywhere useful would have ALARMED on the 21 TB volume and stayed SILENT on
265// the 525 MiB one -- exactly backwards. What ends a write is RUNNING OUT, not being 90% full, so the
266// severity is a function of bytes remaining and the permil rides along as CONTEXT only.
267//
268// PURE and TOTAL like its siblings: a function of already-measured numbers, no clock, no filesystem.
269// A NEGATIVE avail is STATFS_ERR -- could-not-look -- and contributes NOTHING. An unmeasurable volume
270// must never be scored as roomy, and must never be scored as full either: abstention is not a claim.
271func rm_disk_sev(avail_bytes: i64, amber_bytes: i64, red_bytes: i64) -> i64 {
272 if avail_bytes < 0 { return 0 }
273 if red_bytes > 0 { if avail_bytes <= red_bytes { return 2 } }
274 if amber_bytes > 0 { if avail_bytes <= amber_bytes { return 1 } }
275 return 0
276}
277
278// Composes rm_verdict_blind exactly as that composed rm_verdict: the pressure and leak axes keep their
279// arithmetic byte-for-byte and this can only ever RAISE. A run that was GREEN before can become non-GREEN
280// ONLY when a volume genuinely crosses its byte floor -- which is the entire point, and is stated plainly
281// rather than hidden behind the word "additive".
282func rm_verdict_disk(swused_pm: i64, avail_pm: i64, leakers: i64, leak_observed: i64,
283 sw_amber: i64, sw_red: i64, av_amber: i64, av_red: i64,
284 leak_amber: i64, leak_red: i64,
285 leak_age: i64, max_age: i64,
286 disk_avail_bytes: i64, disk_amber_bytes: i64, disk_red_bytes: i64) -> i64 {
287 var sev: i64 = rm_verdict_blind(swused_pm, avail_pm, leakers, leak_observed,
288 sw_amber, sw_red, av_amber, av_red, leak_amber, leak_red,
289 leak_age, max_age)
290 let d: i64 = rm_disk_sev(disk_avail_bytes, disk_amber_bytes, disk_red_bytes)
291 if d > sev { sev = d }
292 return sev
293}
294
295// Composes the incumbent rather than restating it: rm_verdict keeps its exact arithmetic and this only
296// ever RAISES, never lowers -- a blind leading axis must not be able to mask a red pressure axis.
297func rm_verdict_blind(swused_pm: i64, avail_pm: i64, leakers: i64, leak_observed: i64,
298 sw_amber: i64, sw_red: i64, av_amber: i64, av_red: i64,
299 leak_amber: i64, leak_red: i64,
300 leak_age: i64, max_age: i64) -> i64 {
301 var sev: i64 = rm_verdict(swused_pm, avail_pm, leakers, leak_observed,
302 sw_amber, sw_red, av_amber, av_red, leak_amber, leak_red)
303 let b: i64 = rm_blind_sev(leak_observed, leak_age, max_age)
304 if b > sev { sev = b }
305 return sev
306}