pub struct ScratchPool<T>where
T: ScratchElement,{ /* private fields */ }Expand description
A pool of reusable, aligned scratch buffers for a specific element type.
Send but not Sync — designed for thread_local! storage.
§Usage
use mnemosyne_arena::scratch::ScratchPool;
thread_local! {
static POOL: ScratchPool<f64> = ScratchPool::new();
}
POOL.with(|pool| {
pool.with_scratch(1024, |scratch| {
// scratch: &mut [f64] of exactly 1024 elements, 64-byte aligned
});
});Implementations§
Source§impl<T> ScratchPool<T>where
T: ScratchElement,
impl<T> ScratchPool<T>where
T: ScratchElement,
Sourcepub fn with_scratch<R>(&self, n: usize, f: impl FnOnce(&mut [T]) -> R) -> R
pub fn with_scratch<R>(&self, n: usize, f: impl FnOnce(&mut [T]) -> R) -> R
Provides a mutable aligned scratch slice of exactly n elements
to the closure. Borrow depth is released when the closure returns.
If a pool slot is available, the closure receives a direct &mut [T]
into the pooled buffer (zero-copy). If all slots are exhausted (nested
recursive calls), a temporary buffer is allocated instead.
Examples found in repository?
22fn main() {
23 // Construct a pool locally and use it directly (not thread_local! here
24 // to avoid the clippy::missing_const_for_thread_local false positive on
25 // 1.97.0 — see ATLAS-MNEMOSYNE-CI-1). In production callers store the
26 // pool in thread_local! storage so it persists between calls.
27 let f64_pool: ScratchPool<f64> = ScratchPool::new();
28 let f32_pool: ScratchPool<f32> = ScratchPool::new();
29
30 // Simple single-level scratch use: compute a dot product via a temp buffer.
31 f64_pool.with_scratch(1024, |scratch| {
32 for (i, slot) in scratch.iter_mut().enumerate() {
33 *slot = i as f64;
34 }
35 let partial: f64 = scratch.iter().sum();
36 println!(
37 "sum of 0..1024 via scratch: {} (expected {})",
38 partial,
39 (0..1024usize).sum::<usize>() as f64,
40 );
41 assert_eq!(partial, (0..1024usize).sum::<usize>() as f64);
42 });
43
44 // Nested borrows: outer scratch holds the signal; inner holds a window.
45 f64_pool.with_scratch(256, |signal| {
46 for (i, s) in signal.iter_mut().enumerate() {
47 *s = (i as f64).sin();
48 }
49 f64_pool.with_scratch(16, |window| {
50 window.copy_from_slice(&signal[..16]);
51 let dp = dot_product(window, &signal[..16]);
52 println!("windowed dot-product: {dp:.6}");
53 assert!(dp.is_finite());
54 });
55 });
56
57 // F32 pool: same API, different element type.
58 f32_pool.with_scratch(64, |buf| {
59 buf.fill(1.0_f32);
60 let total: f32 = buf.iter().sum();
61 println!("f32 scratch sum: {total}");
62 assert_eq!(total, 64.0_f32);
63 });
64
65 // Banked scratch keeps multiple related roles in one const-generic group,
66 // which matches transform pipelines that need independent temporary views
67 // without falling back to the system allocator.
68 let bank: ScratchBank<f64, 2> = ScratchBank::new();
69 bank.with_scratch::<1, _>(32, |scratch| {
70 scratch.fill(3.5);
71 assert!(scratch.iter().all(|v| *v == 3.5));
72 });
73
74 // Bounded provisioning retains the working set while making geometric
75 // growth headroom reclaimable at a consumer-selected quiescent point.
76 let bounded_pool: ScratchPool<u32> = ScratchPool::new();
77 bounded_pool.with_scratch_bounded(1024, |_| {});
78 bounded_pool.with_scratch_bounded(1025, |_| {});
79 assert_eq!(bounded_pool.capacity(), 2048);
80 let retained = bounded_pool.release();
81 println!(
82 "bounded release: capacity={} -> retained={}",
83 2048, retained[0]
84 );
85 assert_eq!(retained[0], 1025);
86 bounded_pool.reset();
87 assert_eq!(bounded_pool.release()[0], 0);
88
89 println!("MAX_POOL_SLOTS = {MAX_POOL_SLOTS} (max concurrent nested borrows)");
90 println!("all scratch-pool assertions passed");
91}Sourcepub fn with_scratch_bounded<R>(
&self,
n: usize,
f: impl FnOnce(&mut [T]) -> R,
) -> R
pub fn with_scratch_bounded<R>( &self, n: usize, f: impl FnOnce(&mut [T]) -> R, ) -> R
Like with_scratch, but records the request for
release.
Each depth’s largest-ever request becomes that slot’s provision; a
later release may reclaim everything a slot holds above it. The
two forms share the slot storage, so a pool can be driven through
either (or both) — only the provisions differ.
§Panics
Panics if f panics and leaves self.borrow_depth at u8::MAX, where
the depth increment would wrap; with_scratch has the same bound via
slot exhaustion, so this is not a new failure mode.
Examples found in repository?
22fn main() {
23 // Construct a pool locally and use it directly (not thread_local! here
24 // to avoid the clippy::missing_const_for_thread_local false positive on
25 // 1.97.0 — see ATLAS-MNEMOSYNE-CI-1). In production callers store the
26 // pool in thread_local! storage so it persists between calls.
27 let f64_pool: ScratchPool<f64> = ScratchPool::new();
28 let f32_pool: ScratchPool<f32> = ScratchPool::new();
29
30 // Simple single-level scratch use: compute a dot product via a temp buffer.
31 f64_pool.with_scratch(1024, |scratch| {
32 for (i, slot) in scratch.iter_mut().enumerate() {
33 *slot = i as f64;
34 }
35 let partial: f64 = scratch.iter().sum();
36 println!(
37 "sum of 0..1024 via scratch: {} (expected {})",
38 partial,
39 (0..1024usize).sum::<usize>() as f64,
40 );
41 assert_eq!(partial, (0..1024usize).sum::<usize>() as f64);
42 });
43
44 // Nested borrows: outer scratch holds the signal; inner holds a window.
45 f64_pool.with_scratch(256, |signal| {
46 for (i, s) in signal.iter_mut().enumerate() {
47 *s = (i as f64).sin();
48 }
49 f64_pool.with_scratch(16, |window| {
50 window.copy_from_slice(&signal[..16]);
51 let dp = dot_product(window, &signal[..16]);
52 println!("windowed dot-product: {dp:.6}");
53 assert!(dp.is_finite());
54 });
55 });
56
57 // F32 pool: same API, different element type.
58 f32_pool.with_scratch(64, |buf| {
59 buf.fill(1.0_f32);
60 let total: f32 = buf.iter().sum();
61 println!("f32 scratch sum: {total}");
62 assert_eq!(total, 64.0_f32);
63 });
64
65 // Banked scratch keeps multiple related roles in one const-generic group,
66 // which matches transform pipelines that need independent temporary views
67 // without falling back to the system allocator.
68 let bank: ScratchBank<f64, 2> = ScratchBank::new();
69 bank.with_scratch::<1, _>(32, |scratch| {
70 scratch.fill(3.5);
71 assert!(scratch.iter().all(|v| *v == 3.5));
72 });
73
74 // Bounded provisioning retains the working set while making geometric
75 // growth headroom reclaimable at a consumer-selected quiescent point.
76 let bounded_pool: ScratchPool<u32> = ScratchPool::new();
77 bounded_pool.with_scratch_bounded(1024, |_| {});
78 bounded_pool.with_scratch_bounded(1025, |_| {});
79 assert_eq!(bounded_pool.capacity(), 2048);
80 let retained = bounded_pool.release();
81 println!(
82 "bounded release: capacity={} -> retained={}",
83 2048, retained[0]
84 );
85 assert_eq!(retained[0], 1025);
86 bounded_pool.reset();
87 assert_eq!(bounded_pool.release()[0], 0);
88
89 println!("MAX_POOL_SLOTS = {MAX_POOL_SLOTS} (max concurrent nested borrows)");
90 println!("all scratch-pool assertions passed");
91}Sourcepub unsafe fn with_scratch_uninit<R>(
&self,
n: usize,
f: impl FnOnce(*mut [T]) -> R,
) -> R
pub unsafe fn with_scratch_uninit<R>( &self, n: usize, f: impl FnOnce(*mut [T]) -> R, ) -> R
Like with_scratch but provides uninitialized
memory via a raw pointer. The caller must initialize all elements.
§Safety
Every element of the returned slice must be initialized before any safe read on the same allocation.
Source§impl<T> ScratchPool<T>where
T: ScratchElement,
impl<T> ScratchPool<T>where
T: ScratchElement,
Sourcepub fn release(&self) -> [usize; 4]
pub fn release(&self) -> [usize; 4]
Reclaims every slot’s storage above its recorded provision.
With with_scratch_bounded as the only
entry point, a provision is the largest request ever seen at that
depth; the slot keeps capacity for it (warm reuse stays allocation-free)
and surrenders everything above — growth headroom included, so the
retained steady state is exactly the working set. Slots whose provision
is zero are dropped entirely. A slot is reclaimed only when its depth
is idle; busy slots are skipped, never torn from under a live borrow.
The intended quiescent rhythm: run transforms normally, then call this
when the workload idles — not on every with_scratch exit, which would
reintroduce the churn the pool exists to remove. Provisions persist, so
repeated release/idle cycles converge; a smaller steady-state working
set needs reset.
Returns the per-slot capacities after reclamation. A slot that is
currently borrowed reports its provision instead: its live capacity is
not observable without deriving a reference under an existing exclusive
borrow, the same aliasing capacity exists to avoid.
Examples found in repository?
22fn main() {
23 // Construct a pool locally and use it directly (not thread_local! here
24 // to avoid the clippy::missing_const_for_thread_local false positive on
25 // 1.97.0 — see ATLAS-MNEMOSYNE-CI-1). In production callers store the
26 // pool in thread_local! storage so it persists between calls.
27 let f64_pool: ScratchPool<f64> = ScratchPool::new();
28 let f32_pool: ScratchPool<f32> = ScratchPool::new();
29
30 // Simple single-level scratch use: compute a dot product via a temp buffer.
31 f64_pool.with_scratch(1024, |scratch| {
32 for (i, slot) in scratch.iter_mut().enumerate() {
33 *slot = i as f64;
34 }
35 let partial: f64 = scratch.iter().sum();
36 println!(
37 "sum of 0..1024 via scratch: {} (expected {})",
38 partial,
39 (0..1024usize).sum::<usize>() as f64,
40 );
41 assert_eq!(partial, (0..1024usize).sum::<usize>() as f64);
42 });
43
44 // Nested borrows: outer scratch holds the signal; inner holds a window.
45 f64_pool.with_scratch(256, |signal| {
46 for (i, s) in signal.iter_mut().enumerate() {
47 *s = (i as f64).sin();
48 }
49 f64_pool.with_scratch(16, |window| {
50 window.copy_from_slice(&signal[..16]);
51 let dp = dot_product(window, &signal[..16]);
52 println!("windowed dot-product: {dp:.6}");
53 assert!(dp.is_finite());
54 });
55 });
56
57 // F32 pool: same API, different element type.
58 f32_pool.with_scratch(64, |buf| {
59 buf.fill(1.0_f32);
60 let total: f32 = buf.iter().sum();
61 println!("f32 scratch sum: {total}");
62 assert_eq!(total, 64.0_f32);
63 });
64
65 // Banked scratch keeps multiple related roles in one const-generic group,
66 // which matches transform pipelines that need independent temporary views
67 // without falling back to the system allocator.
68 let bank: ScratchBank<f64, 2> = ScratchBank::new();
69 bank.with_scratch::<1, _>(32, |scratch| {
70 scratch.fill(3.5);
71 assert!(scratch.iter().all(|v| *v == 3.5));
72 });
73
74 // Bounded provisioning retains the working set while making geometric
75 // growth headroom reclaimable at a consumer-selected quiescent point.
76 let bounded_pool: ScratchPool<u32> = ScratchPool::new();
77 bounded_pool.with_scratch_bounded(1024, |_| {});
78 bounded_pool.with_scratch_bounded(1025, |_| {});
79 assert_eq!(bounded_pool.capacity(), 2048);
80 let retained = bounded_pool.release();
81 println!(
82 "bounded release: capacity={} -> retained={}",
83 2048, retained[0]
84 );
85 assert_eq!(retained[0], 1025);
86 bounded_pool.reset();
87 assert_eq!(bounded_pool.release()[0], 0);
88
89 println!("MAX_POOL_SLOTS = {MAX_POOL_SLOTS} (max concurrent nested borrows)");
90 println!("all scratch-pool assertions passed");
91}Sourcepub fn reset(&self)
pub fn reset(&self)
Clears the recorded provisions so a later release
reclaims every slot entirely.
For a full working-set changeover (a consumer tearing down one workload
and starting another): reset, then run the new workload through
with_scratch_bounded, then release.
Slots that are not idle keep their buffers; the next release sees their
cleared provisions and reclaims them.
Examples found in repository?
22fn main() {
23 // Construct a pool locally and use it directly (not thread_local! here
24 // to avoid the clippy::missing_const_for_thread_local false positive on
25 // 1.97.0 — see ATLAS-MNEMOSYNE-CI-1). In production callers store the
26 // pool in thread_local! storage so it persists between calls.
27 let f64_pool: ScratchPool<f64> = ScratchPool::new();
28 let f32_pool: ScratchPool<f32> = ScratchPool::new();
29
30 // Simple single-level scratch use: compute a dot product via a temp buffer.
31 f64_pool.with_scratch(1024, |scratch| {
32 for (i, slot) in scratch.iter_mut().enumerate() {
33 *slot = i as f64;
34 }
35 let partial: f64 = scratch.iter().sum();
36 println!(
37 "sum of 0..1024 via scratch: {} (expected {})",
38 partial,
39 (0..1024usize).sum::<usize>() as f64,
40 );
41 assert_eq!(partial, (0..1024usize).sum::<usize>() as f64);
42 });
43
44 // Nested borrows: outer scratch holds the signal; inner holds a window.
45 f64_pool.with_scratch(256, |signal| {
46 for (i, s) in signal.iter_mut().enumerate() {
47 *s = (i as f64).sin();
48 }
49 f64_pool.with_scratch(16, |window| {
50 window.copy_from_slice(&signal[..16]);
51 let dp = dot_product(window, &signal[..16]);
52 println!("windowed dot-product: {dp:.6}");
53 assert!(dp.is_finite());
54 });
55 });
56
57 // F32 pool: same API, different element type.
58 f32_pool.with_scratch(64, |buf| {
59 buf.fill(1.0_f32);
60 let total: f32 = buf.iter().sum();
61 println!("f32 scratch sum: {total}");
62 assert_eq!(total, 64.0_f32);
63 });
64
65 // Banked scratch keeps multiple related roles in one const-generic group,
66 // which matches transform pipelines that need independent temporary views
67 // without falling back to the system allocator.
68 let bank: ScratchBank<f64, 2> = ScratchBank::new();
69 bank.with_scratch::<1, _>(32, |scratch| {
70 scratch.fill(3.5);
71 assert!(scratch.iter().all(|v| *v == 3.5));
72 });
73
74 // Bounded provisioning retains the working set while making geometric
75 // growth headroom reclaimable at a consumer-selected quiescent point.
76 let bounded_pool: ScratchPool<u32> = ScratchPool::new();
77 bounded_pool.with_scratch_bounded(1024, |_| {});
78 bounded_pool.with_scratch_bounded(1025, |_| {});
79 assert_eq!(bounded_pool.capacity(), 2048);
80 let retained = bounded_pool.release();
81 println!(
82 "bounded release: capacity={} -> retained={}",
83 2048, retained[0]
84 );
85 assert_eq!(retained[0], 1025);
86 bounded_pool.reset();
87 assert_eq!(bounded_pool.release()[0], 0);
88
89 println!("MAX_POOL_SLOTS = {MAX_POOL_SLOTS} (max concurrent nested borrows)");
90 println!("all scratch-pool assertions passed");
91}Sourcepub fn prewarm(&self, min_capacity: usize)
pub fn prewarm(&self, min_capacity: usize)
Ensures the primary slot has capacity for at least min_capacity
elements, growing it if necessary. No-op when the pool is borrowed.
Sourcepub fn preload(&self, sizes: &[usize])
pub fn preload(&self, sizes: &[usize])
Prewarms multiple slots in one call.
sizes[i] specifies the minimum capacity for slot i (depth i).
Out-of-range indices or a borrowed slot are silently skipped.
Entries of 0 skip that slot.
Sourcepub fn shrink_all_slots(&self)
pub fn shrink_all_slots(&self)
Releases all slot allocations when not borrowed. No-op when borrowed.
Source§impl<T> ScratchPool<T>where
T: ScratchElement,
impl<T> ScratchPool<T>where
T: ScratchElement,
Sourcepub fn borrow_depth(&self) -> u8
pub fn borrow_depth(&self) -> u8
Returns the current borrow depth (0 = fully available).
Sourcepub fn capacity(&self) -> usize
pub fn capacity(&self) -> usize
Returns the capacity of the first slot (primary buffer).
Callable at any time, including from inside a live
Self::with_scratch borrow of that same slot. The figure is read from
a mirror maintained outside the slot’s UnsafeCell, so the accessor
never derives a reference that could alias the exclusive one the borrow
holds — the reentrant call is sound rather than merely undetected, and it
neither panics nor reports a stale value.
Every slot carries such a mirror; see Self::total_capacity_bytes for
the sum across all of them.
Examples found in repository?
22fn main() {
23 // Construct a pool locally and use it directly (not thread_local! here
24 // to avoid the clippy::missing_const_for_thread_local false positive on
25 // 1.97.0 — see ATLAS-MNEMOSYNE-CI-1). In production callers store the
26 // pool in thread_local! storage so it persists between calls.
27 let f64_pool: ScratchPool<f64> = ScratchPool::new();
28 let f32_pool: ScratchPool<f32> = ScratchPool::new();
29
30 // Simple single-level scratch use: compute a dot product via a temp buffer.
31 f64_pool.with_scratch(1024, |scratch| {
32 for (i, slot) in scratch.iter_mut().enumerate() {
33 *slot = i as f64;
34 }
35 let partial: f64 = scratch.iter().sum();
36 println!(
37 "sum of 0..1024 via scratch: {} (expected {})",
38 partial,
39 (0..1024usize).sum::<usize>() as f64,
40 );
41 assert_eq!(partial, (0..1024usize).sum::<usize>() as f64);
42 });
43
44 // Nested borrows: outer scratch holds the signal; inner holds a window.
45 f64_pool.with_scratch(256, |signal| {
46 for (i, s) in signal.iter_mut().enumerate() {
47 *s = (i as f64).sin();
48 }
49 f64_pool.with_scratch(16, |window| {
50 window.copy_from_slice(&signal[..16]);
51 let dp = dot_product(window, &signal[..16]);
52 println!("windowed dot-product: {dp:.6}");
53 assert!(dp.is_finite());
54 });
55 });
56
57 // F32 pool: same API, different element type.
58 f32_pool.with_scratch(64, |buf| {
59 buf.fill(1.0_f32);
60 let total: f32 = buf.iter().sum();
61 println!("f32 scratch sum: {total}");
62 assert_eq!(total, 64.0_f32);
63 });
64
65 // Banked scratch keeps multiple related roles in one const-generic group,
66 // which matches transform pipelines that need independent temporary views
67 // without falling back to the system allocator.
68 let bank: ScratchBank<f64, 2> = ScratchBank::new();
69 bank.with_scratch::<1, _>(32, |scratch| {
70 scratch.fill(3.5);
71 assert!(scratch.iter().all(|v| *v == 3.5));
72 });
73
74 // Bounded provisioning retains the working set while making geometric
75 // growth headroom reclaimable at a consumer-selected quiescent point.
76 let bounded_pool: ScratchPool<u32> = ScratchPool::new();
77 bounded_pool.with_scratch_bounded(1024, |_| {});
78 bounded_pool.with_scratch_bounded(1025, |_| {});
79 assert_eq!(bounded_pool.capacity(), 2048);
80 let retained = bounded_pool.release();
81 println!(
82 "bounded release: capacity={} -> retained={}",
83 2048, retained[0]
84 );
85 assert_eq!(retained[0], 1025);
86 bounded_pool.reset();
87 assert_eq!(bounded_pool.release()[0], 0);
88
89 println!("MAX_POOL_SLOTS = {MAX_POOL_SLOTS} (max concurrent nested borrows)");
90 println!("all scratch-pool assertions passed");
91}Sourcepub fn is_available(&self) -> bool
pub fn is_available(&self) -> bool
Returns true when the pool has at least one slot available for a
new borrow (borrow_depth < MAX_POOL_SLOTS).
Sourcepub fn slot_capacity(&self, idx: usize) -> usize
pub fn slot_capacity(&self, idx: usize) -> usize
Returns the backing capacity of slot idx, or 0 when idx is out
of range. Callable at any time including during a live borrow.
Sourcepub fn total_capacity_bytes(&self) -> usize
pub fn total_capacity_bytes(&self) -> usize
Sum of backing capacities across all slots, in bytes.
Source§impl<T> ScratchPool<T>where
T: ScratchElement,
impl<T> ScratchPool<T>where
T: ScratchElement,
Sourcepub const fn new() -> ScratchPool<T>
pub const fn new() -> ScratchPool<T>
Creates a new empty scratch pool (zero allocation at construction).
Examples found in repository?
22fn main() {
23 // Construct a pool locally and use it directly (not thread_local! here
24 // to avoid the clippy::missing_const_for_thread_local false positive on
25 // 1.97.0 — see ATLAS-MNEMOSYNE-CI-1). In production callers store the
26 // pool in thread_local! storage so it persists between calls.
27 let f64_pool: ScratchPool<f64> = ScratchPool::new();
28 let f32_pool: ScratchPool<f32> = ScratchPool::new();
29
30 // Simple single-level scratch use: compute a dot product via a temp buffer.
31 f64_pool.with_scratch(1024, |scratch| {
32 for (i, slot) in scratch.iter_mut().enumerate() {
33 *slot = i as f64;
34 }
35 let partial: f64 = scratch.iter().sum();
36 println!(
37 "sum of 0..1024 via scratch: {} (expected {})",
38 partial,
39 (0..1024usize).sum::<usize>() as f64,
40 );
41 assert_eq!(partial, (0..1024usize).sum::<usize>() as f64);
42 });
43
44 // Nested borrows: outer scratch holds the signal; inner holds a window.
45 f64_pool.with_scratch(256, |signal| {
46 for (i, s) in signal.iter_mut().enumerate() {
47 *s = (i as f64).sin();
48 }
49 f64_pool.with_scratch(16, |window| {
50 window.copy_from_slice(&signal[..16]);
51 let dp = dot_product(window, &signal[..16]);
52 println!("windowed dot-product: {dp:.6}");
53 assert!(dp.is_finite());
54 });
55 });
56
57 // F32 pool: same API, different element type.
58 f32_pool.with_scratch(64, |buf| {
59 buf.fill(1.0_f32);
60 let total: f32 = buf.iter().sum();
61 println!("f32 scratch sum: {total}");
62 assert_eq!(total, 64.0_f32);
63 });
64
65 // Banked scratch keeps multiple related roles in one const-generic group,
66 // which matches transform pipelines that need independent temporary views
67 // without falling back to the system allocator.
68 let bank: ScratchBank<f64, 2> = ScratchBank::new();
69 bank.with_scratch::<1, _>(32, |scratch| {
70 scratch.fill(3.5);
71 assert!(scratch.iter().all(|v| *v == 3.5));
72 });
73
74 // Bounded provisioning retains the working set while making geometric
75 // growth headroom reclaimable at a consumer-selected quiescent point.
76 let bounded_pool: ScratchPool<u32> = ScratchPool::new();
77 bounded_pool.with_scratch_bounded(1024, |_| {});
78 bounded_pool.with_scratch_bounded(1025, |_| {});
79 assert_eq!(bounded_pool.capacity(), 2048);
80 let retained = bounded_pool.release();
81 println!(
82 "bounded release: capacity={} -> retained={}",
83 2048, retained[0]
84 );
85 assert_eq!(retained[0], 1025);
86 bounded_pool.reset();
87 assert_eq!(bounded_pool.release()[0], 0);
88
89 println!("MAX_POOL_SLOTS = {MAX_POOL_SLOTS} (max concurrent nested borrows)");
90 println!("all scratch-pool assertions passed");
91}Sourcepub fn with_slot_capacity(capacity: usize) -> ScratchPool<T>
pub fn with_slot_capacity(capacity: usize) -> ScratchPool<T>
Creates a new scratch pool with pre-allocated capacity per slot.