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}