Skip to main content

spg_storage/
posting.rs

1//! Posting lists — the locator sequence stored under one index key.
2//!
3//! # Why this is not a `Vec<RowLocator>`
4//!
5//! Index maps are [`crate::PersistentBTreeMap`]s: copy-on-write B-trees
6//! whose nodes are shared behind `Arc`. Writing to one walks
7//! `Arc::make_mut` down the spine, and a node that is still shared with a
8//! reader gets copied — entries and all. `BNode::clone` deep-copies its
9//! values, so with a plain `Vec` the copy carries every locator under
10//! every key in that node.
11//!
12//! Round 1028 counted the two halves of that on the mailrs import:
13//! 13,194,459 posting-list appends against 16,343 node clones. Eight
14//! hundred and seven appends per clone — `Arc::make_mut` finds the node
15//! uniquely owned almost always, so a statement copies a node once on
16//! first touch and the rest of its appends land in place. The copying is
17//! therefore charged per (node, statement), and at roughly half a megabyte
18//! a node it came to about 8 GB of the import's 15.2 GB of allocation.
19//!
20//! An earlier attempt (round 1026) put the whole list behind an `Arc` so
21//! the node clone would copy pointers. It measured as a null result, and
22//! this shape explains why: the first append then calls `Arc::make_mut` on
23//! the LIST, copying it in full. The copy moved from node granularity to
24//! list granularity and a statement touches most of a node's lists anyway.
25//!
26//! # The shape
27//!
28//! A full prefix of shared blocks plus a short open tail:
29//!
30//! ```text
31//!   frozen: [Arc<[L; 256]>] [Arc<[L; 256]>] [Arc<[L; 256]>]
32//!   tail:   [L, L, L]                        <- < BLOCK, owned
33//! ```
34//!
35//! Cloning copies the block POINTERS and the tail. The tail is bounded by
36//! `BLOCK`, so a clone costs a few kilobytes however long the list is, and
37//! — unlike the `Arc<Vec<_>>` shape — appending afterwards needs no
38//! copy at all: the tail is already owned. Pushing past `BLOCK` freezes
39//! the tail into a new shared block, which the next clone will only
40//! point at.
41//!
42//! # Cost
43//!
44//! Appending is amortised `O(1)`; reading is a pointer hop every `BLOCK`
45//! locators. An empty list allocates nothing (both `Vec`s start empty), so
46//! the per-key overhead against a bare `Vec` is the second `Vec` header —
47//! 24 bytes — paid on every distinct key in exchange for the copy bound.
48
49use alloc::sync::Arc;
50use alloc::vec::Vec;
51
52use crate::row_locator::RowLocator;
53
54/// Locators per frozen block.
55///
56/// This is the clone bound: a copy carries at most this many locators
57/// (`BLOCK * 16` bytes ≈ 4 KB) plus one pointer per frozen block. Larger
58/// wastes more per clone; smaller spends more pointers and more `Arc`
59/// traffic per read.
60const BLOCK: usize = 256;
61
62/// The locators stored under one index key. See the module docs for why
63/// this is not a `Vec`.
64#[derive(Clone, Debug, Default, PartialEq, Eq)]
65pub struct PostingList {
66    /// Full blocks, shared. Every one holds exactly `BLOCK` locators, so
67    /// the length is derivable and is not stored.
68    frozen: Vec<Arc<[RowLocator]>>,
69    /// The open block: owned, always shorter than `BLOCK`.
70    tail: Vec<RowLocator>,
71}
72
73impl PostingList {
74    /// An empty list. Allocates nothing.
75    #[must_use]
76    pub const fn new() -> Self {
77        Self {
78            frozen: Vec::new(),
79            tail: Vec::new(),
80        }
81    }
82
83    /// A list holding one locator — the shape every new index key starts
84    /// in.
85    #[must_use]
86    pub fn single(locator: RowLocator) -> Self {
87        Self {
88            frozen: Vec::new(),
89            tail: alloc::vec![locator],
90        }
91    }
92
93    /// Append one locator.
94    pub fn push(&mut self, locator: RowLocator) {
95        self.tail.push(locator);
96        if self.tail.len() >= BLOCK {
97            let full = core::mem::take(&mut self.tail);
98            self.frozen.push(Arc::from(full.into_boxed_slice()));
99        }
100    }
101
102    /// Number of locators.
103    #[must_use]
104    pub fn len(&self) -> usize {
105        self.frozen.len() * BLOCK + self.tail.len()
106    }
107
108    /// Whether the list holds no locators.
109    #[must_use]
110    pub fn is_empty(&self) -> bool {
111        self.frozen.is_empty() && self.tail.is_empty()
112    }
113
114    /// The locators in insertion order.
115    #[must_use]
116    pub fn iter(&self) -> Iter<'_> {
117        Iter {
118            list: self,
119            block: 0,
120            pos: 0,
121        }
122    }
123
124    /// The locators in insertion order, by value.
125    pub fn iter_copied(&self) -> impl Iterator<Item = RowLocator> + '_ {
126        self.iter().copied()
127    }
128
129    /// The first locator, if any.
130    #[must_use]
131    pub fn first(&self) -> Option<RowLocator> {
132        self.frozen
133            .first()
134            .and_then(|b| b.first().copied())
135            .or_else(|| self.tail.first().copied())
136    }
137
138    /// The last locator, if any.
139    #[must_use]
140    pub fn last(&self) -> Option<RowLocator> {
141        self.tail
142            .last()
143            .copied()
144            .or_else(|| self.frozen.last().and_then(|b| b.last().copied()))
145    }
146
147    /// Whether `locator` appears in the list.
148    #[must_use]
149    pub fn contains(&self, locator: RowLocator) -> bool {
150        self.iter().any(|l| *l == locator)
151    }
152
153    /// Keep only the locators `keep` accepts, rebuilding the blocks.
154    ///
155    /// Rebuilds rather than edits in place: the frozen blocks are shared,
156    /// so dropping from the middle of one would copy it anyway.
157    ///
158    /// The common call drops nothing, and that case must not allocate — the
159    /// insert path prunes a key's dead versions every time its list reaches
160    /// a power-of-two length. So the list is tested first and only rebuilt
161    /// if something is actually going. `keep` therefore sees a retained
162    /// locator TWICE, which is why it is `Fn` and not `FnMut`: a predicate
163    /// here has to be pure.
164    pub fn retain(&mut self, keep: impl Fn(RowLocator) -> bool) {
165        if self.iter().all(|l| keep(*l)) {
166            return;
167        }
168        let kept: Self = self.iter().copied().filter(|l| keep(*l)).collect();
169        *self = kept;
170    }
171
172    /// Collect into a flat `Vec`, for callers that need contiguity.
173    #[must_use]
174    pub fn to_vec(&self) -> Vec<RowLocator> {
175        let mut out = Vec::with_capacity(self.len());
176        out.extend(self.iter().copied());
177        out
178    }
179}
180
181/// Walks a [`PostingList`]'s frozen blocks and then its tail.
182///
183/// Hand-written rather than a `flat_map(..).chain(..)`: the read paths
184/// iterate posting lists per row, and a boxed or deeply-nested adaptor
185/// would put an allocation or a layer of indirection there.
186#[derive(Debug)]
187pub struct Iter<'a> {
188    list: &'a PostingList,
189    /// Index into `frozen`; equal to its length once the tail is being
190    /// walked.
191    block: usize,
192    /// Offset within the current block (or within the tail).
193    pos: usize,
194}
195
196impl<'a> Iterator for Iter<'a> {
197    type Item = &'a RowLocator;
198
199    fn next(&mut self) -> Option<&'a RowLocator> {
200        while self.block < self.list.frozen.len() {
201            let block = &self.list.frozen[self.block];
202            if let Some(locator) = block.get(self.pos) {
203                self.pos += 1;
204                return Some(locator);
205            }
206            self.block += 1;
207            self.pos = 0;
208        }
209        let locator = self.list.tail.get(self.pos)?;
210        self.pos += 1;
211        Some(locator)
212    }
213
214    fn size_hint(&self) -> (usize, Option<usize>) {
215        let seen = self.block * BLOCK + self.pos;
216        let left = self.list.len().saturating_sub(seen);
217        (left, Some(left))
218    }
219}
220
221impl ExactSizeIterator for Iter<'_> {}
222
223impl FromIterator<RowLocator> for PostingList {
224    fn from_iter<I: IntoIterator<Item = RowLocator>>(iter: I) -> Self {
225        let mut out = Self::new();
226        for locator in iter {
227            out.push(locator);
228        }
229        out
230    }
231}
232
233/// Compare against a flat sequence, so a caller holding an expected
234/// order does not have to know how the list is blocked internally.
235impl PartialEq<[RowLocator]> for PostingList {
236    fn eq(&self, other: &[RowLocator]) -> bool {
237        self.len() == other.len() && self.iter().zip(other).all(|(a, b)| a == b)
238    }
239}
240
241impl<const N: usize> PartialEq<[RowLocator; N]> for PostingList {
242    fn eq(&self, other: &[RowLocator; N]) -> bool {
243        *self == other[..]
244    }
245}
246
247impl From<Vec<RowLocator>> for PostingList {
248    fn from(v: Vec<RowLocator>) -> Self {
249        v.into_iter().collect()
250    }
251}
252
253impl<'a> IntoIterator for &'a PostingList {
254    type Item = &'a RowLocator;
255    type IntoIter = alloc::boxed::Box<dyn Iterator<Item = &'a RowLocator> + 'a>;
256
257    fn into_iter(self) -> Self::IntoIter {
258        alloc::boxed::Box::new(self.iter())
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::{BLOCK, PostingList};
265    use crate::row_locator::RowLocator;
266
267    fn hot(i: usize) -> RowLocator {
268        RowLocator::Hot(i)
269    }
270
271    #[test]
272    fn empty_list_allocates_nothing_and_reads_empty() {
273        let list = PostingList::new();
274        assert!(list.is_empty());
275        assert_eq!(list.len(), 0);
276        assert_eq!(list.iter().count(), 0);
277        assert_eq!(list.last(), None);
278    }
279
280    #[test]
281    fn order_and_length_survive_block_boundaries() {
282        // Spans three block boundaries so the frozen/tail split is
283        // exercised on both sides of each.
284        let n = BLOCK * 3 + 7;
285        let list: PostingList = (0..n).map(hot).collect();
286        assert_eq!(list.len(), n);
287        let read: alloc::vec::Vec<_> = list.iter().copied().collect();
288        assert_eq!(read, (0..n).map(hot).collect::<alloc::vec::Vec<_>>());
289        assert_eq!(list.last(), Some(hot(n - 1)));
290    }
291
292    #[test]
293    fn length_is_exact_at_a_block_boundary() {
294        // The boundary case the derived length gets wrong if `push`
295        // freezes late: at exactly BLOCK the tail must be empty.
296        let list: PostingList = (0..BLOCK).map(hot).collect();
297        assert_eq!(list.len(), BLOCK);
298        assert_eq!(list.iter().count(), BLOCK);
299        assert_eq!(list.last(), Some(hot(BLOCK - 1)));
300    }
301
302    #[test]
303    fn a_clone_does_not_see_later_appends() {
304        // The property the whole shape exists for: frozen blocks are
305        // shared, so a clone must still be a snapshot.
306        let mut original: PostingList = (0..BLOCK * 2).map(hot).collect();
307        let snapshot = original.clone();
308        original.push(hot(9999));
309        assert_eq!(snapshot.len(), BLOCK * 2);
310        assert_eq!(original.len(), BLOCK * 2 + 1);
311        assert_eq!(snapshot.last(), Some(hot(BLOCK * 2 - 1)));
312    }
313
314    #[test]
315    fn retain_rebuilds_across_blocks() {
316        let mut list: PostingList = (0..BLOCK * 2 + 5).map(hot).collect();
317        list.retain(|l| matches!(l, RowLocator::Hot(i) if i % 2 == 0));
318        let read: alloc::vec::Vec<_> = list.iter().copied().collect();
319        let want: alloc::vec::Vec<_> = (0..BLOCK * 2 + 5).filter(|i| i % 2 == 0).map(hot).collect();
320        assert_eq!(read, want);
321        assert_eq!(list.len(), want.len());
322    }
323
324    #[test]
325    fn retain_keeping_everything_leaves_the_list_alone() {
326        let list: PostingList = (0..BLOCK + 3).map(hot).collect();
327        let mut same = list.clone();
328        same.retain(|_| true);
329        assert_eq!(same, list);
330    }
331
332    #[test]
333    fn retain_can_empty_the_list() {
334        let mut list: PostingList = (0..BLOCK + 3).map(hot).collect();
335        list.retain(|_| false);
336        assert!(list.is_empty());
337        assert_eq!(list.len(), 0);
338    }
339}