Skip to main content

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}