mnemosyne_arena/scratch/pool/mod.rs
1//! Scratch buffer pool: a depth-tracked set of reusable aligned scratch
2//! buffers for temporal allocations.
3//!
4//! The pool is decomposed by responsibility (each submodule is private; its
5//! methods are re-exported through the `impl` blocks below):
6//! - `borrow` — the PROVISION-generic `borrow_slot`, `with_scratch`,
7//! `with_scratch_bounded`, and `with_scratch_uninit` borrow entry points.
8//! - `manage` — provision-aware `release`, `reset`, `prewarm`, `preload`,
9//! and `shrink_all_slots` lifecycle methods.
10//! - `query` — read-only `borrow_depth`, `capacity`, `slot_capacity`,
11//! `is_available`, `total_capacity_bytes` accessors.
12//!
13//! `mod.rs` keeps only the struct shape, `MAX_POOL_SLOTS`, and the
14//! `new`/`with_slot_capacity`/`Default` constructors.
15
16mod borrow;
17mod manage;
18mod query;
19
20use super::aligned_vec::AlignedVec;
21use super::element::ScratchElement;
22use core::cell::{Cell, UnsafeCell};
23
24/// Maximum concurrent borrows (recursive/nested calls) the pool supports.
25pub const MAX_POOL_SLOTS: usize = 4;
26
27/// A pool of reusable, aligned scratch buffers for a specific element type.
28///
29/// `Send` but **not** `Sync` — designed for `thread_local!` storage.
30///
31/// # Usage
32///
33/// ```rust,ignore
34/// use mnemosyne_arena::scratch::ScratchPool;
35///
36/// thread_local! {
37/// static POOL: ScratchPool<f64> = ScratchPool::new();
38/// }
39///
40/// POOL.with(|pool| {
41/// pool.with_scratch(1024, |scratch| {
42/// // scratch: &mut [f64] of exactly 1024 elements, 64-byte aligned
43/// });
44/// });
45/// ```
46pub struct ScratchPool<T: ScratchElement> {
47 pub(super) slots: [UnsafeCell<AlignedVec<T>>; MAX_POOL_SLOTS],
48 pub(super) borrow_depth: Cell<u8>,
49 /// Per-depth high-water request, recorded by
50 /// [`with_scratch_bounded`](Self::with_scratch_bounded) and honored by
51 /// [`release`](Self::release). Provisioned slots keep capacity for their
52 /// working set across a release; unprovisioned slots reclaim entirely.
53 pub(super) provisions: [Cell<usize>; MAX_POOL_SLOTS],
54 /// Slot 0's capacity, republished by the borrow that grows it.
55 ///
56 /// [`ScratchPool::capacity`] is reachable from inside a live
57 /// [`ScratchPool::with_scratch`] borrow through entirely safe code (both
58 /// take `&self`, and the pool's documented home is a `thread_local!`), so
59 /// the accessor must not derive a reference into a slot that borrow already
60 /// holds exclusively. Mirroring the figure outside the `UnsafeCell` removes
61 /// the aliasing by construction rather than forbidding the call.
62 ///
63 /// Slot 0's capacity changes only where this is written: construction, and
64 /// the grow branch of a depth-0 `with_scratch` (slot index equals borrow
65 /// depth, so only depth 0 touches slot 0). A `debug_assert!` in
66 /// `with_scratch` fails the tests if a future mutation path escapes that
67 /// set.
68 pub(super) slot_capacities: [Cell<usize>; MAX_POOL_SLOTS],
69}
70
71// SAFETY: a `ScratchPool` uniquely owns its slot buffers (each `AlignedVec` owns
72// its heap storage with no aliasing), so moving the whole pool to another thread
73// is sound. It is deliberately *not* `Sync`: the `UnsafeCell` slots and the
74// `Cell` borrow-depth and capacity fields are guarded only by single-threaded
75// `borrow_depth` tracking, which assumes one thread at a time (`thread_local!`
76// storage), so it must never be shared by reference across threads.
77unsafe impl<T: ScratchElement> Send for ScratchPool<T> {}
78
79impl<T: ScratchElement> Default for ScratchPool<T> {
80 #[inline]
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl<T: ScratchElement> ScratchPool<T> {
87 /// Creates a new empty scratch pool (zero allocation at construction).
88 #[inline]
89 pub const fn new() -> Self {
90 Self {
91 slots: [
92 UnsafeCell::new(AlignedVec::dangling()),
93 UnsafeCell::new(AlignedVec::dangling()),
94 UnsafeCell::new(AlignedVec::dangling()),
95 UnsafeCell::new(AlignedVec::dangling()),
96 ],
97 borrow_depth: Cell::new(0),
98 provisions: [const { Cell::new(0) }; MAX_POOL_SLOTS],
99 slot_capacities: [const { Cell::new(0) }; MAX_POOL_SLOTS],
100 }
101 }
102
103 /// Creates a new scratch pool with pre-allocated capacity per slot.
104 #[inline]
105 pub fn with_slot_capacity(capacity: usize) -> Self {
106 let mk = || {
107 if capacity == 0 {
108 AlignedVec::dangling()
109 } else {
110 AlignedVec::with_capacity(capacity)
111 }
112 };
113 Self {
114 slots: [
115 UnsafeCell::new(mk()),
116 UnsafeCell::new(mk()),
117 UnsafeCell::new(mk()),
118 UnsafeCell::new(mk()),
119 ],
120 borrow_depth: Cell::new(0),
121 provisions: [const { Cell::new(0) }; MAX_POOL_SLOTS],
122 // `mk()` gives every slot exactly `capacity` (a zero request yields
123 // the zero-capacity dangling sentinel), so every slot's mirror must
124 // begin with the same warm capacity. This keeps the public
125 // `slot_capacity`/`total_capacity_bytes` figures in sync with the
126 // actual backing allocations across all prewarmed slots.
127 slot_capacities: {
128 let mut mirrors = [const { Cell::new(0) }; MAX_POOL_SLOTS];
129 for cell in &mut mirrors {
130 cell.set(capacity);
131 }
132 mirrors
133 },
134 }
135 }
136}