Skip to main content

ftui_render/
arena.rs

1//! Per-frame bump arena allocation.
2//!
3//! Provides [`FrameArena`], a thin wrapper around [`bumpalo::Bump`] for
4//! per-frame temporary allocations. The arena is reset at frame boundaries,
5//! eliminating allocator churn on the hot render path.
6//!
7//! # Usage
8//!
9//! ```
10//! use ftui_render::arena::FrameArena;
11//!
12//! let mut arena = FrameArena::new(256 * 1024); // 256 KB initial capacity
13//! let s = arena.alloc_str("hello");
14//! assert_eq!(s, "hello");
15//!
16//! let slice = arena.alloc_slice(&[1u32, 2, 3]);
17//! assert_eq!(slice, &[1, 2, 3]);
18//!
19//! arena.reset(); // O(1) — reclaims all memory for reuse
20//! ```
21//!
22//! # Safety
23//!
24//! This module uses only safe code. `bumpalo::Bump` provides a safe bump
25//! allocator with automatic growth. `reset()` is safe and frees all
26//! allocations, making the memory available for reuse.
27
28use bumpalo::Bump;
29
30/// Default initial capacity for the frame arena (256 KB).
31pub const DEFAULT_ARENA_CAPACITY: usize = 256 * 1024;
32
33/// A per-frame bump allocator for temporary render-path allocations.
34///
35/// `FrameArena` wraps [`bumpalo::Bump`] with a focused API for the common
36/// allocation patterns in the render pipeline: strings, slices, and
37/// single values. All allocations are invalidated on [`reset()`](Self::reset),
38/// which should be called at frame boundaries.
39///
40/// # Capacity
41///
42/// The arena starts with an initial capacity and grows automatically when
43/// exhausted. Growth allocates new chunks from the global allocator but
44/// never moves existing allocations.
45#[derive(Debug)]
46pub struct FrameArena {
47    bump: Bump,
48}
49
50impl FrameArena {
51    /// Create a new arena with the given initial capacity in bytes.
52    ///
53    /// # Panics
54    ///
55    /// Panics if the system allocator cannot fulfill the initial allocation.
56    pub fn new(capacity: usize) -> Self {
57        Self {
58            bump: Bump::with_capacity(capacity),
59        }
60    }
61
62    /// Create a new arena with the default capacity (256 KB).
63    pub fn with_default_capacity() -> Self {
64        Self::new(DEFAULT_ARENA_CAPACITY)
65    }
66
67    /// Reset the arena, reclaiming all memory for reuse.
68    ///
69    /// This is an O(1) operation. All previously allocated references
70    /// are invalidated. The arena retains its allocated chunks for
71    /// future allocations, avoiding repeated system allocator calls.
72    pub fn reset(&mut self) {
73        self.bump.reset();
74    }
75
76    /// Allocate a string slice in the arena.
77    ///
78    /// Returns a reference to the arena-allocated copy of `s`.
79    /// The returned reference is valid until the next [`reset()`](Self::reset).
80    pub fn alloc_str(&self, s: &str) -> &str {
81        self.bump.alloc_str(s)
82    }
83
84    /// Allocate a copy of a slice in the arena.
85    ///
86    /// Returns a reference to the arena-allocated copy of `slice`.
87    /// The returned reference is valid until the next [`reset()`](Self::reset).
88    pub fn alloc_slice<T: Copy>(&self, slice: &[T]) -> &[T] {
89        self.bump.alloc_slice_copy(slice)
90    }
91
92    /// Allocate a single value in the arena, constructed by `f`.
93    ///
94    /// Returns a mutable reference to the arena-allocated value.
95    /// The returned reference is valid until the next [`reset()`](Self::reset).
96    pub fn alloc_with<T, F: FnOnce() -> T>(&self, f: F) -> &mut T {
97        self.bump.alloc_with(f)
98    }
99
100    /// Allocate a single value in the arena.
101    ///
102    /// Returns a mutable reference to the arena-allocated value.
103    /// The returned reference is valid until the next [`reset()`](Self::reset).
104    pub fn alloc<T>(&self, val: T) -> &mut T {
105        self.bump.alloc(val)
106    }
107
108    /// Returns the total bytes allocated in the arena (across all chunks).
109    pub fn allocated_bytes(&self) -> usize {
110        self.bump.allocated_bytes()
111    }
112
113    /// Returns the total bytes of unused capacity in the arena.
114    pub fn allocated_bytes_including_metadata(&self) -> usize {
115        self.bump.allocated_bytes_including_metadata()
116    }
117
118    /// Returns a reference to the underlying [`Bump`] allocator.
119    ///
120    /// Use this for advanced allocation patterns not covered by the
121    /// convenience methods.
122    pub fn as_bump(&self) -> &Bump {
123        &self.bump
124    }
125}
126
127impl Default for FrameArena {
128    fn default() -> Self {
129        Self::with_default_capacity()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn new_creates_arena_with_capacity() {
139        let arena = FrameArena::new(1024);
140        // Should be able to allocate without growing
141        let _s = arena.alloc_str("hello");
142    }
143
144    #[test]
145    fn default_uses_256kb() {
146        let arena = FrameArena::default();
147        let _s = arena.alloc_str("test");
148    }
149
150    #[test]
151    fn alloc_str_returns_correct_content() {
152        let arena = FrameArena::new(4096);
153        let s = arena.alloc_str("hello, world!");
154        assert_eq!(s, "hello, world!");
155    }
156
157    #[test]
158    fn alloc_str_empty() {
159        let arena = FrameArena::new(4096);
160        let s = arena.alloc_str("");
161        assert_eq!(s, "");
162    }
163
164    #[test]
165    fn alloc_str_unicode() {
166        let arena = FrameArena::new(4096);
167        let s = arena.alloc_str("こんにちは 🎉");
168        assert_eq!(s, "こんにちは 🎉");
169    }
170
171    #[test]
172    fn alloc_slice_copies_correctly() {
173        let arena = FrameArena::new(4096);
174        let data = [1u32, 2, 3, 4, 5];
175        let slice = arena.alloc_slice(&data);
176        assert_eq!(slice, &[1, 2, 3, 4, 5]);
177    }
178
179    #[test]
180    fn alloc_slice_empty() {
181        let arena = FrameArena::new(4096);
182        let slice: &[u8] = arena.alloc_slice(&[]);
183        assert!(slice.is_empty());
184    }
185
186    #[test]
187    fn alloc_slice_u8() {
188        let arena = FrameArena::new(4096);
189        let data = b"ANSI escape";
190        let slice = arena.alloc_slice(data.as_slice());
191        assert_eq!(slice, b"ANSI escape");
192    }
193
194    #[test]
195    fn alloc_with_constructs_value() {
196        let arena = FrameArena::new(4096);
197        let val = arena.alloc_with(|| 42u64);
198        assert_eq!(*val, 42);
199    }
200
201    #[test]
202    fn alloc_returns_mutable_ref() {
203        let arena = FrameArena::new(4096);
204        let val = arena.alloc(100i32);
205        assert_eq!(*val, 100);
206        *val = 200;
207        assert_eq!(*val, 200);
208    }
209
210    #[test]
211    fn reset_allows_reuse() {
212        let mut arena = FrameArena::new(4096);
213        let _s1 = arena.alloc_str("first frame data");
214        let bytes_before = arena.allocated_bytes();
215        assert!(bytes_before > 0);
216
217        arena.reset();
218
219        // After reset, new allocations reuse the same memory
220        let _s2 = arena.alloc_str("second frame data");
221    }
222
223    #[test]
224    fn multiple_allocations_coexist() {
225        let arena = FrameArena::new(4096);
226        let s1 = arena.alloc_str("hello");
227        let s2 = arena.alloc_str("world");
228        let slice = arena.alloc_slice(&[1u32, 2, 3]);
229        let val = arena.alloc(42u64);
230
231        // All references remain valid simultaneously
232        assert_eq!(s1, "hello");
233        assert_eq!(s2, "world");
234        assert_eq!(slice, &[1, 2, 3]);
235        assert_eq!(*val, 42);
236    }
237
238    #[test]
239    fn arena_grows_beyond_initial_capacity() {
240        let arena = FrameArena::new(64); // Very small initial capacity
241        // Allocate more than 64 bytes — arena should grow automatically
242        let large = "a]".repeat(100);
243        let s = arena.alloc_str(&large);
244        assert_eq!(s, large);
245    }
246
247    #[test]
248    fn allocated_bytes_tracks_usage() {
249        let arena = FrameArena::new(4096);
250        let initial = arena.allocated_bytes();
251        let _s = arena.alloc_str("some text for tracking");
252        assert!(arena.allocated_bytes() >= initial);
253    }
254
255    #[test]
256    fn as_bump_provides_access() {
257        let arena = FrameArena::new(4096);
258        let bump = arena.as_bump();
259        // Can use bump directly for advanced patterns
260        let val = bump.alloc(99u32);
261        assert_eq!(*val, 99);
262    }
263
264    #[test]
265    fn reset_then_heavy_reuse() {
266        let mut arena = FrameArena::new(4096);
267        for frame in 0..100 {
268            let s = arena.alloc_str(&format!("frame {frame}"));
269            assert!(s.starts_with("frame "));
270            let data: Vec<u32> = (0..50).collect();
271            let slice = arena.alloc_slice(&data);
272            assert_eq!(slice.len(), 50);
273            arena.reset();
274        }
275    }
276
277    #[test]
278    fn debug_impl() {
279        let arena = FrameArena::new(1024);
280        let debug = format!("{arena:?}");
281        assert!(debug.contains("FrameArena"));
282    }
283}