Skip to main content

concinnity_core/memory/
arena.rs

1// A linear (bump) allocator for working memory that is thrown away wholesale.
2//
3// Per-frame scratch is the case it exists for: a list built to be walked once
4// and dropped costs a heap allocation and a free every frame, and a thousand of
5// those is a thousand trips through the global allocator plus the churn they
6// leave behind. An arena hands out slices of one buffer by moving a cursor, and
7// `reset` gives the whole frame back at once.
8//
9// Everything stored must be `Copy`. The arena never runs a destructor -- `reset`
10// only rewinds the cursor -- so a type that owns anything would silently leak
11// what it owns. `Copy` makes that a compile error instead of a slow leak.
12//
13// `alloc` takes `&self` while `reset` takes `&mut self`: handing out memory is
14// what a frame does from many call sites, and giving it back is only legal once
15// every borrow has ended, which is exactly what the borrow checker already
16// proves.
17
18use core::alloc::Layout;
19use core::cell::Cell;
20use core::mem::MaybeUninit;
21use core::ptr::NonNull;
22
23use crate::memory::tag::{MemTag, Realm};
24
25// The backing buffer's alignment, and so the strongest alignment the arena can
26// satisfy. Covers everything up to a cache line, which is past any vector type
27// the engine stores.
28const ARENA_ALIGN: usize = 64;
29
30/// A bump allocator over one fixed reservation, reset as a whole.
31pub struct Arena {
32    ptr: NonNull<u8>,
33    cap: usize,
34    used: Cell<usize>,
35    peak: Cell<usize>,
36    // Requests this arena could not satisfy. A caller falling back to the heap
37    // is correct but means the reserve is too small, and a silent fallback
38    // reads as "the arena is sized right" when it is not.
39    overflows: Cell<u32>,
40    // Where the reservation is accounted, for as long as the arena lives.
41    tag: Option<MemTag>,
42}
43
44// Handing out `&mut` from `&self` is the arena's design, not an oversight:
45// allocations never overlap (the cursor only moves forward) and the memory
46// cannot be reclaimed while one is borrowed (`reset` takes `&mut self`). The
47// lint is right about the general case and wrong about this one.
48#[expect(
49    clippy::mut_from_ref,
50    reason = "allocations never overlap and reset takes &mut self, so the general-case lint does not apply"
51)]
52impl Arena {
53    /// Reserve `bytes` up front. The buffer is taken from the global allocator
54    /// once and held until the arena drops; nothing here allocates again.
55    pub fn with_capacity(bytes: usize) -> Self {
56        Self::new(bytes, None)
57    }
58
59    /// As `with_capacity`, reporting the reservation under `tag` in host memory
60    /// for as long as the arena lives. An arena's whole cost is its reservation,
61    /// so it can account for itself rather than making its owner do it.
62    pub fn tagged(bytes: usize, tag: MemTag) -> Self {
63        crate::memory::ledger().add(tag, Realm::Host, bytes as u64);
64        Self::new(bytes, Some(tag))
65    }
66
67    fn new(bytes: usize, tag: Option<MemTag>) -> Self {
68        if bytes == 0 {
69            return Self {
70                ptr: NonNull::dangling(),
71                cap: 0,
72                used: Cell::new(0),
73                peak: Cell::new(0),
74                overflows: Cell::new(0),
75                tag,
76            };
77        }
78        let layout = Layout::from_size_align(bytes, ARENA_ALIGN).expect("arena layout");
79        // SAFETY: `layout` has a non-zero size.
80        let ptr = unsafe { alloc::alloc::alloc(layout) };
81        let Some(ptr) = NonNull::new(ptr) else {
82            alloc::alloc::handle_alloc_error(layout)
83        };
84        Self {
85            ptr,
86            cap: bytes,
87            used: Cell::new(0),
88            peak: Cell::new(0),
89            overflows: Cell::new(0),
90            tag,
91        }
92    }
93
94    /// The reservation size in bytes.
95    pub fn capacity(&self) -> usize {
96        self.cap
97    }
98
99    /// Bytes handed out since the last reset.
100    pub fn used(&self) -> usize {
101        self.used.get()
102    }
103
104    /// Bytes still available before the next request is declined.
105    pub fn remaining(&self) -> usize {
106        self.cap - self.used.get()
107    }
108
109    /// The most this arena has held between resets: what to size it from.
110    pub fn peak(&self) -> usize {
111        self.peak.get()
112    }
113
114    /// Requests declined since the last `clear_overflows`. Non-zero means
115    /// callers fell back to the heap and the reserve wants raising.
116    pub fn overflows(&self) -> u32 {
117        self.overflows.get()
118    }
119
120    /// Reset the overflow counter.
121    pub fn clear_overflows(&self) {
122        self.overflows.set(0);
123    }
124
125    /// Give back everything handed out. Taking `&mut self` is the safety
126    /// argument: no allocation from this arena can still be borrowed.
127    ///
128    /// Deliberately leaves `peak` and `overflows` alone: both describe the worst
129    /// frame so far, which is what sizes the reserve, and a per-frame reset would
130    /// erase exactly the evidence they exist to carry.
131    pub fn reset(&mut self) {
132        self.used.set(0);
133    }
134
135    /// Move `value` into the arena. `None` when the arena is full, which is the
136    /// caller's cue to fall back to the heap rather than a failure.
137    pub fn alloc<T: Copy>(&self, value: T) -> Option<&mut T> {
138        let ptr = self.bump(size_of::<T>(), align_of::<T>())?.cast::<T>();
139        // SAFETY: `bump` returned a region of `size_of::<T>()` bytes aligned for
140        // `T`, inside the arena and not overlapping any other live allocation
141        // (the cursor only ever moves forward until `reset`, which needs
142        // `&mut self` and so cannot run while this borrow lives).
143        unsafe {
144            ptr.write(value);
145            Some(&mut *ptr.as_ptr())
146        }
147    }
148
149    #[cfg(test)]
150    /// A slice of `len` copies of `value`.
151    pub(crate) fn alloc_slice<T: Copy>(&self, len: usize, value: T) -> Option<&mut [T]> {
152        let slice = self.uninit_slice::<T>(len)?;
153        for slot in slice.iter_mut() {
154            slot.write(value);
155        }
156        // SAFETY: every element was just written.
157        Some(unsafe { assume_init_mut(slice) })
158    }
159
160    // A copy of `src` in the arena.
161    #[cfg(test)]
162    pub(crate) fn alloc_slice_copy<T: Copy>(&self, src: &[T]) -> Option<&mut [T]> {
163        let slice = self.uninit_slice::<T>(src.len())?;
164        for (slot, value) in slice.iter_mut().zip(src) {
165            slot.write(*value);
166        }
167        // SAFETY: `slice` and `src` have the same length, so every element was
168        // written.
169        Some(unsafe { assume_init_mut(slice) })
170    }
171
172    /// An empty vector holding `capacity` elements' worth of arena. Pushing past
173    /// that capacity does not grow -- the caller reserves the bound it knows.
174    pub fn vec<T: Copy>(&self, capacity: usize) -> Option<ArenaVec<'_, T>> {
175        Some(ArenaVec {
176            buf: self.uninit_slice::<T>(capacity)?,
177            len: 0,
178        })
179    }
180
181    fn uninit_slice<T>(&self, len: usize) -> Option<&mut [MaybeUninit<T>]> {
182        // A length that overflows is asking for more than exists, so it takes
183        // the same declined-and-counted path as any other oversized request.
184        let bytes = size_of::<T>().saturating_mul(len);
185        let ptr = self.bump(bytes, align_of::<T>())?.cast::<MaybeUninit<T>>();
186        // SAFETY: `bump` returned `len * size_of::<T>()` bytes aligned for `T`
187        // inside the arena, and no other live allocation overlaps them.
188        // `MaybeUninit<T>` is valid for any bit pattern, so the region needs no
189        // initialization to be read as this type.
190        Some(unsafe { core::slice::from_raw_parts_mut(ptr.as_ptr(), len) })
191    }
192
193    // Carve `size` bytes aligned to `align` off the front of what is left,
194    // counting anything this arena could not satisfy.
195    fn bump(&self, size: usize, align: usize) -> Option<NonNull<u8>> {
196        let carved = self.try_bump(size, align);
197        if carved.is_none() {
198            self.overflows.set(self.overflows.get().saturating_add(1));
199        }
200        carved
201    }
202
203    fn try_bump(&self, size: usize, align: usize) -> Option<NonNull<u8>> {
204        if align > ARENA_ALIGN || self.cap == 0 {
205            return None;
206        }
207        let start = self.used.get().checked_next_multiple_of(align)?;
208        let end = start.checked_add(size)?;
209        if end > self.cap {
210            return None;
211        }
212        self.used.set(end);
213        if end > self.peak.get() {
214            self.peak.set(end);
215        }
216        // SAFETY: `start <= end <= cap`, so the offset stays inside the one
217        // allocation `ptr` owns.
218        Some(unsafe { NonNull::new_unchecked(self.ptr.as_ptr().add(start)) })
219    }
220}
221
222impl Drop for Arena {
223    fn drop(&mut self) {
224        if let Some(tag) = self.tag {
225            crate::memory::ledger().release(tag, Realm::Host, self.cap as u64);
226        }
227        if self.cap == 0 {
228            return;
229        }
230        let layout = Layout::from_size_align(self.cap, ARENA_ALIGN).expect("arena layout");
231        // SAFETY: `ptr` came from `alloc` with this exact layout in
232        // `with_capacity`, and nothing can still borrow it: `Drop` takes
233        // `&mut self`.
234        unsafe { alloc::alloc::dealloc(self.ptr.as_ptr(), layout) };
235    }
236}
237
238// SAFETY: an `Arena` owns its buffer outright and hands out borrows tied to
239// itself, so moving one to another thread moves the whole allocation with it.
240// The `Cell` cursor makes it (correctly) `!Sync`, which is what stops two
241// threads bumping the same cursor.
242unsafe impl Send for Arena {}
243
244/// A vector over a reservation in an arena: pushes cost a write, and the whole
245/// thing disappears when the arena resets.
246pub struct ArenaVec<'a, T: Copy> {
247    buf: &'a mut [MaybeUninit<T>],
248    len: usize,
249}
250
251impl<T: Copy> ArenaVec<'_, T> {
252    /// Elements appended so far.
253    pub fn len(&self) -> usize {
254        self.len
255    }
256
257    /// Whether nothing has been appended.
258    pub fn is_empty(&self) -> bool {
259        self.len == 0
260    }
261
262    /// The fixed reservation, in elements.
263    pub fn capacity(&self) -> usize {
264        self.buf.len()
265    }
266
267    pub(crate) fn is_full(&self) -> bool {
268        self.len == self.buf.len()
269    }
270
271    /// Append `value`, reporting whether it fit. The reservation is fixed, so a
272    /// `false` means the caller reserved less than it pushed.
273    #[must_use]
274    pub fn push(&mut self, value: T) -> bool {
275        if self.is_full() {
276            return false;
277        }
278        self.buf[self.len].write(value);
279        self.len += 1;
280        true
281    }
282
283    /// Append until the iterator ends or the reservation fills, returning how
284    /// many were appended.
285    pub fn extend(&mut self, values: impl IntoIterator<Item = T>) -> usize {
286        let before = self.len;
287        for value in values {
288            if !self.push(value) {
289                break;
290            }
291        }
292        self.len - before
293    }
294
295    /// Drop every appended element, keeping the reservation.
296    pub fn clear(&mut self) {
297        self.len = 0;
298    }
299
300    /// The appended elements.
301    pub fn as_slice(&self) -> &[T] {
302        // SAFETY: the first `len` elements were written by `push`.
303        unsafe { assume_init_ref(&self.buf[..self.len]) }
304    }
305
306    /// The appended elements, mutably.
307    pub fn as_mut_slice(&mut self) -> &mut [T] {
308        // SAFETY: the first `len` elements were written by `push`.
309        unsafe { assume_init_mut(&mut self.buf[..self.len]) }
310    }
311}
312
313impl<T: Copy> core::ops::Deref for ArenaVec<'_, T> {
314    type Target = [T];
315
316    fn deref(&self) -> &[T] {
317        self.as_slice()
318    }
319}
320
321impl<T: Copy> core::ops::DerefMut for ArenaVec<'_, T> {
322    fn deref_mut(&mut self) -> &mut [T] {
323        self.as_mut_slice()
324    }
325}
326
327impl<T: Copy + core::fmt::Debug> core::fmt::Debug for ArenaVec<'_, T> {
328    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
329        self.as_slice().fmt(f)
330    }
331}
332
333// SAFETY: every element of `slice` must have been initialized.
334unsafe fn assume_init_ref<T>(slice: &[MaybeUninit<T>]) -> &[T] {
335    // SAFETY: `MaybeUninit<T>` has the same layout as `T`, and the caller
336    // guarantees every element holds an initialized value.
337    unsafe { &*(slice as *const [MaybeUninit<T>] as *const [T]) }
338}
339
340// SAFETY: every element of `slice` must have been initialized.
341unsafe fn assume_init_mut<T>(slice: &mut [MaybeUninit<T>]) -> &mut [T] {
342    // SAFETY: as `assume_init_ref`, for a unique borrow.
343    unsafe { &mut *(slice as *mut [MaybeUninit<T>] as *mut [T]) }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn allocations_come_back_with_their_values() {
352        let arena = Arena::with_capacity(4096);
353        let a = arena.alloc(7u32).expect("fits");
354        let b = arena.alloc_slice(4, 1u16).expect("fits");
355        let c = arena.alloc_slice_copy(&[9u64, 8, 7]).expect("fits");
356
357        assert_eq!(*a, 7);
358        assert_eq!(b, &[1, 1, 1, 1]);
359        assert_eq!(c, &[9, 8, 7]);
360    }
361
362    // Separate allocations must not overlap: writing through one may not be
363    // visible through another.
364    #[test]
365    fn separate_allocations_do_not_overlap() {
366        let arena = Arena::with_capacity(4096);
367        let first = arena.alloc_slice(8, 0u32).expect("fits");
368        let second = arena.alloc_slice(8, 0u32).expect("fits");
369        first.fill(0xAAAA_AAAA);
370        second.fill(0x5555_5555);
371        assert!(first.iter().all(|&v| v == 0xAAAA_AAAA));
372        assert!(second.iter().all(|&v| v == 0x5555_5555));
373    }
374
375    #[test]
376    fn allocations_are_aligned_for_their_type() {
377        let arena = Arena::with_capacity(4096);
378        // A byte first, so the cursor sits at an odd offset.
379        let _ = arena.alloc(1u8).expect("fits");
380        let wide = arena.alloc(1u128).expect("fits");
381        assert!((wide as *const u128).is_aligned());
382
383        let _ = arena.alloc(1u8).expect("fits");
384        let slice = arena.alloc_slice(3, 0u64).expect("fits");
385        assert!(slice.as_ptr().is_aligned());
386    }
387
388    // Running out is a `None`, not a panic: the caller falls back to the heap.
389    #[test]
390    fn a_full_arena_declines_rather_than_panicking() {
391        let arena = Arena::with_capacity(64);
392        assert!(arena.alloc_slice(8, 0u64).is_some());
393        assert!(arena.alloc(0u8).is_none());
394        assert_eq!(arena.remaining(), 0);
395    }
396
397    #[test]
398    fn an_empty_arena_declines_everything() {
399        let arena = Arena::with_capacity(0);
400        assert_eq!(arena.capacity(), 0);
401        assert!(arena.alloc(1u8).is_none());
402    }
403
404    #[test]
405    fn reset_hands_the_whole_arena_back() {
406        let mut arena = Arena::with_capacity(128);
407        {
408            let slice = arena.alloc_slice(16, 0u64).expect("fits");
409            assert_eq!(slice.len(), 16);
410        }
411        assert_eq!(arena.used(), 128);
412        assert!(arena.alloc(0u8).is_none());
413
414        arena.reset();
415        assert_eq!(arena.used(), 0);
416        assert!(arena.alloc_slice(16, 0u64).is_some());
417    }
418
419    // Peak survives resets: it is what sizes the arena, so it must describe the
420    // worst frame rather than the current one.
421    #[test]
422    fn peak_survives_a_reset() {
423        let mut arena = Arena::with_capacity(1024);
424        let _ = arena.alloc_slice(64, 0u8).expect("fits");
425        arena.reset();
426        let _ = arena.alloc_slice(8, 0u8).expect("fits");
427
428        assert_eq!(arena.used(), 8);
429        assert_eq!(arena.peak(), 64);
430    }
431
432    // An over-aligned type the buffer cannot satisfy is declined, not
433    // mis-aligned.
434    #[test]
435    fn an_over_aligned_type_is_declined() {
436        #[repr(align(128))]
437        #[derive(Clone, Copy)]
438        struct Overaligned(u8);
439
440        let arena = Arena::with_capacity(4096);
441        let value = Overaligned(7);
442        assert_eq!(value.0, 7);
443        assert!(arena.alloc(value).is_none());
444    }
445
446    #[test]
447    fn a_vector_pushes_into_its_reservation() {
448        let arena = Arena::with_capacity(4096);
449        let mut v = arena.vec::<u32>(4).expect("fits");
450        assert!(v.is_empty());
451        for i in 0..4 {
452            assert!(v.push(i));
453        }
454        assert!(v.is_full());
455        assert_eq!(v.as_slice(), &[0, 1, 2, 3]);
456        assert_eq!(v.len(), 4);
457    }
458
459    // The reservation is the whole story: pushing past it reports `false`
460    // rather than growing into the rest of the arena.
461    #[test]
462    fn a_vector_declines_pushes_past_its_reservation() {
463        let arena = Arena::with_capacity(4096);
464        let mut v = arena.vec::<u8>(2).expect("fits");
465        assert!(v.push(1));
466        assert!(v.push(2));
467        assert!(!v.push(3));
468        assert_eq!(v.as_slice(), &[1, 2]);
469    }
470
471    #[test]
472    fn extend_reports_what_it_took() {
473        let arena = Arena::with_capacity(4096);
474        let mut v = arena.vec::<u16>(3).expect("fits");
475        assert_eq!(v.extend([1, 2, 3, 4, 5]), 3);
476        assert_eq!(v.as_slice(), &[1, 2, 3]);
477    }
478
479    #[test]
480    fn a_vector_sorts_and_reads_back_through_the_slice() {
481        let arena = Arena::with_capacity(4096);
482        let mut v = arena.vec::<u32>(5).expect("fits");
483        assert_eq!(v.extend([5, 3, 1, 4, 2]), 5);
484        v.sort_unstable();
485        assert_eq!(&*v, &[1, 2, 3, 4, 5]);
486    }
487
488    #[test]
489    fn clearing_a_vector_keeps_its_reservation() {
490        let arena = Arena::with_capacity(4096);
491        let mut v = arena.vec::<u8>(2).expect("fits");
492        assert!(v.push(1));
493        v.clear();
494        assert!(v.is_empty());
495        assert!(v.push(2));
496        assert_eq!(v.as_slice(), &[2]);
497    }
498
499    // Two vectors alive at once must own disjoint reservations.
500    #[test]
501    fn two_vectors_hold_separate_reservations() {
502        let arena = Arena::with_capacity(4096);
503        let mut a = arena.vec::<u32>(2).expect("fits");
504        let mut b = arena.vec::<u32>(2).expect("fits");
505        assert_eq!(a.extend([1, 2]), 2);
506        assert_eq!(b.extend([3, 4]), 2);
507        assert_eq!(a.as_slice(), &[1, 2]);
508        assert_eq!(b.as_slice(), &[3, 4]);
509    }
510
511    // A tagged arena accounts for itself: its reservation appears under its tag
512    // while it lives and is given back when it drops. Asserted as a delta,
513    // since the ledger it reports into is the process-wide one.
514    #[test]
515    fn a_tagged_arena_reports_its_reservation_for_as_long_as_it_lives() {
516        const BYTES: usize = 8192;
517        let held = || {
518            crate::memory::ledger()
519                .usage(MemTag::Scratch, Realm::Host)
520                .bytes
521        };
522
523        let before = held();
524        {
525            let arena = Arena::tagged(BYTES, MemTag::Scratch);
526            assert_eq!(held(), before + BYTES as u64);
527            // What it hands out does not change what it cost.
528            let _ = arena.alloc_slice(16, 0u8).expect("fits");
529            assert_eq!(held(), before + BYTES as u64);
530        }
531        assert_eq!(held(), before);
532    }
533
534    #[test]
535    fn an_untagged_arena_reports_nothing() {
536        let held = || crate::memory::ledger().usage(MemTag::Ui, Realm::Host).bytes;
537        let before = held();
538        let _arena = Arena::with_capacity(8192);
539        assert_eq!(held(), before);
540    }
541
542    #[test]
543    fn a_reservation_larger_than_the_arena_is_declined() {
544        let arena = Arena::with_capacity(64);
545        assert!(arena.vec::<u64>(1024).is_none());
546    }
547
548    // A declined request is counted, so a caller falling back to the heap
549    // leaves evidence the reserve is too small instead of hiding it.
550    #[test]
551    fn declined_requests_are_counted() {
552        let arena = Arena::with_capacity(64);
553        assert_eq!(arena.overflows(), 0);
554
555        assert!(arena.alloc_slice(8, 0u64).is_some());
556        assert_eq!(arena.overflows(), 0, "a request that fits counts nothing");
557
558        assert!(arena.alloc(0u8).is_none());
559        assert!(arena.vec::<u32>(4).is_none());
560        assert_eq!(arena.overflows(), 2);
561
562        arena.clear_overflows();
563        assert_eq!(arena.overflows(), 0);
564    }
565
566    // Sizing evidence has to outlive the frame that produced it, so neither
567    // counter is cleared by the per-frame reset.
568    #[test]
569    fn reset_keeps_the_sizing_evidence() {
570        let mut arena = Arena::with_capacity(64);
571        let _ = arena.alloc_slice(8, 0u64).expect("fits");
572        assert!(arena.alloc(0u8).is_none());
573
574        arena.reset();
575        assert_eq!(arena.used(), 0, "the cursor rewinds");
576        assert_eq!(arena.peak(), 64, "the peak does not");
577        assert_eq!(arena.overflows(), 1, "nor does the overflow count");
578    }
579
580    // An over-aligned type and an oversized length are both declines, and both
581    // reach the counter rather than returning early past it.
582    #[test]
583    fn every_decline_path_reaches_the_counter() {
584        #[repr(align(128))]
585        #[derive(Clone, Copy)]
586        struct Overaligned(u8);
587
588        let arena = Arena::with_capacity(4096);
589        let value = Overaligned(7);
590        assert_eq!(value.0, 7);
591        assert!(arena.alloc(value).is_none());
592        assert!(arena.vec::<u64>(usize::MAX).is_none());
593        assert_eq!(arena.overflows(), 2);
594
595        let empty = Arena::with_capacity(0);
596        assert!(empty.alloc(1u8).is_none());
597        assert_eq!(empty.overflows(), 1);
598    }
599}