Skip to main content

Crate thresher

Crate thresher 

Source
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::alloc returns null, and Rust turns that into handle_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.