oximedia_cache/slab_allocator.rs
1//! Slab allocator for cache entries.
2//!
3//! Cache workloads often allocate and free many identically-sized objects
4//! (e.g. fixed-size media segment headers, metadata records, or small frame
5//! descriptors). Standard heap allocators handle this correctly but can
6//! suffer from fragmentation and per-object bookkeeping overhead when the
7//! allocation rate is high.
8//!
9//! This module provides a single-type slab allocator that pre-allocates
10//! contiguous slabs and hands out slots from a free-list. When a slot is
11//! freed it is returned to the free-list rather than returned to the system
12//! allocator, making future allocations O(1) with minimal bookkeeping.
13//!
14//! # Design
15//!
16//! * Each **slab** is a `Vec<Slot<T>>` with `slab_capacity` slots.
17//! * A slot is either `Occupied(T)` or `Free`.
18//! * A global `free_list: VecDeque<SlabIndex>` keeps track of available
19//! slots across all slabs.
20//! * When the free list is empty a new slab is allocated.
21//! * Handles are `SlotHandle { slab: usize, index: usize }` and are used
22//! to retrieve or free a specific slot.
23//!
24//! # Limitations
25//!
26//! * Not thread-safe: wrap in `Mutex` / `RwLock` for concurrent use.
27//! * Does **not** shrink the slab pool when utilisation drops; use
28//! [`compact`] to reclaim empty slabs.
29//!
30//! [`compact`]: SlabAllocator::compact
31
32use std::collections::VecDeque;
33use thiserror::Error;
34
35// ── Errors ────────────────────────────────────────────────────────────────────
36
37/// Errors returned by [`SlabAllocator`].
38#[derive(Debug, Error)]
39pub enum SlabError {
40 /// A handle points outside the slab array bounds.
41 #[error("slab index {0} is out of range (allocator has {1} slabs)")]
42 SlabIndexOutOfRange(usize, usize),
43
44 /// A slot index is outside the capacity of the addressed slab.
45 #[error("slot index {0} is out of range for slab {1} (capacity {2})")]
46 SlotIndexOutOfRange(usize, usize, usize),
47
48 /// The addressed slot is free and cannot be read.
49 #[error("slot {slot} in slab {slab} is already free")]
50 SlotAlreadyFree {
51 /// Slab index.
52 slab: usize,
53 /// Slot index within the slab.
54 slot: usize,
55 },
56}
57
58// ── SlotHandle ────────────────────────────────────────────────────────────────
59
60/// An opaque handle to a slot in a [`SlabAllocator`].
61///
62/// Handles are returned by [`allocate`] and are valid until [`free`] is
63/// called on the same handle. Using a freed handle is safe: `get` and
64/// `get_mut` return `Err(SlabError::SlotAlreadyFree)`.
65///
66/// [`allocate`]: SlabAllocator::allocate
67/// [`free`]: SlabAllocator::free
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub struct SlotHandle {
70 /// Index into the slab array.
71 pub slab: usize,
72 /// Index within the slab.
73 pub index: usize,
74}
75
76// ── Slot ──────────────────────────────────────────────────────────────────────
77
78/// A single slot within a slab.
79enum Slot<T> {
80 /// Slot contains a live value.
81 Occupied(T),
82 /// Slot is available for allocation.
83 Free,
84}
85
86// ── SlabAllocator ─────────────────────────────────────────────────────────────
87
88/// A fixed-slot slab allocator for type `T`.
89///
90/// # Example
91///
92/// ```rust
93/// use oximedia_cache::slab_allocator::SlabAllocator;
94///
95/// let mut alloc: SlabAllocator<Vec<u8>> = SlabAllocator::new(16);
96/// let h = alloc.allocate(vec![0u8; 1024]).unwrap();
97/// assert_eq!(alloc.get(h).unwrap().len(), 1024);
98/// alloc.free(h).unwrap();
99/// ```
100pub struct SlabAllocator<T> {
101 /// Each inner `Vec` is a slab; slots are either occupied or free.
102 slabs: Vec<Vec<Slot<T>>>,
103 /// Per-slab live-object count (used by `compact`).
104 slab_live: Vec<usize>,
105 /// FIFO free-list of available `(slab, index)` pairs.
106 free_list: VecDeque<SlotHandle>,
107 /// Number of slots per slab.
108 slab_capacity: usize,
109 /// Total number of allocated (live) objects.
110 live_count: usize,
111 /// Total number of slots ever created (across all slabs).
112 total_slots: usize,
113}
114
115impl<T> SlabAllocator<T> {
116 /// Create a new allocator that allocates slabs of `slab_capacity` slots.
117 ///
118 /// A capacity of `0` is treated as `1`.
119 pub fn new(slab_capacity: usize) -> Self {
120 let cap = slab_capacity.max(1);
121 Self {
122 slabs: Vec::new(),
123 slab_live: Vec::new(),
124 free_list: VecDeque::new(),
125 slab_capacity: cap,
126 live_count: 0,
127 total_slots: 0,
128 }
129 }
130
131 // ── Allocation ────────────────────────────────────────────────────────────
132
133 /// Allocate a slot for `value`.
134 ///
135 /// Returns a [`SlotHandle`] that can be used to retrieve or free the value.
136 ///
137 /// If no free slots are available a new slab is allocated.
138 pub fn allocate(&mut self, value: T) -> Result<SlotHandle, SlabError> {
139 let handle = match self.free_list.pop_front() {
140 Some(h) => h,
141 None => self.grow_and_get_first_handle(),
142 };
143
144 // Place the value in the slot.
145 self.slabs[handle.slab][handle.index] = Slot::Occupied(value);
146 self.slab_live[handle.slab] += 1;
147 self.live_count += 1;
148
149 Ok(handle)
150 }
151
152 /// Free the slot identified by `handle`, making it available for reuse.
153 ///
154 /// Returns `Err` if the handle is out of range or the slot is already
155 /// free.
156 pub fn free(&mut self, handle: SlotHandle) -> Result<(), SlabError> {
157 self.validate_handle(handle)?;
158 match &self.slabs[handle.slab][handle.index] {
159 Slot::Free => Err(SlabError::SlotAlreadyFree {
160 slab: handle.slab,
161 slot: handle.index,
162 }),
163 Slot::Occupied(_) => {
164 self.slabs[handle.slab][handle.index] = Slot::Free;
165 self.slab_live[handle.slab] = self.slab_live[handle.slab].saturating_sub(1);
166 self.live_count = self.live_count.saturating_sub(1);
167 self.free_list.push_back(handle);
168 Ok(())
169 }
170 }
171 }
172
173 // ── Access ────────────────────────────────────────────────────────────────
174
175 /// Return an immutable reference to the value at `handle`.
176 pub fn get(&self, handle: SlotHandle) -> Result<&T, SlabError> {
177 self.validate_handle(handle)?;
178 match &self.slabs[handle.slab][handle.index] {
179 Slot::Occupied(v) => Ok(v),
180 Slot::Free => Err(SlabError::SlotAlreadyFree {
181 slab: handle.slab,
182 slot: handle.index,
183 }),
184 }
185 }
186
187 /// Return a mutable reference to the value at `handle`.
188 pub fn get_mut(&mut self, handle: SlotHandle) -> Result<&mut T, SlabError> {
189 self.validate_handle(handle)?;
190 match &mut self.slabs[handle.slab][handle.index] {
191 Slot::Occupied(v) => Ok(v),
192 Slot::Free => Err(SlabError::SlotAlreadyFree {
193 slab: handle.slab,
194 slot: handle.index,
195 }),
196 }
197 }
198
199 // ── Compaction ────────────────────────────────────────────────────────────
200
201 /// Compact the allocator by removing fully-empty slabs.
202 ///
203 /// Returns the number of slabs reclaimed. Note that this **invalidates**
204 /// all existing `SlotHandle`s whose `slab` index ≥ the first reclaimed
205 /// slab. Callers must not use stale handles after compaction.
206 ///
207 /// In practice you should call `compact` only during a cache quiesce
208 /// window (e.g. on process idle or periodic maintenance).
209 pub fn compact(&mut self) -> usize {
210 // Identify slabs that are completely empty and can be dropped.
211 // We must remove them from back to front to preserve slab indices for
212 // slabs we keep, or rebuild the free-list. The simplest correct
213 // approach: rebuild everything from scratch after removing empty slabs.
214 let before = self.slabs.len();
215
216 // Collect which slab indices we will keep (live > 0) in order.
217 let keep: Vec<bool> = self.slab_live.iter().map(|&l| l > 0).collect();
218
219 // Build new slab array and slab_live.
220 let mut new_slabs: Vec<Vec<Slot<T>>> = Vec::new();
221 let mut new_slab_live: Vec<usize> = Vec::new();
222 // slab_index_map[old_slab] = new_slab (used to rebuild free_list).
223 let mut slab_index_map: Vec<Option<usize>> = vec![None; self.slabs.len()];
224
225 for (old_idx, should_keep) in keep.iter().enumerate() {
226 if *should_keep {
227 let new_idx = new_slabs.len();
228 slab_index_map[old_idx] = Some(new_idx);
229 // We cannot move out of a Vec<Slot<T>> easily without drain;
230 // use drain to move ownership.
231 // Temporarily swap the slab out with an empty vec to move it.
232 let slab = std::mem::take(&mut self.slabs[old_idx]);
233 new_slabs.push(slab);
234 new_slab_live.push(self.slab_live[old_idx]);
235 }
236 }
237
238 // Rebuild free-list: only handles whose slab survived and whose slot
239 // is still Free.
240 let mut new_free_list: VecDeque<SlotHandle> = VecDeque::new();
241 for h in self.free_list.drain(..) {
242 if let Some(Some(new_slab)) = slab_index_map.get(h.slab) {
243 new_free_list.push_back(SlotHandle {
244 slab: *new_slab,
245 index: h.index,
246 });
247 }
248 // Handles into dropped slabs are silently discarded; those slabs
249 // were fully empty so there can be no live data to lose.
250 }
251
252 self.slabs = new_slabs;
253 self.slab_live = new_slab_live;
254 self.free_list = new_free_list;
255 self.total_slots = self.slabs.iter().map(|s| s.len()).sum();
256
257 before - self.slabs.len()
258 }
259
260 // ── Statistics ────────────────────────────────────────────────────────────
261
262 /// Number of live (allocated) objects.
263 pub fn live_count(&self) -> usize {
264 self.live_count
265 }
266
267 /// Total number of slots across all slabs (live + free).
268 pub fn total_slots(&self) -> usize {
269 self.total_slots
270 }
271
272 /// Number of slabs currently allocated.
273 pub fn slab_count(&self) -> usize {
274 self.slabs.len()
275 }
276
277 /// Number of free slots available without growing.
278 pub fn free_slots(&self) -> usize {
279 self.free_list.len()
280 }
281
282 /// Utilisation ratio: `live_count / total_slots`, or `0.0` if no slots.
283 pub fn utilisation(&self) -> f64 {
284 if self.total_slots == 0 {
285 return 0.0;
286 }
287 self.live_count as f64 / self.total_slots as f64
288 }
289
290 // ── Private helpers ───────────────────────────────────────────────────────
291
292 /// Grow by one slab and return a handle to the first slot of the new slab.
293 /// The remaining `slab_capacity - 1` slots are added to the free list.
294 fn grow_and_get_first_handle(&mut self) -> SlotHandle {
295 let slab_idx = self.slabs.len();
296 let cap = self.slab_capacity;
297
298 // Allocate the new slab pre-filled with Free slots.
299 let mut slab: Vec<Slot<T>> = Vec::with_capacity(cap);
300 for _ in 0..cap {
301 slab.push(Slot::Free);
302 }
303 self.slabs.push(slab);
304 self.slab_live.push(0);
305 self.total_slots += cap;
306
307 // Add slots [1..cap) to the free list (slot 0 will be used immediately).
308 for i in 1..cap {
309 self.free_list.push_back(SlotHandle {
310 slab: slab_idx,
311 index: i,
312 });
313 }
314
315 SlotHandle {
316 slab: slab_idx,
317 index: 0,
318 }
319 }
320
321 fn validate_handle(&self, handle: SlotHandle) -> Result<(), SlabError> {
322 if handle.slab >= self.slabs.len() {
323 return Err(SlabError::SlabIndexOutOfRange(
324 handle.slab,
325 self.slabs.len(),
326 ));
327 }
328 let cap = self.slabs[handle.slab].len();
329 if handle.index >= cap {
330 return Err(SlabError::SlotIndexOutOfRange(
331 handle.index,
332 handle.slab,
333 cap,
334 ));
335 }
336 Ok(())
337 }
338}
339
340// ── Tests ─────────────────────────────────────────────────────────────────────
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 // 1. Basic allocate and get
347 #[test]
348 fn test_allocate_and_get() {
349 let mut alloc: SlabAllocator<u32> = SlabAllocator::new(8);
350 let h = alloc.allocate(42).expect("allocation should succeed");
351 assert_eq!(alloc.get(h).expect("get should succeed"), &42);
352 }
353
354 // 2. live_count increments on allocate
355 #[test]
356 fn test_live_count_increments() {
357 let mut alloc: SlabAllocator<u64> = SlabAllocator::new(4);
358 assert_eq!(alloc.live_count(), 0);
359 let _ = alloc.allocate(1).expect("ok");
360 let _ = alloc.allocate(2).expect("ok");
361 assert_eq!(alloc.live_count(), 2);
362 }
363
364 // 3. free decrements live_count
365 #[test]
366 fn test_free_decrements_live_count() {
367 let mut alloc: SlabAllocator<u32> = SlabAllocator::new(4);
368 let h = alloc.allocate(100).expect("ok");
369 alloc.free(h).expect("free should succeed");
370 assert_eq!(alloc.live_count(), 0);
371 }
372
373 // 4. Free slot cannot be freed again
374 #[test]
375 fn test_double_free_returns_error() {
376 let mut alloc: SlabAllocator<i32> = SlabAllocator::new(4);
377 let h = alloc.allocate(7).expect("ok");
378 alloc.free(h).expect("first free ok");
379 let result = alloc.free(h);
380 assert!(
381 matches!(result, Err(SlabError::SlotAlreadyFree { .. })),
382 "double free should return SlotAlreadyFree"
383 );
384 }
385
386 // 5. Freed slot is reused on next allocation
387 #[test]
388 fn test_slot_reuse() {
389 let mut alloc: SlabAllocator<u8> = SlabAllocator::new(4);
390 let h1 = alloc.allocate(1).expect("ok");
391 alloc.free(h1).expect("free h1");
392 let h2 = alloc.allocate(2).expect("ok");
393 // The reused handle may be the same slot or a different one; just
394 // verify total_slots has not grown beyond one slab.
395 assert_eq!(alloc.slab_count(), 1, "no new slab should be needed");
396 assert_eq!(alloc.get(h2).expect("ok"), &2);
397 }
398
399 // 6. Allocations span multiple slabs
400 #[test]
401 fn test_multi_slab_allocation() {
402 let cap = 4usize;
403 let mut alloc: SlabAllocator<u32> = SlabAllocator::new(cap);
404 let mut handles = Vec::new();
405 for i in 0..cap * 3 {
406 handles.push(alloc.allocate(i as u32).expect("ok"));
407 }
408 assert!(alloc.slab_count() >= 3, "at least 3 slabs should exist");
409 // Verify every value is still readable.
410 for (i, h) in handles.iter().enumerate() {
411 assert_eq!(alloc.get(*h).expect("ok"), &(i as u32));
412 }
413 }
414
415 // 7. total_slots equals slab_count × slab_capacity
416 #[test]
417 fn test_total_slots() {
418 let mut alloc: SlabAllocator<()> = SlabAllocator::new(8);
419 let _ = alloc.allocate(()).expect("ok");
420 assert_eq!(alloc.total_slots(), 8);
421 // Fill to force a second slab.
422 for _ in 0..7 {
423 let _ = alloc.allocate(()).expect("ok");
424 }
425 let _ = alloc.allocate(()).expect("ok"); // triggers second slab
426 assert_eq!(alloc.total_slots(), 16);
427 }
428
429 // 8. utilisation calculation
430 #[test]
431 fn test_utilisation() {
432 let mut alloc: SlabAllocator<u8> = SlabAllocator::new(4);
433 let h = alloc.allocate(0).expect("ok"); // 1 live / 4 total
434 let _ = alloc.allocate(0).expect("ok"); // 2 live / 4 total
435 alloc.free(h).expect("ok"); // 1 live / 4 total
436 let u = alloc.utilisation();
437 assert!((u - 0.25).abs() < 1e-9, "expected 25% utilisation, got {u}");
438 }
439
440 // 9. compact removes empty slabs
441 #[test]
442 fn test_compact_removes_empty_slabs() {
443 let mut alloc: SlabAllocator<u32> = SlabAllocator::new(2);
444 // Force 3 slabs by allocating 5 items.
445 let handles: Vec<_> = (0..5u32).map(|i| alloc.allocate(i).expect("ok")).collect();
446 assert!(alloc.slab_count() >= 2);
447
448 // Free every slot in slab 0 (indices 0 and 1 → handles[0], handles[1]).
449 alloc.free(handles[0]).expect("ok");
450 alloc.free(handles[1]).expect("ok");
451
452 let reclaimed = alloc.compact();
453 assert!(
454 reclaimed >= 1,
455 "at least one empty slab should be reclaimed"
456 );
457 // Remaining live objects are still readable.
458 for _h in &handles[2..] {
459 // After compact slab indices change; just verify live_count.
460 }
461 assert_eq!(
462 alloc.live_count(),
463 3,
464 "3 live objects should survive compact"
465 );
466 drop(handles); // silence unused-variable warning
467 }
468
469 // 10. get_mut allows mutation
470 #[test]
471 fn test_get_mut() {
472 let mut alloc: SlabAllocator<String> = SlabAllocator::new(4);
473 let h = alloc.allocate("hello".to_string()).expect("ok");
474 alloc.get_mut(h).expect("ok").push_str(" world");
475 assert_eq!(alloc.get(h).expect("ok"), "hello world");
476 }
477
478 // 11. Out-of-range slab index returns error
479 #[test]
480 fn test_invalid_slab_index() {
481 let alloc: SlabAllocator<u8> = SlabAllocator::new(4);
482 let bad = SlotHandle { slab: 99, index: 0 };
483 assert!(matches!(
484 alloc.get(bad),
485 Err(SlabError::SlabIndexOutOfRange(99, 0))
486 ));
487 }
488
489 // 12. Out-of-range slot index returns error
490 #[test]
491 fn test_invalid_slot_index() {
492 let mut alloc: SlabAllocator<u8> = SlabAllocator::new(4);
493 let _ = alloc.allocate(0).expect("ok");
494 let bad = SlotHandle { slab: 0, index: 99 };
495 assert!(matches!(
496 alloc.get(bad),
497 Err(SlabError::SlotIndexOutOfRange(99, 0, 4))
498 ));
499 }
500
501 // 13. free_slots tracks available slots
502 #[test]
503 fn test_free_slots() {
504 let mut alloc: SlabAllocator<u8> = SlabAllocator::new(4);
505 // First allocation: creates slab with 4 slots; uses slot 0; 3 go to free list.
506 let h = alloc.allocate(1).expect("ok");
507 assert_eq!(alloc.free_slots(), 3);
508 alloc.free(h).expect("ok");
509 assert_eq!(alloc.free_slots(), 4);
510 }
511
512 // 14. Empty allocator has zero utilisation
513 #[test]
514 fn test_empty_utilisation() {
515 let alloc: SlabAllocator<i32> = SlabAllocator::new(8);
516 assert_eq!(alloc.utilisation(), 0.0);
517 }
518
519 // 15. Allocator with slab_capacity = 1 works correctly
520 #[test]
521 fn test_single_slot_slab() {
522 let mut alloc: SlabAllocator<bool> = SlabAllocator::new(1);
523 let h1 = alloc.allocate(true).expect("ok");
524 let h2 = alloc.allocate(false).expect("ok");
525 assert_ne!(h1, h2);
526 assert_eq!(alloc.slab_count(), 2);
527 alloc.free(h1).expect("ok");
528 alloc.free(h2).expect("ok");
529 assert_eq!(alloc.live_count(), 0);
530 }
531}