Skip to main content

concinnity_memory/
pool.rs

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