Skip to main content

graphos_common/memory/
bump.rs

1//! Bump allocator for fast temporary allocations.
2//!
3//! The bump allocator is used for short-lived allocations within a single
4//! operation or query. It's extremely fast for allocation but cannot
5//! deallocate individual allocations.
6
7use std::cell::Cell;
8use std::ptr::NonNull;
9
10/// A fast bump allocator for temporary allocations.
11///
12/// Allocates memory by simply incrementing a pointer. All allocated memory
13/// is freed at once when the allocator is reset or dropped.
14///
15/// This allocator is not thread-safe and should only be used from a single thread.
16pub struct BumpAllocator {
17    /// The underlying bumpalo allocator.
18    inner: bumpalo::Bump,
19    /// Number of allocations made.
20    allocation_count: Cell<usize>,
21}
22
23impl BumpAllocator {
24    /// Creates a new bump allocator.
25    #[must_use]
26    pub fn new() -> Self {
27        Self {
28            inner: bumpalo::Bump::new(),
29            allocation_count: Cell::new(0),
30        }
31    }
32
33    /// Creates a new bump allocator with the given initial capacity.
34    #[must_use]
35    pub fn with_capacity(capacity: usize) -> Self {
36        Self {
37            inner: bumpalo::Bump::with_capacity(capacity),
38            allocation_count: Cell::new(0),
39        }
40    }
41
42    /// Allocates memory for a value of type T and initializes it.
43    #[inline]
44    pub fn alloc<T>(&self, value: T) -> &mut T {
45        self.allocation_count.set(self.allocation_count.get() + 1);
46        self.inner.alloc(value)
47    }
48
49    /// Allocates memory for a slice and copies the values.
50    #[inline]
51    pub fn alloc_slice_copy<T: Copy>(&self, values: &[T]) -> &mut [T] {
52        self.allocation_count.set(self.allocation_count.get() + 1);
53        self.inner.alloc_slice_copy(values)
54    }
55
56    /// Allocates memory for a slice and clones the values.
57    #[inline]
58    pub fn alloc_slice_clone<T: Clone>(&self, values: &[T]) -> &mut [T] {
59        self.allocation_count.set(self.allocation_count.get() + 1);
60        self.inner.alloc_slice_clone(values)
61    }
62
63    /// Allocates memory for a string and returns a mutable reference.
64    #[inline]
65    pub fn alloc_str(&self, s: &str) -> &mut str {
66        self.allocation_count.set(self.allocation_count.get() + 1);
67        self.inner.alloc_str(s)
68    }
69
70    /// Allocates raw bytes with the given layout.
71    #[inline]
72    pub fn alloc_layout(&self, layout: std::alloc::Layout) -> NonNull<u8> {
73        self.allocation_count.set(self.allocation_count.get() + 1);
74        self.inner.alloc_layout(layout)
75    }
76
77    /// Resets the allocator, freeing all allocated memory.
78    ///
79    /// This is very fast - it just resets the internal pointer.
80    #[inline]
81    pub fn reset(&mut self) {
82        self.inner.reset();
83        self.allocation_count.set(0);
84    }
85
86    /// Returns the number of bytes currently allocated.
87    #[must_use]
88    #[inline]
89    pub fn allocated_bytes(&self) -> usize {
90        self.inner.allocated_bytes()
91    }
92
93    /// Returns the number of allocations made.
94    #[must_use]
95    #[inline]
96    pub fn allocation_count(&self) -> usize {
97        self.allocation_count.get()
98    }
99
100    /// Returns the number of chunks in use.
101    #[must_use]
102    #[inline]
103    pub fn chunk_count(&mut self) -> usize {
104        self.inner.iter_allocated_chunks().count()
105    }
106}
107
108impl Default for BumpAllocator {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114/// A scoped bump allocator that automatically resets when dropped.
115///
116/// Useful for allocating temporary data within a scope.
117pub struct ScopedBump<'a> {
118    bump: &'a mut BumpAllocator,
119    start_bytes: usize,
120}
121
122impl<'a> ScopedBump<'a> {
123    /// Creates a new scoped allocator.
124    pub fn new(bump: &'a mut BumpAllocator) -> Self {
125        let start_bytes = bump.allocated_bytes();
126        Self { bump, start_bytes }
127    }
128
129    /// Allocates a value.
130    #[inline]
131    pub fn alloc<T>(&self, value: T) -> &mut T {
132        self.bump.alloc(value)
133    }
134
135    /// Returns the number of bytes allocated in this scope.
136    #[must_use]
137    pub fn scope_allocated_bytes(&self) -> usize {
138        self.bump.allocated_bytes() - self.start_bytes
139    }
140}
141
142impl Drop for ScopedBump<'_> {
143    fn drop(&mut self) {
144        // Note: bumpalo doesn't support partial reset, so this is a no-op.
145        // The memory will be freed when the parent BumpAllocator is reset.
146        // This type is mainly for tracking scope-level allocations.
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_bump_basic_allocation() {
156        let bump = BumpAllocator::new();
157
158        let a = bump.alloc(42u64);
159        assert_eq!(*a, 42);
160
161        let b = bump.alloc(String::from("hello"));
162        assert_eq!(b.as_str(), "hello");
163    }
164
165    #[test]
166    fn test_bump_slice_allocation() {
167        let bump = BumpAllocator::new();
168
169        let slice = bump.alloc_slice_copy(&[1, 2, 3, 4, 5]);
170        assert_eq!(slice, &[1, 2, 3, 4, 5]);
171
172        slice[0] = 10;
173        assert_eq!(slice[0], 10);
174    }
175
176    #[test]
177    fn test_bump_string_allocation() {
178        let bump = BumpAllocator::new();
179
180        let s = bump.alloc_str("hello world");
181        assert_eq!(s, "hello world");
182    }
183
184    #[test]
185    fn test_bump_reset() {
186        let mut bump = BumpAllocator::new();
187
188        for _ in 0..100 {
189            bump.alloc(42u64);
190        }
191
192        let bytes_before = bump.allocated_bytes();
193        assert!(bytes_before > 0);
194        assert_eq!(bump.allocation_count(), 100);
195
196        bump.reset();
197
198        // After reset, allocation count should be 0
199        assert_eq!(bump.allocation_count(), 0);
200    }
201
202    #[test]
203    fn test_bump_with_capacity() {
204        let bump = BumpAllocator::with_capacity(1024);
205        assert_eq!(bump.allocation_count(), 0);
206
207        // Allocate less than capacity
208        for _ in 0..10 {
209            bump.alloc(42u64);
210        }
211
212        assert_eq!(bump.allocation_count(), 10);
213    }
214
215    #[test]
216    fn test_scoped_bump() {
217        let mut bump = BumpAllocator::new();
218
219        bump.alloc(1u64);
220        let outer_allocs = bump.allocation_count();
221
222        {
223            let scope = ScopedBump::new(&mut bump);
224            scope.alloc(2u64);
225            scope.alloc(3u64);
226
227            // Note: bumpalo's allocated_bytes() tracks chunk-level memory, not individual
228            // allocations. It may not increase for small allocations that fit in existing chunks.
229            // We verify functionality through allocation_count instead.
230        }
231
232        // Parent bump should have all allocations (outer + 2 from scope)
233        assert_eq!(bump.allocation_count(), outer_allocs + 2);
234    }
235
236    #[test]
237    fn test_bump_many_small_allocations() {
238        let bump = BumpAllocator::new();
239
240        for i in 0u64..10_000u64 {
241            let v = bump.alloc(i);
242            assert_eq!(*v, i);
243        }
244
245        assert_eq!(bump.allocation_count(), 10_000);
246    }
247}