navian-memcheck
Resource-leak / soak testing as a cargo test assertion. Prove that a
workload's memory plateaus after warmup — catching the reachable-but-unbounded
growth that leak detectors are built to ignore.
The bug that motivated this took a process to 52 GB and got it OOM-killed — but
nothing leaked. Every byte was reachable: a per-session map that grew and never
evicted. LeakSanitizer and Valgrind memcheck classify that as "still in use",
not a leak. A profiler shows it, but only as a flamegraph a human reads.
navian-memcheck asserts the property that was actually violated: after warmup,
live memory levels off.
It catches three failure modes leak detectors miss:
- Temporal — at steady load, memory climbs the longer the process runs.
- Unbounded — memory scales with load and never comes back down.
- Type — memory grows faster than its input: the wrong Big-O.
Quickstart — zero code changes
Add the crate and write one test. There's no allocator to install and no setup.
[]
= "0.1"
use ;
Or the one-liner:
assert_plateau;
On a workload that leaks, the failure reads like this:
navian-memcheck: FAIL — still growing: back-half slope 620,389 B/sample > budget 4,096 B/sample
Asserting the load and shape questions too
use ;
// Unbounded check: memory stays under a cap as the driver scales.
assert_bounded;
// Type check: memory grows at most linearly in the driver (no O(n²) blowup).
assert_linear_in;
How it works
It samples live memory on a fixed cadence, fits an ordinary-least-squares line through the back half of the run, and fails if the slope exceeds your budget or a hard cap is breached. RSS is noisy, so the slope budget is your noise knob — set it above your platform's jitter, or use the jemalloc sampler for a clean, low-noise signal. Because reproducing a memory bug doesn't require reproducing production throughput, you drive your event handler directly — bounded by CPU, not I/O — so a climb that takes hours in production surfaces in seconds in CI. Run it from a seed (or under navian-dst) so a failing run reproduces and can be bisected to the event that caused it.
Sampling precision (optional)
The default [RssSampler] reads process RSS — zero-setup, but coarser. For a
cleaner in-process signal, enable the jemalloc feature and pass JemallocSampler
to soak_with; it reads jemalloc's live-heap counter. That needs jemalloc as the
global allocator (one line, and free if your service already uses it).
When to reach for something else
navian-memcheck is a detector, not a localizer. It tells you that memory grew
and gates CI on it — it does not name the allocation site. Once it's red and you
need to find the exact line, hand off to a heap profiler:
dhat, heaptrack, or
Valgrind. And for a one-time leak at startup (freed by exit, so it never grows),
a sanitizer (-Zsanitizer=leak) is the right tool. This crate owns the gap none of
those cover: the always-on gate for unbounded growth under load.
Whole binaries
The navian-memcheck CLI soaks any process's RSS over a
duration and exits non-zero for CI — no code required.