vibesql_executor/
arena.rs

1//! Query-lifetime arena allocator for temporary buffers
2//!
3//! This module provides fast bump-pointer allocation for temporary data
4//! that lives only for the duration of a query execution. Using an arena
5//! dramatically reduces allocation overhead by:
6//!
7//! 1. **Batch Allocation**: Single large allocation serves many requests
8//! 2. **Zero Fragmentation**: Sequential layout, no gaps
9//! 3. **Fast Deallocation**: Reset entire arena in O(1)
10//! 4. **Cache Friendly**: Sequential allocations improve locality
11//!
12//! # Usage
13//!
14//! ```text
15//! let arena = QueryArena::new();
16//!
17//! // Allocate temporary buffers
18//! let bitmap = arena.alloc_bitmap(1024);
19//! bitmap.fill(false);
20//!
21//! // Arena automatically freed when dropped (end of query)
22//! ```
23//!
24//! # Performance
25//!
26//! - Allocation: O(1) bump pointer vs. O(log n) malloc
27//! - Deallocation: O(1) reset vs. O(1) per free
28//! - Memory: ~10% overhead vs. ~15-20% malloc overhead
29//!
30//! # Memory Safety
31//!
32//! Arena allocations are tied to the arena's lifetime. The borrow checker
33//! ensures borrowed data cannot outlive the arena.
34
35use bumpalo::Bump;
36
37/// Query-lifetime arena allocator
38///
39/// Provides fast bump-pointer allocation for temporary buffers during query execution.
40/// All allocations are freed together when the arena is dropped (end of query).
41///
42/// # Thread Safety
43///
44/// `QueryArena` is NOT thread-safe (no Sync/Send). Each thread should have its own
45/// arena for parallel query execution. Use `thread_local!` or pass per-thread.
46///
47/// # Examples
48///
49/// ```text
50/// // Create arena at query start
51/// let arena = QueryArena::new();
52///
53/// // Allocate filter bitmap (fast O(1) bump allocation)
54/// let bitmap = arena.alloc_bitmap(10000);
55/// for i in 0..10000 {
56///     bitmap[i] = evaluate_predicate(i);
57/// }
58///
59/// // Allocate temp buffer for expression results
60/// let results = arena.alloc_values(10000);
61/// for i in 0..10000 {
62///     results[i] = evaluate_expression(i);
63/// }
64///
65/// // Arena automatically freed at end of query (drop)
66/// ```
67pub struct QueryArena {
68    /// Underlying bump allocator
69    bump: Bump,
70}
71
72impl QueryArena {
73    /// Create a new query arena with default capacity (1 MB)
74    ///
75    /// The arena pre-allocates a large chunk to serve many small allocations
76    /// without frequent system calls.
77    #[inline]
78    pub fn new() -> Self {
79        Self::with_capacity(1024 * 1024) // 1 MB default
80    }
81
82    /// Create a new query arena with specified initial capacity
83    ///
84    /// Use this if you have a good estimate of memory needs:
85    /// - Small queries (< 10K rows): 128 KB
86    /// - Medium queries (10K-100K rows): 1 MB
87    /// - Large queries (> 100K rows): 10 MB
88    ///
89    /// Arena will grow automatically if needed.
90    #[inline]
91    pub fn with_capacity(bytes: usize) -> Self {
92        Self {
93            bump: Bump::with_capacity(bytes),
94        }
95    }
96
97    /// Allocate a boolean bitmap with specified length
98    ///
99    /// Common use: Filter bitmaps marking rows that pass predicates
100    ///
101    /// # Example
102    ///
103    /// ```text
104    /// let bitmap = arena.alloc_bitmap(1000);
105    /// for i in 0..1000 {
106    ///     bitmap[i] = row_passes_filter(i);
107    /// }
108    /// ```
109    #[inline]
110    pub fn alloc_bitmap(&self, len: usize) -> &mut [bool] {
111        self.bump.alloc_slice_fill_default(len)
112    }
113
114    /// Allocate a bitmap and initialize with a specific value
115    ///
116    /// More efficient than alloc_bitmap + fill for large bitmaps.
117    ///
118    /// # Example
119    ///
120    /// ```text
121    /// // All rows pass by default, then mark failures
122    /// let bitmap = arena.alloc_bitmap_filled(1000, true);
123    /// for i in failing_rows {
124    ///     bitmap[i] = false;
125    /// }
126    /// ```
127    #[inline]
128    pub fn alloc_bitmap_filled(&self, len: usize, value: bool) -> &mut [bool] {
129        self.bump.alloc_slice_fill_copy(len, value)
130    }
131
132    /// Allocate an array of values
133    ///
134    /// Use for temporary expression results, intermediate calculations, etc.
135    ///
136    /// # Example
137    ///
138    /// ```text
139    /// let temps: &mut [f64] = arena.alloc_slice(1000);
140    /// for i in 0..1000 {
141    ///     temps[i] = calculate(i);
142    /// }
143    /// ```
144    #[inline]
145    pub fn alloc_slice<T: Default + Copy>(&self, len: usize) -> &mut [T] {
146        self.bump.alloc_slice_fill_default(len)
147    }
148
149    /// Allocate an array and initialize with a specific value
150    #[inline]
151    pub fn alloc_slice_filled<T: Copy>(&self, len: usize, value: T) -> &mut [T] {
152        self.bump.alloc_slice_fill_copy(len, value)
153    }
154
155    /// Allocate a single value
156    ///
157    /// Use sparingly - arena allocation is optimized for bulk allocations.
158    /// For single values, normal heap allocation may be comparable.
159    #[inline]
160    pub fn alloc<T>(&self, value: T) -> &mut T {
161        self.bump.alloc(value)
162    }
163
164    /// Allocate a Vec-like collection with specified capacity
165    ///
166    /// The returned Vec will allocate from the arena's chunk, not the heap.
167    ///
168    /// # Example
169    ///
170    /// ```text
171    /// let mut results = arena.alloc_vec();
172    /// for row in rows {
173    ///     if predicate(row) {
174    ///         results.push(row);
175    ///     }
176    /// }
177    /// ```
178    #[inline]
179    pub fn alloc_vec<T>(&self) -> bumpalo::collections::Vec<'_, T> {
180        bumpalo::collections::Vec::new_in(&self.bump)
181    }
182
183    /// Allocate a Vec with specified capacity
184    #[inline]
185    pub fn alloc_vec_with_capacity<T>(&self, capacity: usize) -> bumpalo::collections::Vec<'_, T> {
186        bumpalo::collections::Vec::with_capacity_in(capacity, &self.bump)
187    }
188
189    /// Reset the arena, freeing all allocations at once
190    ///
191    /// This is much faster than freeing individual allocations.
192    /// Use between queries when reusing the same arena.
193    ///
194    /// # Safety
195    ///
196    /// All references to arena-allocated data become invalid after reset.
197    /// The borrow checker prevents this at compile time for normal usage.
198    ///
199    /// # Example
200    ///
201    /// ```text
202    /// let arena = QueryArena::new();
203    ///
204    /// for query in queries {
205    ///     let bitmap = arena.alloc_bitmap(1000);
206    ///     // ... execute query ...
207    ///     arena.reset(); // Free all at once for next query
208    /// }
209    /// ```
210    #[inline]
211    pub fn reset(&mut self) {
212        self.bump.reset();
213    }
214
215    /// Get the number of bytes allocated from the arena
216    ///
217    /// Useful for profiling and capacity tuning.
218    #[inline]
219    pub fn allocated_bytes(&self) -> usize {
220        self.bump.allocated_bytes()
221    }
222}
223
224impl Default for QueryArena {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_basic_allocation() {
236        let arena = QueryArena::new();
237
238        let bitmap = arena.alloc_bitmap(100);
239        assert_eq!(bitmap.len(), 100);
240
241        bitmap[0] = true;
242        bitmap[99] = true;
243        assert!(bitmap[0]);
244        assert!(!bitmap[50]);
245        assert!(bitmap[99]);
246    }
247
248    #[test]
249    fn test_bitmap_filled() {
250        let arena = QueryArena::new();
251
252        let all_true = arena.alloc_bitmap_filled(50, true);
253        assert!(all_true.iter().all(|&b| b));
254
255        let all_false = arena.alloc_bitmap_filled(50, false);
256        assert!(all_false.iter().all(|&b| !b));
257    }
258
259    #[test]
260    fn test_slice_allocation() {
261        let arena = QueryArena::new();
262
263        let ints: &mut [i64] = arena.alloc_slice(10);
264        for (i, val) in ints.iter_mut().enumerate() {
265            *val = i as i64;
266        }
267        assert_eq!(ints[5], 5);
268
269        let floats = arena.alloc_slice_filled(5, 3.5);
270        assert_eq!(floats.len(), 5);
271        assert_eq!(floats[0], 3.5);
272    }
273
274    #[test]
275    fn test_vec_allocation() {
276        let arena = QueryArena::new();
277
278        let mut v = arena.alloc_vec();
279        v.push(1);
280        v.push(2);
281        v.push(3);
282        assert_eq!(v.len(), 3);
283        assert_eq!(v[1], 2);
284    }
285
286    #[test]
287    fn test_reset() {
288        let mut arena = QueryArena::new();
289
290        // First allocation
291        let _bitmap1 = arena.alloc_bitmap(1000);
292        let bytes1 = arena.allocated_bytes();
293        assert!(bytes1 > 0);
294
295        // Reset - bumpalo keeps chunks allocated but resets the bump pointer
296        arena.reset();
297        let bytes2 = arena.allocated_bytes();
298        // After reset, chunks are kept allocated (not freed) for reuse
299        assert_eq!(bytes2, bytes1, "Reset should keep chunks for reuse");
300
301        // Second allocation (reuses space)
302        let _bitmap2 = arena.alloc_bitmap(1000);
303        let bytes3 = arena.allocated_bytes();
304        // Should reuse the same chunk without growing
305        assert_eq!(bytes3, bytes1, "Should reuse existing chunk without growing");
306    }
307
308    #[test]
309    fn test_with_capacity() {
310        let arena = QueryArena::with_capacity(1024);
311        let initial_bytes = arena.allocated_bytes();
312
313        // Bumpalo rounds up capacity to efficient chunk sizes (often page-aligned)
314        // so allocated_bytes() may be larger than requested capacity
315        assert!(initial_bytes >= 1024, "Should allocate at least requested capacity");
316
317        let _bitmap = arena.alloc_bitmap(100);
318        let after_alloc = arena.allocated_bytes();
319
320        // Should not need to grow for this small allocation
321        assert_eq!(after_alloc, initial_bytes, "Small allocation should not grow chunk");
322    }
323
324    #[test]
325    fn test_large_allocation() {
326        let arena = QueryArena::new();
327
328        // Allocate more than default capacity
329        let large = arena.alloc_bitmap(10_000_000); // 10M bools
330        assert_eq!(large.len(), 10_000_000);
331
332        // Arena should have grown automatically
333        assert!(arena.allocated_bytes() >= 10_000_000);
334    }
335}