Skip to main content

beam/
arena.rs

1//! Thread-safe bump allocator implementing `allocator-api2`'s `Allocator` trait.
2//!
3//! ## Why not `bumpalo`?
4//!
5//! [`bumpalo::Bump`] is `!Send + !Sync` — bump allocation is inherently
6//! single-threaded because the bump pointer is a plain `Cell<usize>`. BEAM's
7//! tokio multi-threaded runtime sends `Arc<Put>` between worker threads via
8//! channels, which requires `Put: Send`. If the `BTreeMap` inside `Put` uses
9//! a `!Send` allocator, the entire `Put` becomes `!Send` and cannot cross
10//! thread boundaries.
11//!
12//! ## Design
13//!
14//! `SyncBumpArena` wraps a `Mutex<SyncBumpInner>` behind an `Arc`. The mutex
15//! is only contended during **allocation** (construction of BTreeMap nodes).
16//! Read-only access to the BTreeMap (lookups, iteration, serialization) does
17//! not touch the allocator at all — the `Allocator` trait's `allocate` and
18//! `deallocate` methods are only called during insertion and drop.
19//!
20//! The inner state holds a chunk list (`Vec<Chunk>`) and a cursor pointing
21//! into the current chunk. When the current chunk is exhausted, a new chunk
22//! is allocated from the global allocator (doubling in size, starting at
23//! 4 KiB). `deallocate` is a no-op — all memory is freed at once when the
24//! last `Arc` reference drops.
25//!
26//! ## Performance
27//!
28//! - **Allocation**: `Mutex::lock` + pointer bump. The lock is held for
29//!   nanoseconds (just advancing a cursor). In practice, contention is rare
30//!   because BTreeMap construction happens in a single actor's context.
31//! - **Drop**: O(chunks) — free each chunk. O(1) relative to the number of
32//!   entries in the BTreeMap. This is the primary win: std's BTreeMap drop
33//!   walks every node; ours just frees a few chunks.
34//! - **Clone**: `Arc::clone` — one atomic increment.
35//!
36//! ## Safety
37//!
38//! The `unsafe impl Allocator` delegates to `SyncBumpInner::allocate`,
39//! which returns valid, aligned memory from a chunk. The memory remains
40//! valid until the `Arc<SyncBumpInner>` is dropped (i.e., until all clones
41//! of the arena are gone). `deallocate` is a no-op, which is sound for bump
42//! allocators.
43
44use allocator_api2::alloc::{AllocError, Allocator, Global, Layout};
45use std::ptr::NonNull;
46use std::sync::{Arc, Mutex};
47
48// ───────────────────────────────────────────────────────────────────────
49// Chunk — a single contiguous block of memory
50// ───────────────────────────────────────────────────────────────────────
51
52/// A contiguous block of memory used by the bump allocator.
53///
54/// Memory is allocated from the global allocator and freed on drop.
55struct Chunk {
56    /// The backing memory, allocated via `Global`.
57    /// Stored as `Vec<u8>` for automatic drop — when the `Chunk` is dropped,
58    /// the `Vec` returns its memory to the global allocator.
59    data: Vec<u8>,
60}
61
62impl Chunk {
63    /// Allocates a new chunk of the given size.
64    fn new(size: usize) -> Self {
65        // Use `Layout` for proper alignment on the Vec's backing allocation.
66        // We align to 16 to cover most BTreeMap node types.
67        let layout = Layout::from_size_align(size, 16).expect("invalid layout");
68        let ptr = Global.allocate(layout).expect("global alloc failed").cast();
69        // SAFETY: we just allocated `size` bytes with alignment 16.
70        let data = unsafe { Vec::from_raw_parts(ptr.as_ptr(), 0, size) };
71        Self { data }
72    }
73
74    /// Returns the usable capacity of this chunk.
75    #[inline]
76    fn capacity(&self) -> usize {
77        self.data.capacity()
78    }
79
80    /// Returns a raw pointer to the start of the chunk's unused region.
81    #[inline]
82    fn start(&self) -> *const u8 {
83        self.data.as_ptr()
84    }
85}
86
87// ───────────────────────────────────────────────────────────────────────
88// SyncBumpInner — the actual bump allocator state (behind Mutex)
89// ───────────────────────────────────────────────────────────────────────
90
91/// Internal state of the bump allocator, protected by a `Mutex`.
92struct SyncBumpInner {
93    /// Chunks of backing memory. The last chunk is the "current" one being
94    /// bumped. Previous chunks are full.
95    chunks: Vec<Chunk>,
96    /// Offset (in bytes) into the current chunk where the next allocation
97    /// will start.
98    cursor: usize,
99    /// The capacity of the current (last) chunk.
100    current_cap: usize,
101}
102
103impl SyncBumpInner {
104    /// Creates a new empty inner state.
105    fn new() -> Self {
106        Self {
107            chunks: Vec::new(),
108            cursor: 0,
109            current_cap: 0,
110        }
111    }
112
113    /// Creates a new inner state with an initial chunk of the given size.
114    fn with_capacity(cap: usize) -> Self {
115        let chunk = Chunk::new(cap);
116        let cap = chunk.capacity();
117        Self {
118            chunks: vec![chunk],
119            cursor: 0,
120            current_cap: cap,
121        }
122    }
123
124    /// Allocates `layout.size()` bytes with `layout.align()` alignment from
125    /// the bump arena. Returns a `NonNull<[u8]>` slice.
126    ///
127    /// If the current chunk doesn't have enough space, a new chunk is
128    /// allocated (doubling in size, minimum 4 KiB).
129    fn allocate(&mut self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
130        let size = layout.size();
131        let align = layout.align();
132
133        // Handle zero-sized allocations — return a dangling but aligned pointer.
134        if size == 0 {
135            return Ok(NonNull::slice_from_raw_parts(NonNull::dangling(), 0));
136        }
137
138        // Try the current chunk first.
139        if let Some(ptr) = self.try_alloc_in_current(align, size) {
140            return Ok(NonNull::slice_from_raw_parts(
141                NonNull::new(ptr).expect("non-null from valid chunk"),
142                size,
143            ));
144        }
145
146        // Current chunk is full — grow.
147        self.grow(align, size);
148
149        // Try again with the new chunk.
150        let ptr = self
151            .try_alloc_in_current(align, size)
152            .expect("freshly grown chunk must have room");
153        Ok(NonNull::slice_from_raw_parts(
154            NonNull::new(ptr).expect("non-null from valid chunk"),
155            size,
156        ))
157    }
158
159    /// Attempts to allocate from the current chunk, aligning `cursor` to
160    /// `align` and checking that `size` bytes fit. Returns `Some(ptr)` on
161    /// success, `None` if the chunk is full.
162    #[inline]
163    fn try_alloc_in_current(&mut self, align: usize, size: usize) -> Option<*mut u8> {
164        let chunk = self.chunks.last()?;
165        let base = chunk.start() as usize;
166        let offset = self.cursor;
167
168        // Align the cursor upward.
169        let aligned_offset = (base + offset + align - 1) & !(align - 1);
170        let padding = aligned_offset - base - offset;
171        let new_cursor = offset + padding + size;
172
173        if new_cursor > self.current_cap {
174            return None;
175        }
176
177        self.cursor = new_cursor;
178        Some(aligned_offset as *mut u8)
179    }
180
181    /// Allocates a new chunk large enough for the requested allocation.
182    /// Chunk size doubles each time, starting at 4 KiB.
183    fn grow(&mut self, _align: usize, size: usize) {
184        // Minimum chunk size is 4 KiB. Double from the last chunk, but ensure
185        // we have at least `size` bytes available.
186        let next_size = (self.current_cap * 2).max(4096).max(size);
187
188        let chunk = Chunk::new(next_size);
189        self.current_cap = chunk.capacity();
190        self.cursor = 0;
191        self.chunks.push(chunk);
192    }
193}
194
195// ───────────────────────────────────────────────────────────────────────
196// SyncBumpArena — the public Allocator impl (Arc<Mutex<SyncBumpInner>>)
197// ───────────────────────────────────────────────────────────────────────
198
199/// A thread-safe bump arena allocator implementing `allocator-api2`'s
200/// `Allocator` trait.
201///
202/// Holds an `Arc<Mutex<SyncBumpInner>>` so it is `Clone + Send + Sync + 'static`.
203/// All clones share the same arena — when the last clone drops, all memory is
204/// freed at once (the chunks' `Vec` drop returns memory to the global allocator).
205///
206/// See the [module-level documentation](self) for the full design rationale
207/// and safety model.
208#[derive(Clone)]
209pub struct SyncBumpArena {
210    inner: Arc<Mutex<SyncBumpInner>>,
211}
212
213impl SyncBumpArena {
214    /// Creates a new `SyncBumpArena` with no initial capacity.
215    ///
216    /// The first allocation will trigger a 4 KiB chunk allocation.
217    #[inline]
218    #[must_use]
219    pub fn new() -> Self {
220        Self {
221            inner: Arc::new(Mutex::new(SyncBumpInner::new())),
222        }
223    }
224
225    /// Creates a new `SyncBumpArena` with an initial chunk of the given size.
226    ///
227    /// This avoids a growth step on the first allocations if the approximate
228    /// total size is known in advance.
229    #[inline]
230    #[must_use]
231    pub fn with_capacity(capacity: usize) -> Self {
232        Self {
233            inner: Arc::new(Mutex::new(SyncBumpInner::with_capacity(capacity))),
234        }
235    }
236}
237
238impl Default for SyncBumpArena {
239    #[inline]
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245impl std::fmt::Debug for SyncBumpArena {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        f.debug_struct("SyncBumpArena")
248            .field("inner", &Arc::as_ptr(&self.inner))
249            .finish()
250    }
251}
252
253// SAFETY: `allocate` delegates to `SyncBumpInner::allocate` which returns
254// valid, aligned memory from a chunk. The memory remains valid until the
255// `Arc<Mutex<SyncBumpInner>>` is dropped (when all clones of the arena are
256// gone). `deallocate` is a no-op — sound for bump allocators because memory
257// is freed in bulk on drop, never per-allocation.
258unsafe impl Allocator for SyncBumpArena {
259    #[inline]
260    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
261        let mut inner = self.inner.lock().expect("mutex poisoned");
262        inner.allocate(layout)
263    }
264
265    #[inline]
266    unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
267        // No-op: all memory is freed when the arena drops.
268    }
269
270    #[inline]
271    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
272        let ptr = self.allocate(layout)?;
273        // SAFETY: the memory is valid and we own it exclusively (just allocated).
274        unsafe { ptr.cast::<u8>().as_ptr().write_bytes(0, layout.size()) };
275        Ok(ptr)
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use allocator_api2::alloc::Layout;
283
284    #[test]
285    fn test_allocate_basic() {
286        let arena = SyncBumpArena::new();
287        let layout = Layout::from_size_align(64, 8).unwrap();
288        let ptr = arena.allocate(layout).unwrap();
289        assert_eq!(ptr.len(), 64);
290        // Verify the memory is usable
291        unsafe {
292            ptr.cast::<u8>().as_ptr().write_bytes(0xAB, 64);
293        }
294    }
295
296    #[test]
297    fn test_allocate_alignment() {
298        let arena = SyncBumpArena::new();
299        for &align in &[1usize, 2, 4, 8, 16, 32, 64, 128] {
300            let layout = Layout::from_size_align(1, align).unwrap();
301            let ptr = arena.allocate(layout).unwrap();
302            let addr = ptr.cast::<u8>().as_ptr() as usize;
303            assert_eq!(addr % align, 0, "alignment {} not respected", align);
304        }
305    }
306
307    #[test]
308    fn test_allocate_zeroed() {
309        let arena = SyncBumpArena::new();
310        let layout = Layout::from_size_align(128, 8).unwrap();
311        let ptr = arena.allocate_zeroed(layout).unwrap();
312        let slice = unsafe { ptr.as_ref() };
313        assert!(
314            slice.iter().all(|&b| b == 0),
315            "allocate_zeroed returned non-zero memory"
316        );
317    }
318
319    #[test]
320    fn test_zero_size_allocation() {
321        let arena = SyncBumpArena::new();
322        let layout = Layout::from_size_align(0, 1).unwrap();
323        let ptr = arena.allocate(layout).unwrap();
324        assert_eq!(ptr.len(), 0);
325    }
326
327    #[test]
328    fn test_clone_shares_arena() {
329        let arena1 = SyncBumpArena::new();
330        let arena2 = arena1.clone();
331
332        let layout = Layout::from_size_align(8, 8).unwrap();
333        let ptr1 = arena1.allocate(layout).unwrap();
334        let ptr2 = arena2.allocate(layout).unwrap();
335
336        // Both allocations from the same arena — distinct addresses
337        let addr1 = ptr1.cast::<u8>().as_ptr() as usize;
338        let addr2 = ptr2.cast::<u8>().as_ptr() as usize;
339        assert_ne!(addr1, addr2);
340    }
341
342    #[test]
343    fn test_deallocate_is_noop() {
344        let arena = SyncBumpArena::new();
345        let layout = Layout::from_size_align(32, 8).unwrap();
346        let ptr = arena.allocate(layout).unwrap();
347
348        // deallocate should not crash
349        unsafe { arena.deallocate(ptr.cast(), layout) };
350
351        // Memory is still valid after deallocate
352        unsafe {
353            ptr.cast::<u8>().as_ptr().write_bytes(0xCD, 32);
354        }
355    }
356
357    #[test]
358    fn test_chunk_growth() {
359        // Start with a tiny capacity — force growth
360        let arena = SyncBumpArena::with_capacity(64);
361
362        // Allocate more than the initial capacity
363        let layout = Layout::from_size_align(128, 8).unwrap();
364        let ptr = arena.allocate(layout).unwrap();
365        assert_eq!(ptr.len(), 128);
366
367        // Further allocations should work after growth
368        let layout2 = Layout::from_size_align(256, 8).unwrap();
369        let ptr2 = arena.allocate(layout2).unwrap();
370        assert_eq!(ptr2.len(), 256);
371    }
372
373    #[test]
374    fn test_many_allocations() {
375        let arena = SyncBumpArena::with_capacity(4096);
376        let layout = Layout::from_size_align(48, 8).unwrap();
377
378        // Allocate many times — should trigger multiple chunk growths
379        let mut ptrs = Vec::new();
380        for _ in 0..1000 {
381            ptrs.push(arena.allocate(layout).unwrap());
382        }
383
384        // All pointers should be distinct
385        let addrs: Vec<usize> = ptrs
386            .iter()
387            .map(|p| p.cast::<u8>().as_ptr() as usize)
388            .collect();
389        let unique: std::collections::HashSet<_> = addrs.iter().collect();
390        assert_eq!(
391            unique.len(),
392            1000,
393            "all 1000 allocations should be at distinct addresses"
394        );
395    }
396
397    #[test]
398    fn test_drop_frees_memory() {
399        // Verify Arc refcounting works — dropping all clones frees the arena.
400        let arena1 = SyncBumpArena::new();
401        let arena2 = arena1.clone();
402
403        drop(arena1);
404        // arena2 still alive — allocation should work
405        let layout = Layout::from_size_align(16, 8).unwrap();
406        let _ = arena2.allocate(layout).unwrap();
407
408        drop(arena2);
409        // No way to test memory freeing directly, but no crash = success
410    }
411
412    #[test]
413    fn test_with_capacity() {
414        let arena = SyncBumpArena::with_capacity(8192);
415        let layout = Layout::from_size_align(4096, 8).unwrap();
416        let ptr = arena.allocate(layout).unwrap();
417        assert_eq!(ptr.len(), 4096);
418    }
419
420    #[test]
421    fn test_send_sync_bounds() {
422        // Compile-time verification that SyncBumpArena is Send + Sync
423        fn assert_send_sync<T: Send + Sync>() {}
424        assert_send_sync::<SyncBumpArena>();
425    }
426
427    #[test]
428    fn test_concurrent_allocation() {
429        use std::sync::Arc as StdArc;
430        use std::thread;
431
432        let arena = StdArc::new(SyncBumpArena::with_capacity(4096 * 4));
433        let layout = Layout::from_size_align(64, 8).unwrap();
434
435        // Spawn multiple threads that allocate from the same arena
436        let handles: Vec<_> = (0..4)
437            .map(|_| {
438                let arena = StdArc::clone(&arena);
439                thread::spawn(move || {
440                    let mut ptrs = Vec::new();
441                    for _ in 0..100 {
442                        ptrs.push(arena.allocate(layout).unwrap());
443                    }
444                    // Verify all allocations are distinct
445                    let addrs: Vec<usize> = ptrs
446                        .iter()
447                        .map(|p| p.cast::<u8>().as_ptr() as usize)
448                        .collect();
449                    let unique: std::collections::HashSet<_> = addrs.iter().collect();
450                    assert_eq!(unique.len(), 100);
451                })
452            })
453            .collect();
454
455        for handle in handles {
456            handle.join().expect("thread should not panic");
457        }
458    }
459
460    #[test]
461    fn test_btreemap_with_arena() {
462        use arena_btreemap::BTreeMap;
463
464        let arena = SyncBumpArena::with_capacity(4096);
465        let mut map: BTreeMap<String, i32, SyncBumpArena> = BTreeMap::new_in(arena);
466
467        // Insert entries — each BTreeMap node is allocated from the arena
468        for i in 0..100 {
469            map.insert(format!("key_{:04}", i), i);
470        }
471
472        // Verify entries are correct
473        for i in 0..100 {
474            assert_eq!(map.get(&format!("key_{:04}", i)), Some(&i));
475        }
476
477        // Verify iteration order (sorted by key)
478        let keys: Vec<_> = map.keys().take(5).collect();
479        assert_eq!(keys[0], "key_0000");
480        assert_eq!(keys[4], "key_0004");
481    }
482
483    #[test]
484    fn test_btreemap_clone_shares_arena() {
485        use arena_btreemap::BTreeMap;
486
487        let arena = SyncBumpArena::with_capacity(4096);
488        let mut map: BTreeMap<String, i32, SyncBumpArena> = BTreeMap::new_in(arena.clone());
489        map.insert("a".to_string(), 1);
490        map.insert("b".to_string(), 2);
491
492        // Clone the map — should share the same arena
493        let map2 = map.clone();
494        assert_eq!(map2.get("a"), Some(&1));
495        assert_eq!(map2.get("b"), Some(&2));
496
497        // Original map still works
498        assert_eq!(map.get("a"), Some(&1));
499    }
500}