Skip to main content

Module buffer

Module buffer 

Source
Expand description

The run of values behind a flat vector, and the seam the buffer manager arrives through.

spec/engine/03-data-plane.md section 3.8. Today every vector owns its payload and every scan allocates, which is the right place to start and is not where this ends. At layer three the scan reads a page out of the buffer manager and the vector wants to point into that page rather than copy out of it, and the copy it avoids is the largest single copy in the system, because it is every byte of every column every query reads.

The type that supports both is one enum holding either an owned run or a borrowed one, and the part that has to be decided correctly the first time is how the borrow is expressed, because that shows up in every signature that mentions a vector.

§Why the pin is a handle and not a lifetime

The obvious way to express a borrow in Rust is a lifetime parameter, and it is the wrong one here. A lifetime on Buffer is a lifetime on Data, which is a lifetime on Vector, which is a lifetime on Chunk, which is a lifetime on every operator’s state, on every trait object in the pipeline, and on every queue a chunk is put into for another thread to pick up. The scheduler is exactly that last thing, so the borrow would have to outlive a hand off between threads that the compiler has no way to see the end of. The two ways out of that are unsafe code and a copy at the boundary, and the copy at the boundary is the thing the borrow existed to avoid.

So the pin is a Pin, a reference counted handle the buffer holds, and the page stays alive because the handle is alive rather than because a region ends. It costs one atomic increment per vector construction, which is not measurable next to reading the page it is protecting, and the ownership story stays uniform: a chunk is Send, always, whatever its columns are pointing at.

§Why it landed with one variant

There was no buffer manager, so there was nothing to borrow from, and writing the borrowed variant then would have been writing an interface against an imaginary caller. What landed was the enum, with only the owned variant in it, so that adding a variant later is a change inside this crate rather than a change to every signature in the workspace. The reader side of that migration was already done by the Deref below: everything outside this crate reads a slice, and a slice is what every variant hands back. The section after this one is that bet being collected, and it cost two functions.

The writer side is Buffer::to_mut, which is the one function that has to grow a case. A write through a borrowed buffer has to copy the page into an owned run first, which is what Cow does and for the same reason, and having the call site named now means that day is a change to one function rather than a search for every push.

§The second variant arrived early, and from the other direction

The shared variant is here before the buffer manager is, because the Parquet reader needed the same thing for a different reason. A string column is built over the page it was decoded from rather than copying out of it, so the page becomes the column’s arena and goes downstream with it, and the reader never gets the allocation back. On hits that is ten and a half megabytes a row group, freed and taken again for every row group, and glibc hands a block that size back to the kernel when it is freed, so the next one faults in every page of it. Measured on the URL column of hits-1m-snappy.parquet, running with MALLOC_MMAP_THRESHOLD_ and MALLOC_TRIM_THRESHOLD_ both raised so that nothing is ever handed back took the query from 113.08 milliseconds to 104.08, and the decode stage moved as well as the decompress one, which is what says it is page faults rather than anything about the codec.

So the arena is held by an Arc and the reader keeps a handle to it. When every column built over that page has been dropped the reader is the only holder left, takes the run back out and decompresses the next page into it. That is a page pool with two entries and no eviction policy, which is not the buffer manager, but it is the same shape and it is the first caller that will want one.

It is Arc<Vec<T>> rather than Pin because this caller knows exactly what its page is and can say so in the type. The Pin variant is still coming and is still opaque, because the buffer manager’s page is a frame in a pool that a vector has no business knowing the shape of. Two variants for two situations is the honest answer here: one of them can name its page and the other cannot.

Structs§

Buffer
A run of values of one physical type.

Type Aliases§

Pin
What keeps a page alive for as long as a buffer points into it.