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"
);
}
}