Luigit
repositories / smith

smith

There are many coding harnesses - but this one is fast

owned by admin

smith-alloc/src/lib.rs

Raw
//! Scope-tagged counting allocator (`SMH-SPEC-SPEC0001`, Memory accounting).
//!
//! [`Counting`] wraps [`System`](std::alloc::System) and attributes every
//! allocation, including third-party ones, to the innermost [`Scope`]
//! entered on the allocating thread. Each block carries its scope tag in a
//! header, so a free is charged to the scope that allocated it and live
//! bytes stay exact per scope.
//! Scopes do not propagate to spawned threads; those start at
//! [`Scope::Process`].
//!
//! The only unsafe code is the [`GlobalAlloc`](std::alloc::GlobalAlloc)
//! implementation and its header arithmetic, kept in one private module;
//! the public surface is safe.

#![deny(unsafe_code)]

use std::cell::Cell;
use std::marker::PhantomData;
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};

/// Logical lifetime an allocation is charged to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum Scope {
    /// Everything outside an explicit scope, including pre-main.
    Process,
    /// One session.
    Session,
    /// One agent turn.
    Turn,
    /// One kernel call.
    KernelCall,
    /// One rendered frame.
    Frame,
    /// One I/O request.
    IoRequest,
}

const SCOPES: [Scope; 6] = [
    Scope::Process,
    Scope::Session,
    Scope::Turn,
    Scope::KernelCall,
    Scope::Frame,
    Scope::IoRequest,
];

/// Counters recorded for one scope since process start.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Stats {
    /// Allocations, reallocations included.
    pub allocations: u64,
    /// Bytes requested by those allocations.
    pub bytes: u64,
    /// Bytes allocated in this scope and not yet freed.
    pub live: u64,
    /// Highest `live` observed.
    pub peak: u64,
}

struct Counters {
    allocations: AtomicU64,
    bytes: AtomicU64,
    live: AtomicU64,
    peak: AtomicU64,
}

static COUNTERS: [Counters; SCOPES.len()] = [const {
    Counters {
        allocations: AtomicU64::new(0),
        bytes: AtomicU64::new(0),
        live: AtomicU64::new(0),
        peak: AtomicU64::new(0),
    }
}; SCOPES.len()];

const _: () = assert!(
    SCOPES.len() <= u8::MAX as usize,
    "scope tags must fit in one byte"
);
const DEPTH: usize = 16;

#[derive(Clone, Copy)]
struct Stack {
    len: usize,
    items: [Scope; DEPTH],
}

thread_local! {
    // Const-initialized and drop-free: access never allocates or registers a destructor.
    static STACK: Cell<Stack> = const { Cell::new(Stack { len: 0, items: [Scope::Process; DEPTH] }) };
    // Kept separately so allocation hot paths do not copy `Stack` from its `Cell`.
    static CURRENT_SCOPE: Cell<Scope> = const { Cell::new(Scope::Process) };
    // Zero means inactive; scopes use their discriminant plus one.
    static ACTIVE_MEASUREMENT: Cell<u8> = const { Cell::new(0) };
    static MEASUREMENT: Cell<Measurement> = const { Cell::new(Measurement::EMPTY) };
}

#[derive(Clone, Copy)]
struct Measurement {
    live: u64,
    peak: u64,
}

impl Measurement {
    const EMPTY: Self = Self { live: 0, peak: 0 };
}

const _: () = assert!(
    std::mem::size_of::<Measurement>() == 16,
    "measurement state must remain two u64 words"
);

#[inline]
fn measures(scope: u8) -> bool {
    ACTIVE_MEASUREMENT
        .try_with(|active| active.get() == scope + 1)
        .unwrap_or(false)
}

#[inline]
fn measure_allocation(size: u64) {
    // TLS teardown must not make allocator accounting panic or abort allocation.
    let _measurement_update: std::result::Result<(), std::thread::AccessError> = MEASUREMENT
        .try_with(|cell| {
            let mut measurement = cell.get();
            measurement.live += size;
            measurement.peak = measurement.peak.max(measurement.live);
            cell.set(measurement);
        });
}

#[inline]
fn measure_free(size: u64) {
    // TLS teardown must not make allocator accounting panic or abort deallocation.
    let _measurement_update: std::result::Result<(), std::thread::AccessError> = MEASUREMENT
        .try_with(|cell| {
            let mut measurement = cell.get();
            measurement.live = measurement.live.saturating_sub(size);
            cell.set(measurement);
        });
}

#[inline]
fn current() -> Scope {
    CURRENT_SCOPE.try_with(Cell::get).unwrap_or(Scope::Process)
}

/// Restores the enclosing scope when dropped; bound to its thread.
#[must_use = "the scope ends when the guard drops"]
pub struct Guard {
    previous: usize,
    _thread_bound: PhantomData<*const ()>,
}

impl Drop for Guard {
    fn drop(&mut self) {
        let current = STACK
            .try_with(|cell| {
                let mut stack = cell.get();
                // `min` tolerates non-LIFO drops, such as a `Vec` of guards.
                stack.len = stack.len.min(self.previous);
                let current = stack
                    .items
                    .get(stack.len.wrapping_sub(1))
                    .copied()
                    .unwrap_or(Scope::Process);
                cell.set(stack);
                current
            })
            .unwrap_or(Scope::Process);
        // Scope restoration is best-effort during thread-local destruction.
        let _scope_update: std::result::Result<(), std::thread::AccessError> =
            CURRENT_SCOPE.try_with(|cell| cell.set(current));
    }
}

/// Charge this thread's allocations to `scope` until the guard drops.
///
/// Nesting deeper than 16 keeps charging the outer scope (soft limit).
pub fn enter(scope: Scope) -> Guard {
    let (previous, current) = STACK
        .try_with(|cell| {
            let mut stack = cell.get();
            let previous = stack.len;
            if let Some(slot) = stack.items.get_mut(previous) {
                *slot = scope;
                stack.len += 1;
                cell.set(stack);
            }
            (
                previous,
                stack
                    .items
                    .get(stack.len.wrapping_sub(1))
                    .copied()
                    .unwrap_or(Scope::Process),
            )
        })
        .unwrap_or((0, Scope::Process));
    // Scope setup is best-effort if thread-local state is already being torn down.
    let _scope_update: std::result::Result<(), std::thread::AccessError> =
        CURRENT_SCOPE.try_with(|cell| cell.set(current));
    Guard {
        previous,
        _thread_bound: PhantomData,
    }
}

/// Counters recorded for `scope`.
pub fn snapshot(scope: Scope) -> Stats {
    let counters = &COUNTERS[scope as usize];
    Stats {
        allocations: counters.allocations.load(Relaxed),
        bytes: counters.bytes.load(Relaxed),
        live: counters.live.load(Relaxed),
        peak: counters.peak.load(Relaxed),
    }
}

struct MeasurementGuard {
    scope: Option<Guard>,
    active: u8,
    measurement: Measurement,
}

impl Drop for MeasurementGuard {
    fn drop(&mut self) {
        drop(self.scope.take());
        ACTIVE_MEASUREMENT.set(self.active);
        MEASUREMENT.set(self.measurement);
    }
}

/// Run `work` charged to `scope` and report what it cost.
///
/// `allocations`, `bytes`, and `peak` describe this invocation only.
/// `live` is the scope's process-wide value after it returns.
/// Measurements on the same scope must not overlap on one thread.
pub fn measure<T>(scope: Scope, work: impl FnOnce() -> T) -> (T, Stats) {
    let before = snapshot(scope);
    let guard = MeasurementGuard {
        scope: Some(enter(scope)),
        active: ACTIVE_MEASUREMENT.replace(scope as u8 + 1),
        measurement: MEASUREMENT.replace(Measurement::EMPTY),
    };
    let out = work();
    let measurement = MEASUREMENT.get();
    drop(guard);
    let after = snapshot(scope);
    (
        out,
        Stats {
            allocations: after.allocations - before.allocations,
            bytes: after.bytes - before.bytes,
            live: after.live,
            peak: measurement.peak,
        },
    )
}

/// Every scope with its counters.
pub fn snapshot_all() -> [(Scope, Stats); SCOPES.len()] {
    SCOPES.map(|scope| (scope, snapshot(scope)))
}

fn record_alloc(scope: Scope, size: usize) {
    let counters = &COUNTERS[scope as usize];
    counters.allocations.fetch_add(1, Relaxed);
    counters.bytes.fetch_add(size as u64, Relaxed);
    let now = counters.live.fetch_add(size as u64, Relaxed) + size as u64;
    counters.peak.fetch_max(now, Relaxed);
    if measures(scope as u8) {
        measure_allocation(size as u64);
    }
}

fn record_free(tag: u8, size: usize) {
    // A tag outside the scope range means a foreign block; nothing to charge.
    if let Some(counters) = COUNTERS.get(usize::from(tag)) {
        counters.live.fetch_sub(size as u64, Relaxed);
        if measures(tag) {
            measure_free(size as u64);
        }
    }
}

/// Counting wrapper around [`System`](std::alloc::System); install with
/// `#[global_allocator]`.
pub struct Counting;

/// Block layout: `System` holds `pad = align` header bytes in front of
/// each user block, the last header byte being the scope tag.
#[expect(
    unsafe_code,
    reason = "GlobalAlloc has no safe interface; implementing it is the only way to attribute every allocation, third-party ones included, to a scope"
)]
mod raw {
    use super::{Counting, current, record_alloc, record_free};
    use std::alloc::{GlobalAlloc, Layout, System};

    /// The `System` layout for `layout` and its header size; `pad >= 1`
    /// because every alignment is at least one.
    fn padded(layout: Layout) -> Option<(Layout, usize)> {
        let pad = layout.align();
        let size = layout.size().checked_add(pad)?;
        Layout::from_size_align(size, pad)
            .ok()
            .map(|outer| (outer, pad))
    }

    /// A fresh tagged block for `layout`, or null.
    fn tagged(layout: Layout, zeroed: bool) -> *mut u8 {
        let Some((outer, pad)) = padded(layout) else {
            return std::ptr::null_mut();
        };
        let base = if zeroed {
            // SAFETY: `outer` is `layout` grown by `pad >= 1`, so its size is non-zero.
            unsafe { System.alloc_zeroed(outer) }
        } else {
            // SAFETY: `outer` is `layout` grown by `pad >= 1`, so its size is non-zero.
            unsafe { System.alloc(outer) }
        };
        // SAFETY: `base` is null or a fresh `System` block of `outer`,
        // which spans `pad + layout.size()` bytes.
        unsafe { stamp(base, pad, layout.size()) }
    }

    /// Tag a fresh `System` block with the current scope and charge `size`
    /// to it; null passes through.
    ///
    /// # Safety
    /// `base` is null or a live, writable block spanning `pad + size`
    /// bytes, aligned to `pad`, with `pad >= 1`.
    unsafe fn stamp(base: *mut u8, pad: usize, size: usize) -> *mut u8 {
        if base.is_null() {
            return base;
        }
        let scope = current();
        // `pad` is the alignment, so the user pointer stays aligned.
        let user = base.wrapping_add(pad);
        // SAFETY: `1 <= pad`, so the tag byte `user - 1` is the last header
        // byte, inside the block the caller hands over.
        unsafe { user.wrapping_sub(1).write(scope as u8) };
        record_alloc(scope, size);
        user
    }

    // SAFETY: every block is a `System` block of `padded(layout)`; the user
    // pointer sits `pad` bytes in, so `System` receives back exactly what it
    // gave, and the user block keeps the size and alignment of `layout`.
    // Header and base are reached from the caller's pointer, keeping its
    // provenance: a `Box` protects the pointer it frees, so any other
    // pointer to the block would be a foreign access (Tree Borrows).
    unsafe impl GlobalAlloc for Counting {
        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
            tagged(layout, false)
        }

        unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
            tagged(layout, true)
        }

        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
            let Some((outer, pad)) = padded(layout) else {
                return;
            };
            // SAFETY: the caller passes a block this allocator made with
            // `layout`, so its tag byte lies one byte before `ptr`.
            let tag = unsafe { ptr.wrapping_sub(1).read() };
            record_free(tag, layout.size());
            // SAFETY: the `System` base lies `pad` bytes before `ptr` and was
            // allocated with `outer = padded(layout)`.
            unsafe { System.dealloc(ptr.wrapping_sub(pad), outer) };
        }

        unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
            // The grown outer layout must itself be valid, as `System.realloc` requires.
            let new_layout = Layout::from_size_align(new_size, layout.align()).ok();
            let (Some((outer, pad)), Some((new_outer, _))) =
                (padded(layout), new_layout.and_then(padded))
            else {
                return std::ptr::null_mut();
            };
            // SAFETY: as in `dealloc`, the tag byte lies one byte before `ptr`.
            let tag = unsafe { ptr.wrapping_sub(1).read() };
            // SAFETY: the base `pad` bytes before `ptr` is a live `System`
            // block of `outer`; `new_outer` is a valid layout of the same
            // alignment and non-zero size.
            let base = unsafe { System.realloc(ptr.wrapping_sub(pad), outer, new_outer.size()) };
            // On success `System` moved the header with the data and the old
            // block is gone, so its charge is released first.
            if !base.is_null() {
                record_free(tag, layout.size());
            }
            // SAFETY: `base` is null or a `System` block of `new_outer`,
            // which spans `pad + new_size` bytes aligned to `pad`.
            unsafe { stamp(base, pad, new_size) }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{Counting, Scope, enter, measure, snapshot};

    #[global_allocator]
    static ALLOC: Counting = Counting;

    // Each test owns a distinct scope: counters are process-wide and libtest runs tests in parallel.

    #[cfg(target_pointer_width = "64")]
    #[test]
    fn active_measurement_state_fits_two_words() {
        assert_eq!(std::mem::size_of::<super::Measurement>(), 16);
    }

    #[test]
    fn foreign_free_tags_are_ignored_during_and_outside_measurement() {
        let active = super::ACTIVE_MEASUREMENT.replace(0);
        let measurement = super::MEASUREMENT.replace(super::Measurement { live: 7, peak: 11 });
        let before = snapshot(Scope::Session);
        for tag in 6..=u8::MAX {
            super::record_free(tag, 13);
        }
        assert_eq!(snapshot(Scope::Session), before);
        assert_eq!(
            (super::MEASUREMENT.get().live, super::MEASUREMENT.get().peak),
            (7, 11)
        );

        super::ACTIVE_MEASUREMENT.set(Scope::IoRequest as u8 + 1);
        for tag in 6..=u8::MAX {
            super::record_free(tag, 13);
        }
        assert_eq!(snapshot(Scope::Session), before);
        assert_eq!(
            (super::MEASUREMENT.get().live, super::MEASUREMENT.get().peak),
            (7, 11)
        );

        super::ACTIVE_MEASUREMENT.set(active);
        super::MEASUREMENT.set(measurement);
    }

    #[test]
    fn free_outside_scope_is_charged_to_allocating_scope() {
        let guard = enter(Scope::Frame);
        let block = std::hint::black_box(vec![0u8; 1000]);
        drop(guard);
        let inside = snapshot(Scope::Frame);
        assert_eq!(
            (inside.allocations, inside.bytes, inside.live),
            (1, 1000, 1000)
        );
        drop(block);
        let after = snapshot(Scope::Frame);
        assert_eq!((after.live, after.peak), (0, 1000));
    }

    #[test]
    fn measure_nested_scope_restores_outer_peak() {
        let (_, outer) = measure(Scope::Turn, || {
            let first = std::hint::black_box(vec![0u8; 4]);
            let (_, inner) = measure(Scope::Process, || std::hint::black_box(vec![0u8; 8]));
            assert_eq!(inner.peak, 8);
            drop(first);
            std::hint::black_box(vec![0u8; 16])
        });
        assert_eq!((outer.allocations, outer.bytes, outer.peak), (2, 20, 16));
    }

    #[test]
    fn panicking_measure_restores_enclosing_measurement() {
        let (_, outer) = measure(Scope::Turn, || {
            let result = std::panic::catch_unwind(|| {
                let _ = measure(Scope::Process, || {
                    let _block = std::hint::black_box(vec![0u8; 8]);
                    std::panic::resume_unwind(Box::new(()));
                });
            });
            assert!(result.is_err());
            std::hint::black_box(vec![0u8; 16])
        });
        assert_eq!(outer.peak, 16);
    }

    #[test]
    fn nested_scope_restores_outer_and_realloc_retags() {
        let outer = enter(Scope::KernelCall);
        let mut grown: Vec<u8> = Vec::with_capacity(8);
        {
            let _inner = enter(Scope::IoRequest);
            grown.reserve_exact(64);
        }
        let tail = std::hint::black_box(vec![1u8; 16]);
        drop(outer);
        let kernel = snapshot(Scope::KernelCall);
        let io = snapshot(Scope::IoRequest);
        assert_eq!((kernel.allocations, kernel.bytes, kernel.live), (2, 24, 16));
        assert_eq!((io.allocations, io.bytes, io.live), (1, 64, 64));
        drop((grown, tail));
        assert_eq!(snapshot(Scope::IoRequest).live, 0);
    }

    #[test]
    fn zeroed_blocks_are_zero_after_a_dirty_free() {
        drop(std::hint::black_box(vec![0xAAu8; 64]));
        let zeroed = std::hint::black_box(vec![0u8; 64]);
        assert!(zeroed.iter().all(|byte| *byte == 0));
    }

    #[test]
    fn overaligned_blocks_stay_aligned() {
        #[repr(align(4096))]
        struct Page([u8; 4096]);
        let page = Box::new(Page([0; 4096]));
        assert_eq!(std::ptr::from_ref(&*page).addr() % 4096, 0);
        assert_eq!(page.0[4095], 0);
    }

    #[test]
    #[expect(
        unsafe_code,
        reason = "drives GlobalAlloc directly: no safe caller can request a size past the header capacity"
    )]
    fn realloc_past_header_capacity_fails_and_keeps_block() {
        use std::alloc::{GlobalAlloc, Layout};
        let layout = Layout::new::<u64>();
        // Valid for the caller, but no room left for the header in front.
        let new_size = isize::MAX.cast_unsigned() - 7;
        // SAFETY: `layout` has non-zero size.
        let block = unsafe { Counting.alloc(layout) };
        assert!(!block.is_null());
        // SAFETY: `block` is live with `layout`; `new_size` is non-zero and,
        // rounded to the alignment, fits `isize`.
        assert!(unsafe { Counting.realloc(block, layout, new_size) }.is_null());
        // SAFETY: the failed `realloc` kept `block`, still live with `layout`.
        unsafe { Counting.dealloc(block, layout) };
    }

    #[test]
    fn spawned_threads_start_at_process_scope() {
        let _turn = enter(Scope::Turn);
        let seen = std::thread::spawn(super::current)
            .join()
            .map_err(|_| "join");
        assert_eq!(seen, Ok(Scope::Process));
    }

    #[test]
    fn overflowing_depth_keeps_outer_scope() {
        let _session = enter(Scope::Session);
        let guards: Vec<_> = (0..20).map(|_| enter(Scope::Process)).collect();
        assert_eq!(super::current(), Scope::Process);
        drop(guards);
        assert_eq!(super::current(), Scope::Session);
    }
}