Skip to main content

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}