Skip to main content

radiate_utils/buff/
versioned.rs

1use std::fmt::Debug;
2
3#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
4struct Slot {
5    version: u64,
6    count: u32,
7}
8
9/// A reusable counter keyed by `usize` with O(1) "clear" between sessions.
10///
11/// Storage is positional — `bucket[idx]` holds the count for source index `idx`.
12/// Each session bumps a `version` stamp; slots are considered live only when their
13/// stamp matches the current version, so a session "clear" is a single increment
14/// instead of a vec-wide write.
15///
16/// Designed for hot paths that previously allocated a fresh `HashMap<usize, u32>`
17/// per call. After the first `begin(capacity)` warms the buffer, subsequent
18/// sessions allocate nothing.
19#[derive(Clone)]
20pub struct VersionedCounts {
21    buckets: Vec<Slot>,
22    current: u64,
23}
24
25impl VersionedCounts {
26    pub fn new() -> Self {
27        Self {
28            buckets: Vec::new(),
29            current: 0,
30        }
31    }
32
33    pub fn with_capacity(capacity: usize) -> Self {
34        let mut s = Self::new();
35        s.begin(capacity);
36        s
37    }
38
39    #[inline]
40    pub fn unique_count(&self) -> usize {
41        self.iter_live().count()
42    }
43
44    /// Start a new session. Resizes if needed and bumps the version so all
45    /// previously-touched slots read as stale on the first `bump`.
46    #[inline]
47    pub fn begin(&mut self, capacity: usize) {
48        if self.buckets.len() < capacity {
49            self.buckets.resize(capacity, Slot::default());
50        }
51        self.current = self.current.wrapping_add(1);
52    }
53
54    /// Increment the count at `idx`, returning the new count for this session.
55    /// Stale slots are reset to `1` before counting.
56    #[inline]
57    pub fn bump(&mut self, idx: usize) -> u32 {
58        let slot = &mut self.buckets[idx];
59        if slot.version != self.current {
60            slot.version = self.current;
61            slot.count = 1;
62        } else {
63            slot.count += 1;
64        }
65        slot.count
66    }
67
68    /// Read the count at `idx` for the current session.
69    /// Returns `0` if the slot is out of bounds or stale.
70    #[inline]
71    pub fn get(&self, idx: usize) -> u32 {
72        match self.buckets.get(idx) {
73            Some(s) if s.version == self.current => s.count,
74            _ => 0,
75        }
76    }
77
78    /// Live `(idx, count)` pairs in ascending idx order.
79    #[inline]
80    pub fn iter_live(&self) -> impl Iterator<Item = (usize, u32)> + '_ {
81        let cur = self.current;
82        self.buckets
83            .iter()
84            .enumerate()
85            .filter_map(move |(i, s)| (s.version == cur).then_some((i, s.count)))
86    }
87
88    /// Live `(idx, count)` pairs in descending idx order — convenient for
89    /// callers that need to mutate an indexed collection without invalidating
90    /// not-yet-processed indices (e.g. `Vec::swap_remove`).
91    #[inline]
92    pub fn iter_live_rev(&self) -> impl Iterator<Item = (usize, u32)> + '_ {
93        let cur = self.current;
94        let len = self.buckets.len();
95        (0..len).rev().filter_map(move |i| {
96            let s = &self.buckets[i];
97            (s.version == cur).then_some((i, s.count))
98        })
99    }
100
101    /// Walk both buffers in parallel in descending idx order, yielding
102    /// `(idx, count_self, count_other)` for any idx where at least one side
103    /// is live this session. Slots stale or out-of-bounds on either side
104    /// contribute `0` for that side. Stops at `max(self.len, other.len)`.
105    #[inline]
106    pub fn iter_pair_live_rev<'a>(
107        &'a self,
108        other: &'a Self,
109    ) -> impl Iterator<Item = (usize, u32, u32)> + 'a {
110        let cur_self = self.current;
111        let cur_other = other.current;
112        let len = self.buckets.len().max(other.buckets.len());
113        (0..len).rev().filter_map(move |i| {
114            let a = self
115                .buckets
116                .get(i)
117                .filter(|s| s.version == cur_self)
118                .map(|s| s.count)
119                .unwrap_or(0);
120            let b = other
121                .buckets
122                .get(i)
123                .filter(|s| s.version == cur_other)
124                .map(|s| s.count)
125                .unwrap_or(0);
126            if a == 0 && b == 0 {
127                None
128            } else {
129                Some((i, a, b))
130            }
131        })
132    }
133}
134
135impl Default for VersionedCounts {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141impl Debug for VersionedCounts {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        f.debug_struct("VersionedCounts")
144            .field("capacity", &self.buckets.len())
145            .field("current", &self.current)
146            .field("live", &self.iter_live().collect::<Vec<_>>())
147            .finish()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn fresh_session_is_empty() {
157        let mut c = VersionedCounts::new();
158        c.begin(8);
159        assert_eq!(c.iter_live().count(), 0);
160    }
161
162    #[test]
163    fn bump_counts_per_session() {
164        let mut c = VersionedCounts::new();
165        c.begin(8);
166        assert_eq!(c.bump(3), 1);
167        assert_eq!(c.bump(3), 2);
168        assert_eq!(c.bump(5), 1);
169        assert_eq!(c.bump(3), 3);
170
171        let mut live: Vec<_> = c.iter_live().collect();
172        live.sort_by_key(|(i, _)| *i);
173        assert_eq!(live, vec![(3, 3), (5, 1)]);
174    }
175
176    #[test]
177    fn begin_clears_previous_session() {
178        let mut c = VersionedCounts::new();
179        c.begin(8);
180        c.bump(3);
181        c.bump(7);
182        assert_eq!(c.iter_live().count(), 2);
183
184        c.begin(8);
185        assert_eq!(c.iter_live().count(), 0);
186
187        c.bump(2);
188        let live: Vec<_> = c.iter_live().collect();
189        assert_eq!(live, vec![(2, 1)]);
190    }
191
192    #[test]
193    fn iter_live_rev_yields_descending() {
194        let mut c = VersionedCounts::new();
195        c.begin(10);
196        c.bump(7);
197        c.bump(2);
198        c.bump(5);
199        c.bump(2);
200
201        let rev: Vec<_> = c.iter_live_rev().collect();
202        assert_eq!(rev, vec![(7, 1), (5, 1), (2, 2)]);
203    }
204
205    #[test]
206    fn begin_grows_capacity_but_keeps_history() {
207        let mut c = VersionedCounts::new();
208        c.begin(4);
209        c.bump(0);
210        c.begin(16);
211        assert_eq!(c.iter_live().count(), 0);
212        c.bump(10);
213        assert_eq!(c.iter_live().collect::<Vec<_>>(), vec![(10, 1)]);
214    }
215
216    #[test]
217    fn iter_pair_live_rev_yields_union_descending() {
218        let mut a = VersionedCounts::new();
219        let mut b = VersionedCounts::new();
220        a.begin(10);
221        b.begin(10);
222
223        // overlapping at idx 5; a-only at idx 7; b-only at idx 2.
224        a.bump(5);
225        a.bump(5);
226        a.bump(7);
227        b.bump(5);
228        b.bump(2);
229        b.bump(2);
230        b.bump(2);
231
232        let pairs: Vec<_> = a.iter_pair_live_rev(&b).collect();
233        assert_eq!(pairs, vec![(7, 1, 0), (5, 2, 1), (2, 0, 3)]);
234    }
235
236    #[test]
237    fn iter_pair_live_rev_handles_stale_other() {
238        let mut a = VersionedCounts::new();
239        let mut b = VersionedCounts::new();
240        a.begin(8);
241        b.begin(8);
242        a.bump(4);
243        b.bump(4);
244
245        // Re-begin b without bumping anything — its slots are now stale.
246        b.begin(8);
247
248        let pairs: Vec<_> = a.iter_pair_live_rev(&b).collect();
249        assert_eq!(pairs, vec![(4, 1, 0)]);
250    }
251}