Expand description
A memory allocation wrapper that hits a callback when a threshold is reached:
use std::alloc;
use thresher::Thresher;
#[global_allocator]
// Wrap the standard system allocator
static THRESHER: Thresher<alloc::System> = Thresher::new(alloc::System);
fn main() {
// Set the threshold we care about reaching
THRESHER.set_callback_threshold(100 * 1024 * 1024);
// Set the callback when the threshold is reached (note: may be called multiple times)
THRESHER.set_callback(|allocation| {
println!("Threshold reached! Allocated: {} bytes", allocation);
});
}§Accounting
Every allocation and deallocation in the process passes through here, so the bookkeeping has to be close to free or it becomes the bottleneck it is supposed to be watching.
Rather than touch a shared counter on every call, each thread keeps its own
running balance and only reconciles with the shared total once it has drifted
by RECONCILE_BATCH bytes. Allocators keep their arenas thread local for
the same reason: a single atomic that every thread has to write to serialises
all of them on one cache line. A thread reconciles what it is still holding
when it exits, so threads coming and going do not make the total drift.
The consequence is that Thresher::get_allocated is approximate: it can
lag by up to RECONCILE_BATCH bytes per running thread. For picking a
threshold in the hundreds of megabytes this does not matter; if you need the
total to be exact at a point in time, call Thresher::flush from the thread
you care about.
Note also that this only ever sees what is requested through
GlobalAlloc — an allocator holding on to freed pages, or mapping a large
arena up front, is invisible here. If you need the real resident figure you
can poll it out of band (/proc/self/statm, or your allocator’s own stats)
and feed it back in with Thresher::set_allocated.
§Two levels
There are two independent levels, and they are meant to be used together:
-
The callback threshold (
Thresher::set_callback_threshold) is advisory. Crossing it runs your callback and nothing else — dump a heap profile, shed load, drop caches. Allocation carries on. -
The hard limit (
Thresher::set_hard_limit) refuses. Allocations that would take the process past it fail:GlobalAlloc::allocreturns null, and Rust turns that intohandle_alloc_error.
Set the threshold below the limit and you get a warning shot before anything starts failing:
THRESHER.set_callback_threshold(3 * 1024 * 1024 * 1024); // profile at 3 GiB
THRESHER.set_hard_limit(3 * 1024 * 1024 * 1024 + 512 * 1024 * 1024); // refuse at 3.5 GiB§Scope
The running total is process wide, not per instance: it lives in a static,
because a thread that exits has to fold its outstanding balance back in from
a thread local destructor, which has no way back to the allocator it was
accounting for. Only one allocator can be the #[global_allocator], so there
is only one total worth keeping.
The practical consequence is that a Thresher which is not the global
allocator — one wrapping a sub-allocator to measure a single subsystem, say —
still reports and limits the whole process. Thresher::mark_current_thread is
likewise a property of the thread rather than of the instance.
Structs§
- Thresher
- The main allocation wrapper.
Thresher::new()to wrap an existing allocator
Enums§
- Enforcement
- Which threads the hard limit is allowed to fail allocations on.
Constants§
- RECONCILE_
BATCH - How far a thread’s own balance may drift from the shared total before it reconciles.