Skip to main content

grafeo_common/memory/
bump.rs

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