Skip to main content

concinnity_memory/
arena.rs

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