Skip to main content

concinnity_core/memory/
pool.rs

1// A fixed-capacity pool for objects of one type: storage reserved once, slots
2// handed out and taken back through a free list.
3//
4// The case it exists for is a population that churns but never grows without
5// bound -- spawned entities, in-flight loads, voices. Each of those costs an
6// allocation and a free per item from the global allocator, and the frees leave
7// holes behind; a pool pays for the whole population once and reuses the same
8// slots forever.
9//
10// Handles carry a generation, so a handle to a removed object reads as absent
11// rather than silently addressing whatever took its slot.
12
13use alloc::vec::Vec;
14
15// A slot's occupant, or the emptiness left when it was removed.
16struct Slot<T> {
17    value: Option<T>,
18    // Bumped when a slot is vacated, which is what makes old handles stale.
19    generation: u32,
20}
21
22/// A reference to one object in a pool. Copyable and small: pass it around
23/// instead of the object.
24///
25/// The generation is what makes a handle to a removed object read as absent
26/// rather than silently addressing whatever took its slot.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct PoolHandle {
29    index: u32,
30    generation: u32,
31}
32
33impl PoolHandle {
34    /// Rebuild a handle from parts a caller stored elsewhere.
35    ///
36    /// Parts that never named a live object read as absent, exactly as a
37    /// stale handle does, so this widens no access the pool did not grant.
38    pub const fn from_parts(index: u32, generation: u32) -> Self {
39        Self { index, generation }
40    }
41
42    /// Position in the pool, for a caller keeping a table alongside it.
43    pub const fn index(self) -> usize {
44        self.index as usize
45    }
46
47    /// The generation this handle was minted at.
48    pub const fn generation(self) -> u32 {
49        self.generation
50    }
51}
52
53/// A fixed-capacity slot pool handing out generation-checked handles.
54pub struct Pool<T> {
55    slots: Vec<Slot<T>>,
56    // Vacant slots, most recently vacated first.
57    free: Vec<u32>,
58    len: usize,
59}
60
61impl<T> Pool<T> {
62    /// Reserve room for `capacity` objects. The pool never allocates again: it
63    /// hands out `None` when full rather than growing.
64    pub fn with_capacity(capacity: usize) -> Self {
65        let mut slots = Vec::with_capacity(capacity);
66        let mut free = Vec::with_capacity(capacity);
67        for index in 0..capacity {
68            slots.push(Slot {
69                value: None,
70                generation: 0,
71            });
72            // Reversed, so the first insert takes slot 0 and a fresh pool fills
73            // in order.
74            free.push((capacity - 1 - index) as u32);
75        }
76        Self {
77            slots,
78            free,
79            len: 0,
80        }
81    }
82
83    /// Slots the pool reserved.
84    pub fn capacity(&self) -> usize {
85        self.slots.len()
86    }
87
88    /// Live objects.
89    pub fn len(&self) -> usize {
90        self.len
91    }
92
93    /// Whether the pool holds no live objects.
94    pub fn is_empty(&self) -> bool {
95        self.len == 0
96    }
97
98    #[cfg(test)]
99    pub(crate) fn is_full(&self) -> bool {
100        self.free.is_empty()
101    }
102
103    /// Bytes the pool reserved, occupied or not: what it costs the process
104    /// whatever its occupancy, and so what it reports to a byte budget.
105    pub fn reserved_bytes(&self) -> u64 {
106        (self.capacity() * size_of::<Slot<T>>()) as u64
107    }
108
109    /// Place `value` in a free slot. `None` when the pool is full, which is the
110    /// caller's cue to drop the request or widen the pool at setup.
111    pub fn insert(&mut self, value: T) -> Option<PoolHandle> {
112        let index = self.free.pop()?;
113        let slot = &mut self.slots[index as usize];
114        slot.value = Some(value);
115        self.len += 1;
116        Some(PoolHandle {
117            index,
118            generation: slot.generation,
119        })
120    }
121
122    /// Take the object back out, freeing its slot. `None` when the handle is
123    /// stale or already removed.
124    pub fn remove(&mut self, handle: PoolHandle) -> Option<T> {
125        let slot = self.slots.get_mut(handle.index as usize)?;
126        if slot.generation != handle.generation {
127            return None;
128        }
129        let value = slot.value.take()?;
130        slot.generation = slot.generation.wrapping_add(1);
131        self.free.push(handle.index);
132        self.len -= 1;
133        Some(value)
134    }
135
136    /// Borrow the object a handle names, if the handle is still live.
137    pub fn get(&self, handle: PoolHandle) -> Option<&T> {
138        let slot = self.slots.get(handle.index as usize)?;
139        (slot.generation == handle.generation).then_some(slot.value.as_ref()?)
140    }
141
142    /// Mutably borrow the object a handle names, if it is still live.
143    pub fn get_mut(&mut self, handle: PoolHandle) -> Option<&mut T> {
144        let slot = self.slots.get_mut(handle.index as usize)?;
145        if slot.generation != handle.generation {
146            return None;
147        }
148        slot.value.as_mut()
149    }
150
151    /// Borrow whatever occupies a slot, by position rather than by handle.
152    ///
153    /// For a caller that keeps its own tables alongside the pool and indexes
154    /// them by slot: the position came from the pool, so re-checking a
155    /// generation it never left would only cost a branch.
156    pub fn get_at(&self, index: usize) -> Option<&T> {
157        self.slots.get(index)?.value.as_ref()
158    }
159
160    /// Mutably borrow whatever occupies a slot, by position.
161    pub fn get_at_mut(&mut self, index: usize) -> Option<&mut T> {
162        self.slots.get_mut(index)?.value.as_mut()
163    }
164
165    /// The handle naming whatever occupies a slot, so a caller that walks
166    /// positions can hand one back out. `None` when the slot is vacant.
167    pub fn handle_at(&self, index: usize) -> Option<PoolHandle> {
168        let slot = self.slots.get(index)?;
169        slot.value.as_ref()?;
170        Some(PoolHandle {
171            index: index as u32,
172            generation: slot.generation,
173        })
174    }
175
176    /// Whether a handle still names a live object.
177    pub fn contains(&self, handle: PoolHandle) -> bool {
178        self.get(handle).is_some()
179    }
180
181    /// Every live object with its handle, in slot order.
182    pub fn iter(&self) -> impl Iterator<Item = (PoolHandle, &T)> {
183        self.slots.iter().enumerate().filter_map(|(index, slot)| {
184            let value = slot.value.as_ref()?;
185            Some((
186                PoolHandle {
187                    index: index as u32,
188                    generation: slot.generation,
189                },
190                value,
191            ))
192        })
193    }
194
195    /// Every live object with its handle, mutably, in slot order.
196    pub fn iter_mut(&mut self) -> impl Iterator<Item = (PoolHandle, &mut T)> {
197        self.slots
198            .iter_mut()
199            .enumerate()
200            .filter_map(|(index, slot)| {
201                let generation = slot.generation;
202                let value = slot.value.as_mut()?;
203                Some((
204                    PoolHandle {
205                        index: index as u32,
206                        generation,
207                    },
208                    value,
209                ))
210            })
211    }
212
213    /// Drop every occupant, keeping the reserved storage. Outstanding handles go
214    /// stale, as they would if each object were removed individually.
215    pub fn clear(&mut self) {
216        self.free.clear();
217        for (index, slot) in self.slots.iter_mut().enumerate().rev() {
218            if slot.value.take().is_some() {
219                slot.generation = slot.generation.wrapping_add(1);
220            }
221            self.free.push(index as u32);
222        }
223        self.len = 0;
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn inserts_read_back_through_their_handles() {
233        let mut pool = Pool::with_capacity(4);
234        let a = pool.insert("a").expect("room");
235        let b = pool.insert("b").expect("room");
236
237        assert_eq!(pool.get(a), Some(&"a"));
238        assert_eq!(pool.get(b), Some(&"b"));
239        assert_eq!(pool.len(), 2);
240        assert_eq!(pool.capacity(), 4);
241    }
242
243    #[test]
244    fn a_fresh_pool_fills_its_slots_in_order() {
245        let mut pool = Pool::with_capacity(3);
246        for expected in 0..3 {
247            assert_eq!(pool.insert(expected).expect("room").index(), expected);
248        }
249    }
250
251    #[test]
252    fn removal_frees_the_slot_for_reuse() {
253        let mut pool = Pool::with_capacity(2);
254        let a = pool.insert(1).expect("room");
255        let b = pool.insert(2).expect("room");
256        assert!(pool.is_full());
257
258        assert_eq!(pool.remove(a), Some(1));
259        assert_eq!(pool.len(), 1);
260        let c = pool.insert(3).expect("the freed slot");
261        assert_eq!(c.index(), a.index(), "the vacated slot is reused");
262        assert_eq!(pool.get(b), Some(&2));
263        assert_eq!(pool.get(c), Some(&3));
264    }
265
266    // A caller walking positions has to be able to hand a handle back out,
267    // and the handle it gets must be the one the pool minted.
268    #[test]
269    fn a_slot_hands_back_the_handle_naming_its_occupant() {
270        let mut pool = Pool::with_capacity(2);
271        let a = pool.insert("a").expect("room");
272        assert_eq!(pool.handle_at(a.index()), Some(a));
273        assert_eq!(pool.handle_at(1), None, "vacant");
274        assert_eq!(pool.handle_at(99), None, "out of range");
275
276        pool.remove(a);
277        assert_eq!(pool.handle_at(a.index()), None);
278        let b = pool.insert("b").expect("the freed slot");
279        assert_eq!(pool.handle_at(b.index()), Some(b));
280        assert_ne!(
281            pool.handle_at(b.index()),
282            Some(a),
283            "the generation moved on"
284        );
285    }
286
287    // The point of the generation: a handle to a removed object must not reach
288    // whatever took its slot.
289    #[test]
290    fn a_stale_handle_does_not_reach_the_slots_new_occupant() {
291        let mut pool = Pool::with_capacity(1);
292        let old = pool.insert("first").expect("room");
293        assert_eq!(pool.remove(old), Some("first"));
294        let new = pool.insert("second").expect("the freed slot");
295
296        assert_eq!(new.index(), old.index());
297        assert_eq!(pool.get(old), None);
298        assert!(!pool.contains(old));
299        assert_eq!(pool.get_mut(old), None);
300        assert_eq!(pool.remove(old), None);
301        assert_eq!(pool.get(new), Some(&"second"));
302    }
303
304    // Slot access is for callers holding their own table: it must reach the
305    // live occupant and report an empty or out-of-range slot as absent.
306    #[test]
307    fn slot_access_reaches_the_occupant_and_skips_the_vacancies() {
308        let mut pool = Pool::with_capacity(3);
309        let a = pool.insert(1).expect("room");
310        let b = pool.insert(2).expect("room");
311        assert_eq!(pool.get_at(a.index()), Some(&1));
312        assert_eq!(pool.get_at(b.index()), Some(&2));
313        assert_eq!(pool.get_at(2), None);
314        assert_eq!(pool.get_at(99), None);
315
316        *pool.get_at_mut(b.index()).expect("live") = 20;
317        assert_eq!(pool.get(b), Some(&20));
318        pool.remove(a);
319        assert_eq!(pool.get_at(a.index()), None);
320        assert_eq!(pool.get_at_mut(99), None);
321    }
322
323    #[test]
324    fn a_full_pool_declines_rather_than_growing() {
325        let mut pool = Pool::with_capacity(2);
326        assert!(pool.insert(1).is_some());
327        assert!(pool.insert(2).is_some());
328        assert!(pool.insert(3).is_none());
329        assert_eq!(pool.capacity(), 2);
330        assert_eq!(pool.len(), 2);
331    }
332
333    #[test]
334    fn a_zero_capacity_pool_holds_nothing() {
335        let mut pool = Pool::with_capacity(0);
336        assert!(pool.is_full());
337        assert!(pool.insert(1).is_none());
338        assert_eq!(pool.iter().count(), 0);
339    }
340
341    #[test]
342    fn objects_can_be_mutated_in_place() {
343        let mut pool = Pool::with_capacity(2);
344        let h = pool.insert(10).expect("room");
345        *pool.get_mut(h).expect("live") += 5;
346        assert_eq!(pool.get(h), Some(&15));
347
348        for (_, value) in pool.iter_mut() {
349            *value *= 2;
350        }
351        assert_eq!(pool.get(h), Some(&30));
352    }
353
354    #[test]
355    fn iteration_visits_live_objects_only() {
356        let mut pool = Pool::with_capacity(4);
357        let a = pool.insert(1).expect("room");
358        let _b = pool.insert(2).expect("room");
359        let c = pool.insert(3).expect("room");
360        pool.remove(a);
361
362        let live: alloc::vec::Vec<i32> = pool.iter().map(|(_, v)| *v).collect();
363        assert_eq!(live, [2, 3]);
364        // Handles from iteration address the objects they were read from.
365        let (handle, _) = pool.iter().next().expect("a live object");
366        assert_eq!(pool.get(handle), Some(&2));
367        assert_eq!(pool.get(c), Some(&3));
368    }
369
370    #[test]
371    fn clear_empties_the_pool_and_stales_its_handles() {
372        let mut pool = Pool::with_capacity(3);
373        let a = pool.insert(1).expect("room");
374        let b = pool.insert(2).expect("room");
375        pool.clear();
376
377        assert!(pool.is_empty());
378        assert_eq!(pool.capacity(), 3);
379        assert_eq!(pool.get(a), None);
380        assert_eq!(pool.get(b), None);
381        // And the storage is all available again, in order.
382        assert_eq!(pool.insert(9).expect("room").index(), 0);
383    }
384
385    // A handle a caller stored as parts and rebuilt must still address the same
386    // object, and a rebuilt handle whose parts never named one must not.
387    #[test]
388    fn handles_rebuild_from_their_parts() {
389        let mut pool = Pool::with_capacity(2);
390        let a = pool.insert("a").expect("room");
391        let rebuilt = PoolHandle::from_parts(a.index() as u32, a.generation());
392        assert_eq!(rebuilt, a);
393        assert_eq!(pool.get(rebuilt), Some(&"a"));
394
395        assert_eq!(pool.get(PoolHandle::from_parts(0, 7)), None);
396        assert_eq!(pool.get(PoolHandle::from_parts(99, 0)), None);
397    }
398
399    #[test]
400    fn a_reused_slot_reports_a_later_generation() {
401        let mut pool = Pool::with_capacity(1);
402        let first = pool.insert(1).expect("room");
403        assert_eq!(first.generation(), 0);
404        pool.remove(first);
405        let second = pool.insert(2).expect("the freed slot");
406        assert_eq!(second.index(), first.index());
407        assert_eq!(second.generation(), 1);
408    }
409
410    // The pool's cost is its reservation, not its occupancy: that is the figure
411    // a byte budget needs.
412    #[test]
413    fn reserved_bytes_counts_the_reservation_not_the_occupancy() {
414        let mut pool = Pool::<u64>::with_capacity(16);
415        let reserved = pool.reserved_bytes();
416        assert!(reserved >= 16 * size_of::<u64>() as u64);
417        pool.insert(1);
418        assert_eq!(pool.reserved_bytes(), reserved);
419    }
420
421    // Dropping the pool must drop its occupants, not leak them.
422    #[test]
423    fn occupants_are_dropped_with_the_pool() {
424        use alloc::rc::Rc;
425
426        let witness = Rc::new(());
427        {
428            let mut pool = Pool::with_capacity(2);
429            pool.insert(Rc::clone(&witness));
430            assert_eq!(Rc::strong_count(&witness), 2);
431        }
432        assert_eq!(Rc::strong_count(&witness), 1);
433    }
434
435    // And so must removing one.
436    #[test]
437    fn a_removed_occupant_is_handed_back_intact() {
438        use alloc::rc::Rc;
439
440        let witness = Rc::new(());
441        let mut pool = Pool::with_capacity(2);
442        let h = pool.insert(Rc::clone(&witness)).expect("room");
443        let taken = pool.remove(h).expect("live");
444        assert_eq!(Rc::strong_count(&witness), 2);
445        drop(taken);
446        assert_eq!(Rc::strong_count(&witness), 1);
447    }
448}