heapster 0.9.0

A lightweight wrapper enhancing the global allocator with useful metrics.
Documentation

📊 heapster

crates.io docs.rs

Lightweight heap telemetry for Rust, built on relaxed atomics.

heapster is a lightweight, generic wrapper over any GlobalAlloc that tracks allocations, deallocations, and reallocations using pure relaxed atomics.

It is designed to be always-on, allowing you to identify allocation patterns, diff heap usage between code paths, and export raw allocator metrics to your telemetry dashboards with minimal overhead.

Why Heapster?

Heap profilers like dhat or heaptrack capture rich per-allocation data but add significant overhead and require dedicated viewers. Heapster occupies a lighter tier: aggregate counters and histograms only, with overhead low enough to leave on in production.

  • Atomics-only: No mutexes, no thread-locals, no external viewer files. Just relaxed atomic counters.
  • no_std by default: Uses only core and alloc in the default build. The sole default dependency is portable-atomic, which compiles down to core atomics on targets with native 64-bit atomics and provides a fallback on those without them. The fmt and serde features add std requirements.
  • Generic over any allocator: Wraps System, jemalloc, mimalloc, or any custom GlobalAlloc.
  • Size histograms: Power-of-two buckets for allocations and reallocations make the size distribution visible at a glance.
  • Realloc classification: Tracks growth and shrinkage of reallocations, and (orthogonally) whether they forced a full memory move (copying) — a moved growth realloc counts towards both metrics.
  • Snapshot diffing: measure() returns a Stats delta for a closure, suitable for assertion-style tests and benchmark comparisons.
  • Dual view of byte movement: separate "fresh-only" and "inclusive" sums let you reason about allocation pattern shape independently from total heap pressure.

Quickstart

Add heapster to your Cargo.toml.

[dependencies]
heapster = { version = "0.X" }

Wrap your global allocator of choice (e.g., System) in your main.rs or lib.rs:

use heapster::Heapster;
use std::alloc::System;

#[global_allocator]
static GLOBAL: Heapster<System> = Heapster::new(System);

fn main() {
    // ... do some heavy work ...

    // See what has transpired in the heap
    println!("{}", GLOBAL.stats());
}

Use Cases

1. Benchmarking and regression tests

Stop guessing if a PR increased allocations. heapster lets you measure the heap stats of critical sections of code.

let (result, heap_diff) = GLOBAL.measure(|| operation_to_measure());
assert!(heap_diff.alloc_count < 10, "Regression: The operation allocated too many times!");

2. Catching reallocation thrashing

When a Vec or String grows beyond its capacity, the underlying buffer may be moved to a new location, copying the contents. Heapster's realloc_move_count makes these moves visible so you can pre-size collections that thrash.

3. Always-On production metrics

Overhead is a small constant per allocation (typically tens of nanoseconds for the atomic operations), so Heapster can be left on in production. stats() exposes a Stats struct that's straightforward to wire into a Prometheus or other metrics endpoint, especially with the serde feature enabled.

Feature Guide

  • baseline (no default features): Tracks total allocation counts, sums, and breaks down reallocations by growth and shrinkage. This provides the fastest operational speed while granting solid insights into heap use.
  • histograms: Generates power-of-two size distribution charts for allocations and reallocations. Disabling this saves the math required to compute the bucket offset and the atomic increment to the bucket array.
  • use_curr: Tracks the current active heap usage by incrementing on allocations and decrementing on deallocations.
  • use_max: Tracks the high-water mark of your heap usage. Disable this if you are experiencing thread-spinning under heavy load, as the underlying fetch_max CAS loop is the most expensive operation in the crate. (Note: Enabling this automatically enables use_curr).
  • realloc_moves: Tracks instances where a reallocation forced a full memory copy. When disabled, you still see the net growth or shrinkage of the reallocation, but skip the extra bookkeeping for pointer moves.
  • fmt / serde: Strictly formatting and serialization traits. These have zero impact on the hot path but will require the std library if enabled.
  • full: Enables all the features.

Feature Flags vs Performance

While all metrics are tracked using pure, lock-free atomics, high-concurrency workloads can still suffer from cache line bouncing when multiple threads rapidly update the same shared counters.

To squeeze out maximum performance in extreme contention scenarios, you can opt out of the default features to drastically reduce the number of atomic memory operations (AMOs) executed per allocator call.

This table illustrates the maximum number of atomic operations executed on the "happy path" (successful allocations/deallocations).

Feature Flag alloc dealloc realloc (in-place) realloc (move) Performance Impact
baseline (no features) 2 2 2 2 Ultra-low overhead. Relies purely on fast fetch_add instructions for totals and counts.
use_curr +1 +1 +1 +1 Low. Adds a fetch_add/fetch_sub pair to track the live size of the heap.
use_max +1 0 +1 (growth only) +1 (growth only) High under contention. Relies on fetch_max, which may compile to a Compare-And-Swap (CAS) loop on some hardware architectures.
histograms +1 0 +1 +1 Moderate. Adds one fetch_add to a calculated bucket array slot.
realloc_moves 0 0 0 +2 Moderate on memory moves. Adds two atomics only when the allocator is forced to move memory to satisfy a reallocation.

Simple Histogram Output

The fmt feature provides Display impls that render stats and ASCII histograms.

alloc_count: 10,949,628
alloc_avg: 2.45 KiB

dealloc_count: 10,949,372
dealloc_avg: 4.09 KiB

realloc_growth_count: 365,968
realloc_growth_avg: 49.12 KiB

realloc_move_count: 351,933
realloc_move_avg: 7.21 KiB

use_curr: 260.39 KiB
use_max: 25.01 MiB

alloc_histogram:
[    4 B ..     8 B):         2  █
[    8 B ..    16 B):   642,064  ███████████
[   16 B ..    32 B):       155  █
[   32 B ..    64 B): 1,639,279  █████████████████████████████
[   64 B ..   128 B): 1,926,643  ██████████████████████████████████
[  128 B ..   256 B): 1,123,746  ████████████████████
[  256 B ..   512 B): 1,284,154  ██████████████████████
[  512 B ..   1 KiB): 2,246,658  ████████████████████████████████████████
[  1 KiB ..   2 KiB): 1,283,935  ██████████████████████
[  2 KiB ..   4 KiB):   160,612  ██
[  4 KiB ..   8 KiB):       411  █
[  8 KiB ..  16 KiB):   320,985  █████
[ 16 KiB ..  32 KiB):         1  █
[ 32 KiB ..  64 KiB):         1  █
[ 64 KiB .. 128 KiB):   320,982  █████

realloc_growth_histogram:
[    1 B ..     2 B):        16  █
[    2 B ..     4 B):         0  
[    4 B ..     8 B):         0  
[    8 B ..    16 B):         0  
[   16 B ..    32 B):         0  
[   32 B ..    64 B):    25,477  ███
[   64 B ..   128 B):    14,976  █
[  128 B ..   256 B):     4,411  █
[  256 B ..   512 B):       106  █
[  512 B ..   1 KiB):         0  
[  1 KiB ..   2 KiB):         0  
[  2 KiB ..   4 KiB):         0  
[  4 KiB ..   8 KiB):         0  
[  8 KiB ..  16 KiB):         0  
[ 16 KiB ..  32 KiB):         0  
[ 32 KiB ..  64 KiB):   320,982  ████████████████████████████████████████

License

Dual-licensed under either of:

at your option.