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

nordisk/facett

facett-osm/src/alloc_probe.rs

//! **A counting allocator, test builds only** — the instrument that turns "this
//! parser borrows instead of copying" from a claim into an assertion.
//!
//! Nothing else can see the difference. The tag list that comes out of the borrowing
//! parser and the tag list that comes out of the owned one compare EQUAL string for
//! string; only the heap traffic behind them differs, and that is precisely the
//! defect stinky `korpdep-15` / `prof-2` measured (`parse_tags` at 55.9 % of every
//! allocation in a real z12 tile decode, 44.0 % of the bytes).
//!
//! ## Shape
//!
//! A `#[global_allocator]` wrapper that counts allocations into a **thread-local**,
//! and only while a measurement window is open. Thread-local because the test harness
//! runs tests in parallel and a process-wide counter would have every test measuring
//! every other one. Windowed because the harness itself allocates constantly between
//! tests.
//!
//! Both cells are `const`-initialised, so accessing them registers no TLS destructor
//! and therefore cannot itself allocate — a counting allocator that allocates on its
//! own counting path is an infinite recursion, not a measurement.
//!
//! ## The counter's own guard
//!
//! `the_probe_can_read_a_nonzero_number` allocates a known buffer inside a window and
//! asserts the reading moves, and asserts an empty window reads exactly 0. A counter
//! stuck at zero makes every budget below it green for ever — which is the failure
//! mode this whole file exists to avoid, so it does not get to be assumed.

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

thread_local! {
    /// Allocations counted since the current window opened.
    static COUNT: Cell<u64> = const { Cell::new(0) };
    /// **Bytes** requested since the current window opened — see [`measure_heap`].
    static BYTES: Cell<u64> = const { Cell::new(0) };
    /// Is a measurement window open on THIS thread?
    static ARMED: Cell<bool> = const { Cell::new(false) };
}

/// `System`, plus a counter that only ticks inside [`measure`].
pub struct Counting;

unsafe impl GlobalAlloc for Counting {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        bump(layout.size());
        unsafe { System.alloc(layout) }
    }
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        unsafe { System.dealloc(ptr, layout) }
    }
    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
        bump(layout.size());
        unsafe { System.alloc_zeroed(layout) }
    }
    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
        // A realloc is a fresh allocation as far as the heap is concerned — a growth
        // loop that reallocated a thousand times would otherwise read as free.
        bump(new_size);
        unsafe { System.realloc(ptr, layout, new_size) }
    }
}

/// Tick the counters if a window is open. `try_with` because a thread tearing down
/// still allocates, and a panic in the allocator is not recoverable.
fn bump(size: usize) {
    let _ = ARMED.try_with(|a| {
        if a.get() {
            let _ = COUNT.try_with(|c| c.set(c.get() + 1));
            let _ = BYTES.try_with(|b| b.set(b.get() + size as u64));
        }
    });
}

/// Run `f` with the counter armed on this thread; return its value and how many
/// allocations it made. Windows do not nest — a nested call would double-count — so
/// this asserts it is not already armed.
pub fn measure<T>(f: impl FnOnce() -> T) -> (T, u64) {
    let (out, n, _) = measure_heap(f);
    (out, n)
}

/// [`measure`] with the **byte volume** as well: `(value, allocations, bytes)`.
///
/// Why bytes and not just a count: a **copy of a vertex run is ONE allocation** no
/// matter how long the run is, so an allocation counter cannot see the difference
/// between moving a way's coordinates into it and copying them (stinky `korpdep-13`,
/// which priced its own defect in bytes for exactly this reason: ≈ 1.97 GB over a
/// Sweden read, and not one extra allocation *per vertex*). The byte reading makes
/// the copy visible as a slope: hold the row count fixed, raise the vertices per
/// row, and read off bytes per ADDED vertex — 16 of them are a `(f64, f64)` that was
/// copied rather than moved.
///
/// `bytes` is what the program **asked the allocator for**, summed over the window:
/// `layout.size()` per `alloc`/`alloc_zeroed` and `new_size` per `realloc`. It is
/// therefore traffic, not peak residency — a `Vec` that grows 1 → 2 → 4 contributes
/// all three sizes, which is the honest reading for "how much did this path write
/// and throw away".
pub fn measure_heap<T>(f: impl FnOnce() -> T) -> (T, u64, u64) {
    ARMED.with(|a| {
        assert!(!a.get(), "alloc_probe windows do not nest");
        a.set(true);
    });
    COUNT.with(|c| c.set(0));
    BYTES.with(|b| b.set(0));
    let out = f();
    let n = COUNT.with(|c| c.get());
    let bytes = BYTES.with(|b| b.get());
    ARMED.with(|a| a.set(false));
    (out, n, bytes)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// **The probe can read a nonzero number, and reads zero when nothing happens.**
    ///
    /// Both directions. A counter that always says 0 makes every allocation budget in
    /// this crate green for ever; a counter that always says "some" would pass a
    /// nonzero assertion while proving nothing. So: a window that allocates a known
    /// buffer must read at least 1, and an empty window must read exactly 0.
    #[test]
    fn the_probe_can_read_a_nonzero_number() {
        let (v, n) = measure(|| Vec::<u8>::with_capacity(4096));
        assert_eq!(v.capacity(), 4096);
        assert!(n >= 1, "a 4 KiB Vec allocation read as {n} allocations — the probe is blind");

        let (_, none) = measure(|| std::hint::black_box(1u64 + 1));
        assert_eq!(none, 0, "an arithmetic-only window must read exactly 0, read {none}");

        // …and it counts per allocation, not per window: ten buffers must read more
        // than one. A probe stuck on a constant fails here.
        let (_, ten) = measure(|| {
            let mut keep = Vec::with_capacity(0);
            for _ in 0..10 {
                keep.push(Vec::<u8>::with_capacity(4096));
            }
            keep
        });
        assert!(ten >= 10, "ten 4 KiB buffers read as {ten} allocations");
    }

    /// **The BYTE reading moves with the size, and reads zero on an empty window.**
    ///
    /// The allocation counter cannot tell a 16-byte buffer from a 16 MiB one, which
    /// is precisely why [`measure_heap`] exists — so its own arm has to be proved in
    /// both directions, and proved to SCALE. A byte counter that returned the
    /// allocation count, or a constant, or the same number for both sizes, would make
    /// every vertex-copy budget in this crate green for ever.
    #[test]
    fn the_probe_reads_bytes_and_not_just_allocations() {
        let (_, _, none) = measure_heap(|| std::hint::black_box(1u64 + 1));
        assert_eq!(none, 0, "an arithmetic-only window must read exactly 0 bytes, read {none}");

        let (_, small_n, small_b) = measure_heap(|| Vec::<u8>::with_capacity(4_096));
        let (_, big_n, big_b) = measure_heap(|| Vec::<u8>::with_capacity(1_048_576));
        assert_eq!(small_n, big_n, "one Vec each, so the ALLOCATION counts must agree ({small_n} vs {big_n})");
        assert!(small_b >= 4_096, "a 4 KiB buffer read {small_b} bytes");
        assert!(big_b >= 1_048_576, "a 1 MiB buffer read {big_b} bytes");
        // …and the counts being equal while the bytes differ 256x is the whole point.
        assert!(
            big_b >= 200 * small_b,
            "4 KiB read {small_b} B and 1 MiB read {big_b} B — the byte counter is not \
             tracking the size, so it cannot see a copied vertex run"
        );
    }
}