Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map/tests/common/heap.rs

//! **The measuring allocator** — one writer for every "what did this cost the heap"
//! guard in this crate.
//!
//! `#[global_allocator]` is a per-BINARY item and integration tests are separate
//! binaries, so the *installation* cannot be shared; the body can, and this is it.
//! A test file that wants to measure declares
//!
//! ```ignore
//! #[path = "common/heap.rs"]
//! mod heap;
//! #[global_allocator]
//! static A: heap::Counting = heap::Counting;
//! ```
//!
//! and reaches for [`measure`]. It goes through `#[path]` rather than `mod common;`
//! because `common/mod.rs` is the wgpu device gate and pulls `wgpu` in — a heap
//! measurement must stay runnable on a host with no adapter and on a default
//! (feature-less) build.
//!
//! # Why the counters are THREAD-LOCAL
//!
//! The allocator is process-wide but `#[test]` bodies run on parallel threads, so a
//! global tally measures "whatever the whole process allocated during my window",
//! which is not what any assertion means. MEASURED 2026-08-03 in
//! `zero_copy_gpu_lane.rs` at `--test-threads=2`: a body that allocates nothing read
//! **777 378 B**, and the number moved run to run. Scoping to the measuring thread
//! fixes it by construction: another thread's allocation simply is not this thread's
//! count. `Cell` with a `const` initialiser so the TLS access inside `alloc` cannot
//! itself allocate and recurse, and `try_with` so an allocation during TLS teardown
//! is ignored rather than panicking inside the allocator.
//!
//! # Why COUNT and not only BYTES
//!
//! Bytes alone cannot see a per-item temporary: a throwaway `Vec` that is built and
//! immediately memcpy'd into a shared buffer allocates roughly the same bytes as the
//! shared buffer's own growth, but it does it once **per item** instead of
//! `O(log n)` times in total. The allocation COUNT is the observable that separates
//! the two shapes, so both are reported.

#![allow(dead_code)]

use std::alloc::{GlobalAlloc, Layout, System};

thread_local! {
    static BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
    static ALLOCS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
    static ARMED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}

/// Add one allocation of `n` bytes to this thread's tally, but only while armed.
#[inline]
fn tally(n: usize) {
    let armed = ARMED.try_with(std::cell::Cell::get).unwrap_or(false);
    if armed {
        let _ = BYTES.try_with(|b| b.set(b.get() + n));
        let _ = ALLOCS.try_with(|c| c.set(c.get() + 1));
    }
}

/// The `System` allocator with a thread-local tally in front of it.
pub struct Counting;

unsafe impl GlobalAlloc for Counting {
    unsafe fn alloc(&self, l: Layout) -> *mut u8 {
        tally(l.size());
        unsafe { System.alloc(l) }
    }
    unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
        unsafe { System.dealloc(p, l) }
    }
    unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 {
        // Only the GROWTH is new bytes — a `Vec` doubling its way to `S` bytes has
        // then allocated `S`, not `2S`. The realloc still counts as one allocation,
        // which is what makes a doubling buffer read as `O(log n)` events.
        tally(n.saturating_sub(l.size()));
        unsafe { System.realloc(p, l, n) }
    }
}

/// What one call cost this thread's heap: `(value, allocations, bytes)`.
///
/// Build the fixture BEFORE calling this — only `f` is inside the armed window.
pub fn measure<T>(f: impl FnOnce() -> T) -> (T, usize, usize) {
    BYTES.with(|b| b.set(0));
    ALLOCS.with(|c| c.set(0));
    ARMED.with(|a| a.set(true));
    let r = f();
    ARMED.with(|a| a.set(false));
    (r, ALLOCS.with(std::cell::Cell::get), BYTES.with(std::cell::Cell::get))
}