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 { bump: Bump::with_capacity(bytes) }
93 }
94
95 /// Allocate a boolean bitmap with specified length
96 ///
97 /// Common use: Filter bitmaps marking rows that pass predicates
98 ///
99 /// # Example
100 ///
101 /// ```text
102 /// let bitmap = arena.alloc_bitmap(1000);
103 /// for i in 0..1000 {
104 /// bitmap[i] = row_passes_filter(i);
105 /// }
106 /// ```
107 #[inline]
108 pub fn alloc_bitmap(&self, len: usize) -> &mut [bool] {
109 self.bump.alloc_slice_fill_default(len)
110 }
111
112 /// Allocate a bitmap and initialize with a specific value
113 ///
114 /// More efficient than alloc_bitmap + fill for large bitmaps.
115 ///
116 /// # Example
117 ///
118 /// ```text
119 /// // All rows pass by default, then mark failures
120 /// let bitmap = arena.alloc_bitmap_filled(1000, true);
121 /// for i in failing_rows {
122 /// bitmap[i] = false;
123 /// }
124 /// ```
125 #[inline]
126 pub fn alloc_bitmap_filled(&self, len: usize, value: bool) -> &mut [bool] {
127 self.bump.alloc_slice_fill_copy(len, value)
128 }
129
130 /// Allocate an array of values
131 ///
132 /// Use for temporary expression results, intermediate calculations, etc.
133 ///
134 /// # Example
135 ///
136 /// ```text
137 /// let temps: &mut [f64] = arena.alloc_slice(1000);
138 /// for i in 0..1000 {
139 /// temps[i] = calculate(i);
140 /// }
141 /// ```
142 #[inline]
143 pub fn alloc_slice<T: Default + Copy>(&self, len: usize) -> &mut [T] {
144 self.bump.alloc_slice_fill_default(len)
145 }
146
147 /// Allocate an array and initialize with a specific value
148 #[inline]
149 pub fn alloc_slice_filled<T: Copy>(&self, len: usize, value: T) -> &mut [T] {
150 self.bump.alloc_slice_fill_copy(len, value)
151 }
152
153 /// Allocate a single value
154 ///
155 /// Use sparingly - arena allocation is optimized for bulk allocations.
156 /// For single values, normal heap allocation may be comparable.
157 #[inline]
158 pub fn alloc<T>(&self, value: T) -> &mut T {
159 self.bump.alloc(value)
160 }
161
162 /// Allocate a Vec-like collection with specified capacity
163 ///
164 /// The returned Vec will allocate from the arena's chunk, not the heap.
165 ///
166 /// # Example
167 ///
168 /// ```text
169 /// let mut results = arena.alloc_vec();
170 /// for row in rows {
171 /// if predicate(row) {
172 /// results.push(row);
173 /// }
174 /// }
175 /// ```
176 #[inline]
177 pub fn alloc_vec<T>(&self) -> bumpalo::collections::Vec<'_, T> {
178 bumpalo::collections::Vec::new_in(&self.bump)
179 }
180
181 /// Allocate a Vec with specified capacity
182 #[inline]
183 pub fn alloc_vec_with_capacity<T>(&self, capacity: usize) -> bumpalo::collections::Vec<'_, T> {
184 bumpalo::collections::Vec::with_capacity_in(capacity, &self.bump)
185 }
186
187 /// Reset the arena, freeing all allocations at once
188 ///
189 /// This is much faster than freeing individual allocations.
190 /// Use between queries when reusing the same arena.
191 ///
192 /// # Safety
193 ///
194 /// All references to arena-allocated data become invalid after reset.
195 /// The borrow checker prevents this at compile time for normal usage.
196 ///
197 /// # Example
198 ///
199 /// ```text
200 /// let arena = QueryArena::new();
201 ///
202 /// for query in queries {
203 /// let bitmap = arena.alloc_bitmap(1000);
204 /// // ... execute query ...
205 /// arena.reset(); // Free all at once for next query
206 /// }
207 /// ```
208 #[inline]
209 pub fn reset(&mut self) {
210 self.bump.reset();
211 }
212
213 /// Get the number of bytes allocated from the arena
214 ///
215 /// Useful for profiling and capacity tuning.
216 #[inline]
217 pub fn allocated_bytes(&self) -> usize {
218 self.bump.allocated_bytes()
219 }
220}
221
222impl Default for QueryArena {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_basic_allocation() {
234 let arena = QueryArena::new();
235
236 let bitmap = arena.alloc_bitmap(100);
237 assert_eq!(bitmap.len(), 100);
238
239 bitmap[0] = true;
240 bitmap[99] = true;
241 assert!(bitmap[0]);
242 assert!(!bitmap[50]);
243 assert!(bitmap[99]);
244 }
245
246 #[test]
247 fn test_bitmap_filled() {
248 let arena = QueryArena::new();
249
250 let all_true = arena.alloc_bitmap_filled(50, true);
251 assert!(all_true.iter().all(|&b| b));
252
253 let all_false = arena.alloc_bitmap_filled(50, false);
254 assert!(all_false.iter().all(|&b| !b));
255 }
256
257 #[test]
258 fn test_slice_allocation() {
259 let arena = QueryArena::new();
260
261 let ints: &mut [i64] = arena.alloc_slice(10);
262 for (i, val) in ints.iter_mut().enumerate() {
263 *val = i as i64;
264 }
265 assert_eq!(ints[5], 5);
266
267 let floats = arena.alloc_slice_filled(5, 3.5);
268 assert_eq!(floats.len(), 5);
269 assert_eq!(floats[0], 3.5);
270 }
271
272 #[test]
273 fn test_vec_allocation() {
274 let arena = QueryArena::new();
275
276 let mut v = arena.alloc_vec();
277 v.push(1);
278 v.push(2);
279 v.push(3);
280 assert_eq!(v.len(), 3);
281 assert_eq!(v[1], 2);
282 }
283
284 #[test]
285 fn test_reset() {
286 let mut arena = QueryArena::new();
287
288 // First allocation
289 let _bitmap1 = arena.alloc_bitmap(1000);
290 let bytes1 = arena.allocated_bytes();
291 assert!(bytes1 > 0);
292
293 // Reset - bumpalo keeps chunks allocated but resets the bump pointer
294 arena.reset();
295 let bytes2 = arena.allocated_bytes();
296 // After reset, chunks are kept allocated (not freed) for reuse
297 assert_eq!(bytes2, bytes1, "Reset should keep chunks for reuse");
298
299 // Second allocation (reuses space)
300 let _bitmap2 = arena.alloc_bitmap(1000);
301 let bytes3 = arena.allocated_bytes();
302 // Should reuse the same chunk without growing
303 assert_eq!(bytes3, bytes1, "Should reuse existing chunk without growing");
304 }
305
306 #[test]
307 fn test_with_capacity() {
308 let arena = QueryArena::with_capacity(1024);
309 let initial_bytes = arena.allocated_bytes();
310
311 // Bumpalo rounds up capacity to efficient chunk sizes (often page-aligned)
312 // so allocated_bytes() may be larger than requested capacity
313 assert!(initial_bytes >= 1024, "Should allocate at least requested capacity");
314
315 let _bitmap = arena.alloc_bitmap(100);
316 let after_alloc = arena.allocated_bytes();
317
318 // Should not need to grow for this small allocation
319 assert_eq!(after_alloc, initial_bytes, "Small allocation should not grow chunk");
320 }
321
322 #[test]
323 fn test_large_allocation() {
324 let arena = QueryArena::new();
325
326 // Allocate more than default capacity
327 let large = arena.alloc_bitmap(10_000_000); // 10M bools
328 assert_eq!(large.len(), 10_000_000);
329
330 // Arena should have grown automatically
331 assert!(arena.allocated_bytes() >= 10_000_000);
332 }
333}