Skip to main content

rudb_vector/
buffer.rs

1//! The run of values behind a flat vector, and the seam the buffer manager arrives through.
2//!
3//! `spec/engine/03-data-plane.md` section 3.8. Today every vector owns its payload and every scan
4//! allocates, which is the right place to start and is not where this ends. At layer three the scan
5//! reads a page out of the buffer manager and the vector wants to point into that page rather than
6//! copy out of it, and the copy it avoids is the largest single copy in the system, because it is
7//! every byte of every column every query reads.
8//!
9//! The type that supports both is one enum holding either an owned run or a borrowed one, and the
10//! part that has to be decided correctly the first time is how the borrow is expressed, because that
11//! shows up in every signature that mentions a vector.
12//!
13//! # Why the pin is a handle and not a lifetime
14//!
15//! The obvious way to express a borrow in Rust is a lifetime parameter, and it is the wrong one
16//! here. A lifetime on [`Buffer`] is a lifetime on [`Data`](crate::Data), which is a lifetime on
17//! [`Vector`](crate::Vector), which is a lifetime on [`Chunk`](crate::Chunk), which is a lifetime on
18//! every operator's state, on every trait object in the pipeline, and on every queue a chunk is put
19//! into for another thread to pick up. The scheduler is exactly that last thing, so the borrow would
20//! have to outlive a hand off between threads that the compiler has no way to see the end of. The
21//! two ways out of that are unsafe code and a copy at the boundary, and the copy at the boundary is
22//! the thing the borrow existed to avoid.
23//!
24//! So the pin is a [`Pin`], a reference counted handle the buffer holds, and the page stays alive
25//! because the handle is alive rather than because a region ends. It costs one atomic increment per
26//! vector construction, which is not measurable next to reading the page it is protecting, and the
27//! ownership story stays uniform: a chunk is `Send`, always, whatever its columns are pointing at.
28//!
29//! # Why it landed with one variant
30//!
31//! There was no buffer manager, so there was nothing to borrow from, and writing the borrowed
32//! variant then would have been writing an interface against an imaginary caller. What landed was
33//! the enum, with only the owned variant in it, so that adding a variant later is a change inside
34//! this crate rather than a change to every signature in the workspace. The reader side of that
35//! migration was already done by the [`Deref`] below: everything outside this crate reads a slice,
36//! and a slice is what every variant hands back. The section after this one is that bet being
37//! collected, and it cost two functions.
38//!
39//! The writer side is [`Buffer::to_mut`], which is the one function that has to grow a case. A write
40//! through a borrowed buffer has to copy the page into an owned run first, which is what `Cow` does
41//! and for the same reason, and having the call site named now means that day is a change to one
42//! function rather than a search for every `push`.
43//!
44//! # The second variant arrived early, and from the other direction
45//!
46//! The shared variant is here before the buffer manager is, because the Parquet reader needed the
47//! same thing for a different reason. A string column is built over the page it was decoded from
48//! rather than copying out of it, so the page becomes the column's arena and goes downstream with
49//! it, and the reader never gets the allocation back. On `hits` that is ten and a half megabytes a
50//! row group, freed and taken again for every row group, and glibc hands a block that size back to
51//! the kernel when it is freed, so the next one faults in every page of it. Measured on the URL
52//! column of `hits-1m-snappy.parquet`, running with `MALLOC_MMAP_THRESHOLD_` and
53//! `MALLOC_TRIM_THRESHOLD_` both raised so that nothing is ever handed back took the query from
54//! 113.08 milliseconds to 104.08, and the decode stage moved as well as the decompress one, which
55//! is what says it is page faults rather than anything about the codec.
56//!
57//! So the arena is held by an [`Arc`] and the reader keeps a handle to it. When every column built
58//! over that page has been dropped the reader is the only holder left, takes the run back out and
59//! decompresses the next page into it. That is a page pool with two entries and no eviction policy,
60//! which is not the buffer manager, but it is the same shape and it is the first caller that will
61//! want one.
62//!
63//! It is [`Arc<Vec<T>>`] rather than [`Pin`] because this caller knows exactly what its page is and
64//! can say so in the type. The [`Pin`] variant is still coming and is still opaque, because the
65//! buffer manager's page is a frame in a pool that a vector has no business knowing the shape of.
66//! Two variants for two situations is the honest answer here: one of them can name its page and the
67//! other cannot.
68
69use std::any::Any;
70use std::ops::Deref;
71use std::sync::Arc;
72
73/// What keeps a page alive for as long as a buffer points into it.
74///
75/// Opaque on purpose. The vector does not know what a page is and has no business looking inside
76/// one, it only has to hold the thing that stops the page being evicted, and the buffer manager at
77/// layer three decides what that thing is. The bounds are the load bearing part and they are here
78/// now: `Send` and `Sync`, because a chunk carrying one of these crosses a thread boundary every
79/// time the scheduler moves a pipeline, and `'static`, because a handle with a lifetime on it would
80/// have put the lifetime back on [`Vector`](crate::Vector) by another route.
81pub type Pin = Arc<dyn Any + Send + Sync>;
82
83/// A run of values of one physical type.
84///
85/// Derefs to a slice, which is how every reader in the workspace gets at it, so a reader does not
86/// know or care which variant it is holding. Writers go through [`Self::push`] and
87/// [`Self::to_mut`].
88#[derive(Debug, Clone)]
89pub struct Buffer<T> {
90    store: Store<T>,
91}
92
93/// Where the values actually are.
94///
95/// The third is a run inside a page, carrying the [`Pin`] that keeps the page where it is, and it
96/// arrives with the buffer manager because that is the only thing that can hand one out. It is an
97/// enum rather than a newtype around `Vec<T>` because the shape is what the rest of the workspace
98/// is compiled against, and a newtype that turns into an enum later is the flag day this exists to
99/// avoid.
100#[derive(Debug, Clone)]
101enum Store<T> {
102    /// The vector owns the values.
103    Owned(Vec<T>),
104    /// The values are a whole page somebody else is holding a handle to as well.
105    ///
106    /// Read only, which is not enforced and does not need to be: the only way to write is
107    /// [`Buffer::to_mut`] and that copies out first, so a shared page is never written through even
108    /// by a caller that has forgotten what it is holding.
109    Shared(Arc<Vec<T>>),
110}
111
112impl<T> Buffer<T> {
113    /// An empty buffer.
114    #[must_use]
115    pub fn new() -> Self {
116        Self { store: Store::Owned(Vec::new()) }
117    }
118
119    /// An empty buffer with room for `capacity` values.
120    #[must_use]
121    pub fn with_capacity(capacity: usize) -> Self {
122        Self { store: Store::Owned(Vec::with_capacity(capacity)) }
123    }
124
125    /// A buffer owning `values`.
126    #[must_use]
127    pub fn from_vec(values: Vec<T>) -> Self {
128        Self { store: Store::Owned(values) }
129    }
130
131    /// A buffer over a whole page that somebody else is holding a handle to as well.
132    ///
133    /// The caller that wants this is one that decoded the page and is going to want the allocation
134    /// back when the last reader of it is gone, which it gets by keeping its own handle and waiting
135    /// for [`Arc::get_mut`] to start answering. Nothing here enforces that, and a caller that drops
136    /// its handle has simply built an owned buffer with an extra indirection.
137    #[must_use]
138    pub fn from_arc(page: Arc<Vec<T>>) -> Self {
139        Self { store: Store::Shared(page) }
140    }
141
142    /// Whether this buffer is a page it shares rather than a run it owns.
143    ///
144    /// For a caller deciding whether a write is about to cost a copy of the page, and for the tests
145    /// that assert the reader did not quietly stop sharing.
146    #[must_use]
147    pub fn is_shared(&self) -> bool {
148        matches!(self.store, Store::Shared(_))
149    }
150
151    /// How many bytes of memory this buffer is holding.
152    ///
153    /// Capacity rather than length, because capacity is what was taken from the allocator.
154    ///
155    /// A shared page is charged to the buffers over it in equal parts, which is an approximation
156    /// and is worth being plain about. The exact answer needs to know who else is holding the page
157    /// and what they are charging, and no buffer can see that. Splitting it means the live buffers
158    /// over one page add up to slightly less than the page, never to several times it, and that is
159    /// the direction to be wrong in: a scan that emits fifty chunks over one ten megabyte page
160    /// would otherwise report half a gigabyte and trip a memory limit that nothing came close to.
161    #[must_use]
162    pub fn footprint(&self) -> usize {
163        match &self.store {
164            Store::Owned(values) => values.capacity() * size_of::<T>(),
165            Store::Shared(page) => page.capacity() * size_of::<T>() / Arc::strong_count(page),
166        }
167    }
168
169    /// The values.
170    #[must_use]
171    #[inline]
172    pub fn as_slice(&self) -> &[T] {
173        match &self.store {
174            Store::Owned(values) => values,
175            Store::Shared(page) => page,
176        }
177    }
178}
179
180impl<T: Clone> Buffer<T> {
181    /// The values as an owned run, copying only if they were not owned already.
182    ///
183    /// One of the two places that can copy a page, and it is spelled as a method rather than a
184    /// field access so that it is greppable. The copy is skipped when the caller turns out to hold
185    /// the last handle to the page, which is the common case for a buffer that was shared only so
186    /// that its producer could get the allocation back.
187    #[must_use]
188    pub fn into_vec(self) -> Vec<T> {
189        match self.store {
190            Store::Owned(values) => values,
191            Store::Shared(page) => Arc::try_unwrap(page).unwrap_or_else(|page| page.to_vec()),
192        }
193    }
194
195    /// The values, writable, copying them out of the page first if they are not owned.
196    ///
197    /// The copy on write point, and the only one. Everything that mutates a buffer goes through
198    /// here, so a variant that is not owned needs a case in this function and in nothing else.
199    #[inline]
200    pub fn to_mut(&mut self) -> &mut Vec<T> {
201        if let Store::Shared(page) = &self.store {
202            self.store = Store::Owned(page.to_vec());
203        }
204        match &mut self.store {
205            Store::Owned(values) => values,
206            // A page cannot be here: the line above just replaced it.
207            Store::Shared(_) => unreachable!("a shared page was copied out one statement ago"),
208        }
209    }
210
211    /// Appends one value.
212    #[inline]
213    pub fn push(&mut self, value: T) {
214        self.to_mut().push(value);
215    }
216
217    /// Room for `additional` more values, taken in one allocation.
218    pub fn reserve(&mut self, additional: usize) {
219        self.to_mut().reserve(additional);
220    }
221
222    /// Appends a run of values.
223    pub fn extend_from_slice(&mut self, values: &[T]) {
224        self.to_mut().extend_from_slice(values);
225    }
226}
227
228/// Equality is the values and not where they live.
229///
230/// Derived equality would call an owned run different from a page holding the same values, which
231/// would make every test in the workspace that compares two vectors assert on how the vector was
232/// built. [`crate::StringColumn`] settles the same question the same way and for the same reason.
233impl<T: PartialEq> PartialEq for Buffer<T> {
234    fn eq(&self, other: &Self) -> bool {
235        self.as_slice() == other.as_slice()
236    }
237}
238
239impl<T: Eq> Eq for Buffer<T> {}
240
241impl<T> Default for Buffer<T> {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247impl<T> Deref for Buffer<T> {
248    type Target = [T];
249
250    #[inline]
251    fn deref(&self) -> &[T] {
252        self.as_slice()
253    }
254}
255
256impl<T> From<Vec<T>> for Buffer<T> {
257    fn from(values: Vec<T>) -> Self {
258        Self::from_vec(values)
259    }
260}
261
262impl<T> FromIterator<T> for Buffer<T> {
263    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
264        Self::from_vec(iter.into_iter().collect())
265    }
266}
267
268impl<T: Clone> Extend<T> for Buffer<T> {
269    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
270        self.to_mut().extend(iter);
271    }
272}
273
274impl<T: Clone> IntoIterator for Buffer<T> {
275    type Item = T;
276    type IntoIter = std::vec::IntoIter<T>;
277
278    fn into_iter(self) -> Self::IntoIter {
279        self.into_vec().into_iter()
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use std::sync::Arc;
286
287    use super::{Buffer, Pin};
288
289    #[test]
290    fn a_buffer_reads_back_as_a_slice() {
291        let buffer: Buffer<i32> = vec![1, 2, 3].into();
292        assert_eq!(buffer.as_slice(), &[1, 2, 3]);
293        assert_eq!(buffer.len(), 3);
294        assert_eq!(buffer[1], 2);
295        assert_eq!(buffer.iter().sum::<i32>(), 6);
296        assert_eq!(buffer.clone().into_vec(), vec![1, 2, 3]);
297    }
298
299    #[test]
300    fn writing_goes_through_one_function() {
301        let mut buffer = Buffer::with_capacity(4);
302        buffer.push(1u8);
303        buffer.extend_from_slice(&[2, 3]);
304        buffer.extend([4u8]);
305        buffer.to_mut().sort_unstable_by(|a, b| b.cmp(a));
306        assert_eq!(buffer.as_slice(), &[4, 3, 2, 1]);
307    }
308
309    #[test]
310    fn an_empty_buffer_is_the_default_and_collects_like_a_vector() {
311        assert!(Buffer::<u64>::default().is_empty());
312        assert!(Buffer::<u64>::new().is_empty());
313        let collected: Buffer<u64> = (0..4).collect();
314        assert_eq!(collected.as_slice(), &[0, 1, 2, 3]);
315        assert_eq!(collected.into_iter().count(), 4);
316    }
317
318    #[test]
319    fn a_shared_page_reads_like_an_owned_run_and_compares_equal_to_one() {
320        let page = Arc::new(vec![1u8, 2, 3]);
321        let buffer = Buffer::from_arc(Arc::clone(&page));
322        assert!(buffer.is_shared());
323        assert_eq!(buffer.as_slice(), &[1, 2, 3]);
324        assert_eq!(buffer[2], 3);
325        assert_eq!(buffer, Buffer::from_vec(vec![1u8, 2, 3]));
326        assert_eq!(Buffer::from_vec(vec![1u8, 2, 3]), buffer);
327        assert_ne!(buffer, Buffer::from_vec(vec![1u8, 2]));
328    }
329
330    /// The point of the variant. The producer keeps a handle, the reader drops its buffer, and the
331    /// producer gets the allocation back rather than the allocator getting it.
332    #[test]
333    fn the_producer_gets_the_page_back_once_the_last_buffer_over_it_is_gone() {
334        let mut page = Arc::new(vec![0u8; 64]);
335        let address = page.as_ptr();
336        let first = Buffer::from_arc(Arc::clone(&page));
337        let second = Buffer::from_arc(Arc::clone(&page));
338        assert!(Arc::get_mut(&mut page).is_none());
339        drop(first);
340        assert!(Arc::get_mut(&mut page).is_none());
341        drop(second);
342        let run = Arc::get_mut(&mut page).expect("the last handle");
343        assert_eq!(run.as_ptr(), address, "the page was reallocated rather than reused");
344    }
345
346    /// Writing through a shared page copies it out, and the page the producer is holding is left
347    /// exactly as it was. The one case where getting this wrong would corrupt another reader.
348    #[test]
349    fn writing_through_a_shared_page_copies_it_and_leaves_the_page_alone() {
350        let page = Arc::new(vec![1u8, 2, 3]);
351        let mut buffer = Buffer::from_arc(Arc::clone(&page));
352        buffer.push(4);
353        assert!(!buffer.is_shared());
354        assert_eq!(buffer.as_slice(), &[1, 2, 3, 4]);
355        assert_eq!(page.as_slice(), &[1, 2, 3]);
356        assert_eq!(Buffer::from_arc(Arc::clone(&page)).into_vec(), vec![1, 2, 3]);
357    }
358
359    /// Taking the run out of the last handle to a page does not copy it, which is what makes the
360    /// shared variant free for a caller that ends up being the only reader after all.
361    #[test]
362    fn the_last_buffer_over_a_page_takes_the_run_without_copying_it() {
363        let page = Arc::new(vec![5u8; 32]);
364        let address = page.as_ptr();
365        let run = Buffer::from_arc(page).into_vec();
366        assert_eq!(run.as_ptr(), address);
367    }
368
369    /// The accounting rule from [`Buffer::footprint`], which is that the buffers over a page add up
370    /// to at most the page rather than to a multiple of it.
371    #[test]
372    fn a_shared_page_is_charged_once_across_the_buffers_over_it() {
373        let page = Arc::new(vec![0u64; 100]);
374        let over: Vec<_> = (0..4).map(|_| Buffer::from_arc(Arc::clone(&page))).collect();
375        let charged: usize = over.iter().map(Buffer::footprint).sum();
376        assert!(
377            charged <= page.capacity() * 8,
378            "{charged} charged for a {} byte page",
379            page.len() * 8
380        );
381        assert!(charged > 0);
382        assert_eq!(Buffer::from_vec(vec![0u64; 100]).footprint(), 800);
383    }
384
385    /// The property the whole section 3.8 decision is about. A buffer of any payload can be sent to
386    /// another thread without a lifetime being involved, and so can a pin, which is what makes a
387    /// chunk `Send` once the borrowed variant exists. Asserted rather than assumed, because a pin
388    /// that was an `Rc` would compile everywhere else and fail here.
389    #[test]
390    fn a_buffer_and_a_pin_both_cross_a_thread_boundary() {
391        const fn assert_send<T: Send>() {}
392        assert_send::<Buffer<i64>>();
393        assert_send::<Pin>();
394        let pin: Pin = Arc::new(vec![0u8; 8]);
395        let buffer: Buffer<i64> = vec![7; 2].into();
396        let handle = std::thread::spawn(move || (buffer.len(), Arc::strong_count(&pin)));
397        assert_eq!(handle.join().expect("the thread"), (2, 1));
398    }
399}