nordisk_alloc/lib.rs
1//! # nordisk-alloc — the fleet's allocator, configured for a process that lives for weeks
2//!
3//! Every fat nordisk binary installs `mimalloc` as its process-wide global
4//! allocator. That choice is right: the workloads are many-thread allocation storms
5//! (nornir's deep-scan flush, korp's ingest, gunnar's pack walk) and mimalloc's
6//! per-thread heaps scale them near-linear where glibc's arena contention does not.
7//!
8//! What was missing everywhere is the other half of that choice — **telling mimalloc
9//! to hand freed memory back to the OS.**
10//!
11//! ## The defect, and why it is a FLEET defect
12//!
13//! * `mimalloc = "0.1"` resolves `libmimalloc-sys`, whose `build.rs` compiles the
14//! **v3** C sources unless the `v2` cargo feature is set. Nothing in this
15//! constellation sets it, so every fat node ships mimalloc v3.
16//! * mimalloc **v2** defaulted `purge_delay` to **10 ms**. mimalloc **v3** defaults it
17//! to **1000 ms** (`c_src/mimalloc/v3/src/options.c`, the `purge_delay` row).
18//! * MEASURED on `mimalloc` 0.1.52 / `libmimalloc-sys` 0.1.49, a program that builds a
19//! 2 GB heap of ~1 KB `String`s and drops it, three times over:
20//!
21//! | configuration | peak RSS | RSS after the drop |
22//! |------------------------------|----------|--------------------|
23//! | as shipped (v3 default 1000) | 2006 MB | **2006 MB** — never returned |
24//! | `MIMALLOC_PURGE_DELAY=10` | 2006 MB | 156 MB |
25//! | `MIMALLOC_PURGE_DELAY=0` | 2006 MB | **7 MB** |
26//!
27//! At the shipped default the freed 2 GB is retained by the allocator for the life
28//! of the process. RSS is therefore not "memory in use" — it is the **running
29//! maximum** of every transient the process has ever built.
30//!
31//! ### INCIDENT 2026-08-17/18 — `dwarves-server` OOM-killed the 62 GB `dwarf` box
32//!
33//! Eleven kills over two days, the last four at 22:43 · 23:23 · 00:20 · 01:15, each at
34//! `anon-rss` 49–53 GB with `total-vm` 61–73 GB, after which the host itself died at
35//! 02:17:31 with journald reporting 152 × "Under memory pressure, flushing caches".
36//! `anon-rss` at each kill EQUALLED systemd's reported `memory peak` for that
37//! invocation — RSS *being* the high-water mark is exactly what that equality means.
38//! Near-identical kill RSS across all eleven: the ceiling is set by the same sweep
39//! peak each time, not by anything that accumulates per request.
40//!
41//! On a real embedded-nornir republish (357 dep crates, 13 344 files, 12 023 symbol
42//! scans), two independent pairs, same binary, allocator the only variable:
43//!
44//! | run | PEAK RSS | settled floor |
45//! |------------------|----------|---------------|
46//! | as shipped | 3.08 GB | 2.53 GB |
47//! | `purge_delay=10` | 2.71 GB | 2.46 GB |
48//! | as shipped | 3.38 GB | 2.49 GB |
49//! | `purge_delay=10` | 2.72 GB | 2.43 GB |
50//!
51//! The **peak** falls 12–20 %, reproducibly — the term that matters for an OOM kill,
52//! since the process dies AT its high-water mark. The settled **floor** barely moves,
53//! because at ~2.5 GB it is *live* memory, not allocator-held. **This crate lowers the
54//! ceiling the kernel trips on. It is not a claim that 50 GB becomes 500 MB.**
55//!
56//! ## Why a shared leaf and not a copy per repo
57//!
58//! The fix was measured, written and proved inside `dwarves` — and stayed there. As of
59//! 2026-08-25 `alloc.rs` existed only under `dwarves/crates/dwarves/src/`, while
60//! `nornir/src/bin/nornir-server.rs:19` still installed a bare
61//! `mimalloc::MiMalloc` with no tuning at all, and `purge_delay` had **zero hits**
62//! anywhere in edda. So the one process that was measured ratcheting got the fix and
63//! every other nordisk binary — each running the same allocator, with the same v3
64//! default, for the same weeks at a time — did not. LAW #5: one owner, and the
65//! consumers point at it.
66//!
67//! Nothing here is re-derived. The option indices, the fingerprint check, the 10 ms
68//! value and the reasoning are the ones dwarves MEASURED; this leaf is where they
69//! live so the other five binaries can have them.
70//!
71//! ## What this crate does — and what it deliberately does not do
72//!
73//! [`tune_for_long_lived_server`] sets `purge_delay` to [`DEFAULT_PURGE_DELAY_MS`]
74//! (10 ms — mimalloc v2's long-standing value) and pins `arena_purge_mult` to 1, so a
75//! freed arena slice is `madvise`d back on the same schedule as a freed page. A
76//! transient becomes transient again and RSS tracks live memory instead of history.
77//!
78//! It is **not** a memory limit (that is a `MemoryMax=` in the unit —
79//! `skidbladnir::service::ServiceSpec::memory_max`), **not** a periodic `mi_collect`
80//! sweeper, and it does not make any individual transient smaller. Shrinking the
81//! transients themselves belongs to whichever repo builds them.
82//!
83//! ## Why the option is set through FFI and not `MIMALLOC_PURGE_DELAY`
84//!
85//! mimalloc initialises each option lazily on first read and then latches it
86//! (`MI_OPTION_UNINIT` → `MI_OPTION_INITIALIZED`). `purge_delay` is already read
87//! during pre-`main` allocation, so `std::env::set_var` at the top of `main` is too
88//! late — MEASURED: the env-in-`main` variant retained the full 2006 MB, exactly like
89//! the untuned default, while the `mi_option_set` variant returned it. Only
90//! `mi_option_set` works after the fact, which is why `libmimalloc-sys` is linked with
91//! its `extended` feature. An operator who wants the env route still has it: setting
92//! `MIMALLOC_PURGE_DELAY` in the unit is read by mimalloc *before* `main` and the
93//! `off` opt-out below then leaves it alone.
94//!
95//! This is the LAW's last-resort FFI clause used honestly: mimalloc is a C allocator,
96//! there is no Rust reimplementation of it to reach for, and the alternative is not a
97//! Rust version of the option table — it is no fix at all.
98//!
99//! ## Using it
100//!
101//! ```no_run
102//! // In the final artifact — only an artifact may declare a global allocator.
103//! #[global_allocator]
104//! static GLOBAL: nordisk_alloc::MiMalloc = nordisk_alloc::MiMalloc;
105//!
106//! fn main() {
107//! // As early as possible; the product name picks the per-product env override.
108//! eprintln!("{}", nordisk_alloc::tune_for_long_lived_server("nornir").log_line());
109//! }
110//! ```
111
112pub mod selftest;
113
114/// **The allocator itself**, re-exported so a consumer needs ONE dependency to both
115/// install and tune it: `#[global_allocator] static G: nordisk_alloc::MiMalloc = …`.
116pub use mimalloc::MiMalloc;
117
118/// `mi_option_purge_delay`, as an index into mimalloc's option table.
119///
120/// `libmimalloc-sys` exports constants only for the stable options, and `purge_delay`
121/// is not one of them, so the index is named here. It is the 16th row of the table in
122/// `c_src/mimalloc/v3/src/options.c` (0-based: 15), and [`tune_for_long_lived_server`]
123/// **verifies** it before writing: v3 gives `purge_delay` the default 1000, and no
124/// other option in that table defaults to 1000, so reading 1000 back identifies the row.
125const MI_OPTION_PURGE_DELAY: i32 = 15;
126
127/// `mi_option_arena_purge_mult`, likewise by index (row 25, 0-based 24). The v3
128/// default is already 1; it is pinned anyway so a future default change cannot
129/// silently re-lengthen the arena purge deadline behind this fix.
130const MI_OPTION_ARENA_PURGE_MULT: i32 = 24;
131
132/// The `purge_delay` this crate installs, in milliseconds — mimalloc **v2**'s
133/// long-standing default, and the value measured above to return a dropped 2 GB
134/// transient to the OS.
135///
136/// Not 0: purging on every free trades the OOM for `madvise` churn on the hot
137/// allocation path. 10 ms is long enough to let a burst reuse its own pages and short
138/// enough that the deadline actually expires within the burst.
139pub const DEFAULT_PURGE_DELAY_MS: i64 = 10;
140
141/// The **fleet-wide** env override: a value in milliseconds, or `off`.
142///
143/// Consulted second, after the per-product variable — see [`product_purge_delay_env`].
144/// It exists so an operator can set one variable in a slice/environment file and have
145/// every nordisk service on the box honour it, rather than needing to know each
146/// binary's own spelling.
147pub const GENERIC_PURGE_DELAY_ENV: &str = "NORDISK_MIMALLOC_PURGE_DELAY_MS";
148
149/// The **per-product** env override for `product`, consulted FIRST:
150/// `product_purge_delay_env("dwarves")` is `"DWARVES_MIMALLOC_PURGE_DELAY_MS"`.
151///
152/// This is not decoration. `DWARVES_MIMALLOC_PURGE_DELAY_MS` is the variable the fix
153/// already shipped with and that an operator may already have set on `dwarf`;
154/// deriving the name per product means dwarves repoints at this leaf without any
155/// deployed override silently ceasing to work.
156///
157/// `-` becomes `_` and the name is upper-cased, so `"korp-server"` gives
158/// `KORP_SERVER_MIMALLOC_PURGE_DELAY_MS`.
159pub fn product_purge_delay_env(product: &str) -> String {
160 let stem: String = product
161 .chars()
162 .map(|c| if c == '-' || c == '.' { '_' } else { c })
163 .collect();
164 format!("{}_MIMALLOC_PURGE_DELAY_MS", stem.to_uppercase())
165}
166
167/// The value mimalloc **v3** ships as the `purge_delay` default — the number this
168/// crate exists because of, and the fingerprint [`tune_for_long_lived_server`] uses to
169/// confirm it is writing to the row it means to.
170pub const MIMALLOC_V3_DEFAULT_PURGE_DELAY_MS: i64 = 1000;
171
172/// What [`purge_delay_ms`] decided, and why — a value rather than a log line, so the
173/// decision is unit-testable without a process.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum PurgeDelayPlan {
176 /// Install this many milliseconds (the default, or an operator's override).
177 Set(i64),
178 /// An override said `off` — change nothing.
179 LeaveAlone,
180}
181
182/// Decide the `purge_delay` to install from ONE explicit env value — pure, so the
183/// ladder is testable without mutating process environment.
184///
185/// * `None` / empty ⇒ [`PurgeDelayPlan::Set`]`(`[`DEFAULT_PURGE_DELAY_MS`]`)`.
186/// * `off` (any case) ⇒ [`PurgeDelayPlan::LeaveAlone`].
187/// * a non-negative integer ⇒ [`PurgeDelayPlan::Set`] of it.
188/// * anything else ⇒ the default. A typo must not silently disable the fix that keeps
189/// the box alive, so an unparseable value falls FORWARD to the safe value rather
190/// than back to mimalloc's.
191pub fn purge_delay_plan(env: Option<&str>) -> PurgeDelayPlan {
192 let raw = match env.map(str::trim) {
193 None | Some("") => return PurgeDelayPlan::Set(DEFAULT_PURGE_DELAY_MS),
194 Some(v) => v,
195 };
196 if raw.eq_ignore_ascii_case("off") {
197 return PurgeDelayPlan::LeaveAlone;
198 }
199 match raw.parse::<i64>() {
200 Ok(ms) if ms >= 0 => PurgeDelayPlan::Set(ms),
201 _ => PurgeDelayPlan::Set(DEFAULT_PURGE_DELAY_MS),
202 }
203}
204
205/// The two-rung ladder, pure: the per-product value wins over the fleet-wide one, and
206/// an EMPTY per-product value is not a decision — it falls through to the generic
207/// rather than shadowing it.
208///
209/// That fall-through is the whole reason this is a separate function: a service
210/// started from a unit that mentions the variable without giving it a value would
211/// otherwise silently override an operator's fleet-wide setting with the default.
212pub fn purge_delay_plan_from(product: Option<&str>, generic: Option<&str>) -> PurgeDelayPlan {
213 fn said_something(v: Option<&str>) -> Option<&str> {
214 v.map(str::trim).filter(|s| !s.is_empty())
215 }
216 purge_delay_plan(said_something(product).or_else(|| said_something(generic)))
217}
218
219/// [`purge_delay_plan_from`] over the real environment: `<PRODUCT>_…` then
220/// [`GENERIC_PURGE_DELAY_ENV`].
221pub fn purge_delay_ms(product: &str) -> PurgeDelayPlan {
222 let specific = std::env::var(product_purge_delay_env(product)).ok();
223 let generic = std::env::var(GENERIC_PURGE_DELAY_ENV).ok();
224 purge_delay_plan_from(specific.as_deref(), generic.as_deref())
225}
226
227/// What [`tune_for_long_lived_server`] actually did to the running allocator —
228/// returned (not just logged) so a caller and a test can both read it.
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct AllocTuning {
231 /// The product name the tuning was asked for — it names the process in
232 /// [`log_line`](AllocTuning::log_line) and picks the env override.
233 pub product: String,
234 /// `purge_delay` before the call (mimalloc's own resolution).
235 pub purge_delay_before_ms: i64,
236 /// `purge_delay` after the call.
237 pub purge_delay_after_ms: i64,
238 /// `arena_purge_mult` after the call.
239 pub arena_purge_mult: i64,
240 /// False when NOTHING was written — either an override said `off`, or the option
241 /// row did not carry a value this crate recognises (see [`MI_OPTION_PURGE_DELAY`]).
242 pub applied: bool,
243}
244
245impl AllocTuning {
246 /// The one boot line an operator reads. Says what changed, or why it did not —
247 /// and names the variable that would turn it back on, for THIS product.
248 pub fn log_line(&self) -> String {
249 if !self.applied {
250 return format!(
251 "{}: allocator purge_delay left at {} ms (`{}`/`{GENERIC_PURGE_DELAY_ENV}` \
252 said off, or an unrecognised mimalloc option layout) — a dropped \
253 transient may stay resident; see nordisk_alloc",
254 self.product,
255 self.purge_delay_before_ms,
256 product_purge_delay_env(&self.product),
257 );
258 }
259 format!(
260 "{}: allocator purge_delay {} ms → {} ms (arena_purge_mult {}) — freed \
261 transients return to the OS",
262 self.product,
263 self.purge_delay_before_ms,
264 self.purge_delay_after_ms,
265 self.arena_purge_mult,
266 )
267 }
268}
269
270/// **Configure the running mimalloc so freed memory goes back to the OS.** Call once,
271/// as early in `main` as the process can.
272///
273/// `product` names the binary for the log line and picks its env override — see
274/// [`product_purge_delay_env`].
275///
276/// Idempotent: calling it twice writes the same values and the second call simply
277/// reports `before == after`.
278///
279/// Refuses to write when the option row does not read back
280/// [`MIMALLOC_V3_DEFAULT_PURGE_DELAY_MS`] or the value this crate already installed —
281/// if a future mimalloc reorders its option table, poking a numbered row would
282/// configure something else entirely, and a wrong write is worse than no write. The
283/// refusal is reported, never silent, and the behavioural regression test
284/// (`tests/mimalloc_transient_peak.rs`) goes red on it.
285pub fn tune_for_long_lived_server(product: &str) -> AllocTuning {
286 // SAFETY: `mi_option_get`/`mi_option_set` are thread-safe reads/writes of
287 // mimalloc's static option table (`options.c`), valid at any point in the
288 // process' life. The allocator is already initialised — the Rust runtime
289 // allocated before `main`.
290 let before = unsafe { libmimalloc_sys::mi_option_get(MI_OPTION_PURGE_DELAY) } as i64;
291 let mult = || unsafe { libmimalloc_sys::mi_option_get(MI_OPTION_ARENA_PURGE_MULT) } as i64;
292 let untouched = |arena_purge_mult| AllocTuning {
293 product: product.to_string(),
294 purge_delay_before_ms: before,
295 purge_delay_after_ms: before,
296 arena_purge_mult,
297 applied: false,
298 };
299
300 let want = match purge_delay_ms(product) {
301 PurgeDelayPlan::LeaveAlone => return untouched(mult()),
302 PurgeDelayPlan::Set(ms) => ms,
303 };
304
305 // The row fingerprint: mimalloc v3's own default, or a value we already wrote.
306 if !(before == MIMALLOC_V3_DEFAULT_PURGE_DELAY_MS || before == want) {
307 return untouched(mult());
308 }
309
310 // SAFETY: as above.
311 unsafe {
312 libmimalloc_sys::mi_option_set(MI_OPTION_PURGE_DELAY, want as std::os::raw::c_long);
313 libmimalloc_sys::mi_option_set(MI_OPTION_ARENA_PURGE_MULT, 1);
314 }
315 AllocTuning {
316 product: product.to_string(),
317 purge_delay_before_ms: before,
318 purge_delay_after_ms: current_purge_delay_ms(),
319 arena_purge_mult: mult(),
320 applied: true,
321 }
322}
323
324/// The `purge_delay` the running allocator is currently using, in milliseconds.
325///
326/// Exposed so the behavioural cell (`tests/mimalloc_transient_peak.rs`) can state its
327/// premise — "the shipped default really is [`MIMALLOC_V3_DEFAULT_PURGE_DELAY_MS`]" —
328/// without duplicating the option index that [`tune_for_long_lived_server`] guards.
329pub fn current_purge_delay_ms() -> i64 {
330 // SAFETY: a thread-safe read of mimalloc's static option table.
331 unsafe { libmimalloc_sys::mi_option_get(MI_OPTION_PURGE_DELAY) as i64 }
332}
333
334/// This process' resident set in KiB, read from `/proc/self/statm` (field 2, in
335/// pages). The one measurement that distinguishes "freed" from "returned", and the
336/// thing the regression test asserts on. `None` off Linux or when `/proc` is absent.
337pub fn self_rss_kb() -> Option<u64> {
338 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
339 let pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
340 let page_kb = 4u64; // every target this ships on
341 Some(pages * page_kb)
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 /// The env ladder, including the two ways it must NOT fail: a typo and a negative
349 /// number both fall FORWARD to the safe default rather than back to mimalloc's
350 /// 1000 ms.
351 #[test]
352 fn the_purge_delay_ladder_never_falls_back_to_the_default_that_caused_the_oom() {
353 assert_eq!(
354 purge_delay_plan(None),
355 PurgeDelayPlan::Set(DEFAULT_PURGE_DELAY_MS)
356 );
357 assert_eq!(
358 purge_delay_plan(Some(" ")),
359 PurgeDelayPlan::Set(DEFAULT_PURGE_DELAY_MS)
360 );
361 assert_eq!(purge_delay_plan(Some("0")), PurgeDelayPlan::Set(0));
362 assert_eq!(purge_delay_plan(Some(" 25 ")), PurgeDelayPlan::Set(25));
363 assert_eq!(purge_delay_plan(Some("OFF")), PurgeDelayPlan::LeaveAlone);
364 assert_eq!(purge_delay_plan(Some("off")), PurgeDelayPlan::LeaveAlone);
365 // A typo must not disable the fix.
366 assert_eq!(
367 purge_delay_plan(Some("ten")),
368 PurgeDelayPlan::Set(DEFAULT_PURGE_DELAY_MS)
369 );
370 assert_eq!(
371 purge_delay_plan(Some("-1")),
372 PurgeDelayPlan::Set(DEFAULT_PURGE_DELAY_MS)
373 );
374 // And the default we install must not be mimalloc v3's.
375 assert_ne!(
376 DEFAULT_PURGE_DELAY_MS, MIMALLOC_V3_DEFAULT_PURGE_DELAY_MS,
377 "installing v3's own default would be a no-op fix"
378 );
379 }
380
381 /// **The per-product rung — the one thing this leaf adds over the copy it came
382 /// from, and the reason repointing dwarves cannot break a deployed override.**
383 ///
384 /// dwarves shipped with `DWARVES_MIMALLOC_PURGE_DELAY_MS`. If moving the code here
385 /// only understood `NORDISK_…`, an operator who had set the old variable on `dwarf`
386 /// would get the default back with no message anywhere — the silent kind of
387 /// regression a lift-and-shift produces.
388 #[test]
389 fn the_products_own_variable_still_works_and_an_empty_one_does_not_shadow_the_fleet() {
390 assert_eq!(
391 product_purge_delay_env("dwarves"),
392 "DWARVES_MIMALLOC_PURGE_DELAY_MS"
393 );
394 assert_eq!(
395 product_purge_delay_env("nornir"),
396 "NORNIR_MIMALLOC_PURGE_DELAY_MS"
397 );
398 // A hyphenated binary name is a legal env stem only after the hyphen goes.
399 assert_eq!(
400 product_purge_delay_env("korp-server"),
401 "KORP_SERVER_MIMALLOC_PURGE_DELAY_MS"
402 );
403
404 // The product rung wins.
405 assert_eq!(
406 purge_delay_plan_from(Some("off"), Some("25")),
407 PurgeDelayPlan::LeaveAlone
408 );
409 assert_eq!(
410 purge_delay_plan_from(Some("40"), Some("25")),
411 PurgeDelayPlan::Set(40)
412 );
413 // …and the fleet rung is reached when the product says nothing.
414 assert_eq!(
415 purge_delay_plan_from(None, Some("25")),
416 PurgeDelayPlan::Set(25)
417 );
418 // AN EMPTY PRODUCT VALUE IS NOT A DECISION. A unit that mentions the variable
419 // without a value must not shadow the operator's fleet-wide setting.
420 assert_eq!(
421 purge_delay_plan_from(Some(""), Some("off")),
422 PurgeDelayPlan::LeaveAlone
423 );
424 assert_eq!(
425 purge_delay_plan_from(Some(" "), Some("25")),
426 PurgeDelayPlan::Set(25)
427 );
428 // Neither says anything ⇒ the safe default.
429 assert_eq!(
430 purge_delay_plan_from(None, None),
431 PurgeDelayPlan::Set(DEFAULT_PURGE_DELAY_MS)
432 );
433 }
434
435 /// The option row is still `purge_delay`, the tuning is a real write, and it is
436 /// idempotent. **One test, not three**, because the option table is process-global
437 /// mutable state and cargo runs `#[test]`s on parallel threads — split up, the
438 /// row-identity assertion would race the write and flake.
439 ///
440 /// RED WHEN BROKEN: a mimalloc upgrade that reorders its option table (or changes
441 /// the default) fails the first assertion, in a named test, instead of silently
442 /// configuring some unrelated option in production.
443 ///
444 /// Note the test harness does NOT install `MiMalloc` as its global allocator (only
445 /// a final artifact may), but the C library is linked and its option table is the
446 /// same static — so the row identity is exactly what a fat binary would see.
447 #[test]
448 fn the_named_option_row_is_purge_delay_and_the_tuning_applies_idempotently() {
449 // SAFETY: a thread-safe read of mimalloc's static option table.
450 let got = unsafe { libmimalloc_sys::mi_option_get(MI_OPTION_PURGE_DELAY) } as i64;
451 assert_eq!(
452 got, MIMALLOC_V3_DEFAULT_PURGE_DELAY_MS,
453 "option row {MI_OPTION_PURGE_DELAY} no longer reads mimalloc v3's \
454 purge_delay default ({MIMALLOC_V3_DEFAULT_PURGE_DELAY_MS} ms) but {got} — \
455 the option table moved; re-derive the index from \
456 libmimalloc-sys/c_src/mimalloc/v3/src/options.c before trusting it"
457 );
458 // arena_purge_mult's v3 default is 1 — the other half of the fingerprint.
459 let mult = unsafe { libmimalloc_sys::mi_option_get(MI_OPTION_ARENA_PURGE_MULT) } as i64;
460 assert_eq!(
461 mult, 1,
462 "option row {MI_OPTION_ARENA_PURGE_MULT} no longer reads arena_purge_mult's \
463 v3 default (1) but {mult}"
464 );
465
466 let first = tune_for_long_lived_server("nordisk-alloc-selftest");
467 assert!(first.applied, "{}", first.log_line());
468 assert_eq!(first.purge_delay_after_ms, DEFAULT_PURGE_DELAY_MS);
469 assert_eq!(first.arena_purge_mult, 1);
470 assert!(
471 first.log_line().contains("return to the OS"),
472 "{}",
473 first.log_line()
474 );
475
476 let second = tune_for_long_lived_server("nordisk-alloc-selftest");
477 assert!(
478 second.applied,
479 "a second call is a no-op write, not a refusal"
480 );
481 assert_eq!(second.purge_delay_before_ms, DEFAULT_PURGE_DELAY_MS);
482 assert_eq!(second.purge_delay_after_ms, DEFAULT_PURGE_DELAY_MS);
483 }
484
485 /// The opt-out is a real escape hatch and it is HONEST about it — an operator who
486 /// turns the fix off gets a line saying a dropped transient may stay resident, and
487 /// the line names THAT PRODUCT's variable, not some other binary's.
488 #[test]
489 fn the_opt_out_is_reported_and_never_silent() {
490 let untuned = AllocTuning {
491 product: "nornir".into(),
492 purge_delay_before_ms: MIMALLOC_V3_DEFAULT_PURGE_DELAY_MS,
493 purge_delay_after_ms: MIMALLOC_V3_DEFAULT_PURGE_DELAY_MS,
494 arena_purge_mult: 1,
495 applied: false,
496 };
497 let line = untuned.log_line();
498 assert!(line.contains("may stay resident"), "{line}");
499 assert!(
500 line.contains("1000"),
501 "the line must name the value in force: {line}"
502 );
503 assert!(
504 line.contains("NORNIR_MIMALLOC_PURGE_DELAY_MS"),
505 "the line must name the variable that turns it back on FOR THIS PRODUCT: {line}"
506 );
507 assert!(line.starts_with("nornir:"), "{line}");
508 }
509
510 /// `/proc/self/statm` is readable and plausible — the regression test's whole
511 /// measurement rests on it.
512 #[test]
513 fn self_rss_is_readable_and_nonzero() {
514 let rss = self_rss_kb().expect("/proc/self/statm on linux");
515 assert!(rss > 1024, "an rss of {rss} KiB is not a running process");
516 }
517}