mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-11 18:36:15 -04:00
8e3af49f64
# Description of Changes Fixes https://github.com/clockworklabs/SpacetimeDB/issues/2824. Defines a global pool `BsatnRowListBuilderPool` which reclaims the buffers of a `ServerMessage<BsatnFormat>` and which is then used when building new `ServerMessage<BsatnFormat>`s. Notes: 1. The new pool `BsatnRowListBuilderPool` reports the same kind of metrics to prometheus as `PagePool` does. 2. `BsatnRowListBuilder` now works in terms of `BytesMut`. 3. The trait method `fn to_bsatn_extend` is redefined to be capable of dealing with `BytesMut` as well as `Vec<u8>`. 4. A trait `ConsumeEachBuffer` is defined from `ServerMessage<BsatnFormat>` and down to extract buffers. `<ServerMessage<_> as ConsumeEachBuffer>::consume_each_buffer(...)` is then called in `messages::serialize(...)` just after bsatn-encoding the entire message and before any compression is done. This is the place where the pool reclaims buffers. # Benchmarks Benchmark numbers vs. master using `cargo bench --bench subscription -- --baseline subs` on i7-7700K, 64GB RAM: ``` footprint-scan time: [21.607 ms 21.873 ms 22.187 ms] change: [-62.090% -61.438% -60.787%] (p = 0.00 < 0.05) Performance has improved. full-scan time: [22.185 ms 22.245 ms 22.324 ms] change: [-36.884% -36.497% -36.166%] (p = 0.00 < 0.05) Performance has improved. ``` The improvements in `footprint-scan` are mostly thanks to https://github.com/clockworklabs/SpacetimeDB/pull/2918, but 7 ms of the improvements here are thanks to the pool. The improvements to `full-scan` should be only thanks to the pool. # API and ABI breaking changes None # Expected complexity level and risk 2? # Testing - Tests for `Pool<T>` also apply to `BsatnRowListBuilderPool`.
695 lines
28 KiB
Rust
695 lines
28 KiB
Rust
//! This module implements a fast path for converting certain row types between BFLATN <-> BSATN.
|
|
//!
|
|
//! The key insight is that a majority of row types will have a known fixed length,
|
|
//! with no variable-length members.
|
|
//! BFLATN is designed with this in mind, storing fixed-length portions of rows inline,
|
|
//! at the expense of an indirection to reach var-length columns like strings.
|
|
//! A majority of these types will also have a fixed BSATN length,
|
|
//! but note that BSATN stores sum values (enums) without padding,
|
|
//! so row types which contain sums may not have a fixed BSATN length
|
|
//! if the sum's variants have different "live" unpadded lengths.
|
|
//!
|
|
//! For row types with fixed BSATN lengths, we can reduce the BFLATN <-> BSATN conversions
|
|
//! to a series of `memcpy`s, skipping over padding sequences.
|
|
//! This is potentially much faster than the more general
|
|
//! [`crate::bflatn_from::serialize_row_from_page`] and [`crate::bflatn_to::write_row_to_page`] ,
|
|
//! which both traverse a [`RowTypeLayout`] and dispatch on the type of each column.
|
|
//!
|
|
//! For example, to serialize a row of type `(u64, u64, u32, u64)`,
|
|
//! [`bflatn_from`] will do four dispatches, three calls to `serialize_u64` and one to `serialize_u32`.
|
|
//! This module will make 2 `memcpy`s (or actually, `<[u8]>::copy_from_slice`s):
|
|
//! one of 20 bytes to copy the leading `(u64, u64, u32)`, which contains no padding,
|
|
//! and then one of 8 bytes to copy the trailing `u64`, skipping over 4 bytes of padding in between.
|
|
|
|
use super::{
|
|
indexes::{Byte, Bytes},
|
|
util::range_move,
|
|
};
|
|
use core::mem::MaybeUninit;
|
|
use core::ptr;
|
|
use smallvec::SmallVec;
|
|
use spacetimedb_data_structures::slim_slice::SlimSmallSliceBox;
|
|
use spacetimedb_sats::bsatn::BufReservedFill;
|
|
use spacetimedb_sats::layout::{
|
|
AlgebraicTypeLayout, HasLayout, PrimitiveType, ProductTypeElementLayout, ProductTypeLayoutView, RowTypeLayout,
|
|
SumTypeLayout, SumTypeVariantLayout,
|
|
};
|
|
use spacetimedb_sats::memory_usage::MemoryUsage;
|
|
|
|
/// A precomputed layout for a type whose encoded BSATN and BFLATN lengths are both known constants,
|
|
/// enabling fast BFLATN <-> BSATN conversions.
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
#[repr(align(8))]
|
|
pub struct StaticLayout {
|
|
/// The length of the encoded BSATN representation of a row of this type,
|
|
/// in bytes.
|
|
///
|
|
/// Storing this allows us to pre-allocate correctly-sized buffers,
|
|
/// avoiding potentially-expensive `realloc`s.
|
|
pub(crate) bsatn_length: u16,
|
|
|
|
/// A series of `memcpy` invocations from a BFLATN src/dst <-> a BSATN src/dst
|
|
/// which are sufficient to convert BSATN to BFLATN and vice versa.
|
|
fields: SlimSmallSliceBox<MemcpyField, 3>,
|
|
}
|
|
|
|
impl MemoryUsage for StaticLayout {
|
|
fn heap_usage(&self) -> usize {
|
|
let Self { bsatn_length, fields } = self;
|
|
bsatn_length.heap_usage() + fields.heap_usage()
|
|
}
|
|
}
|
|
|
|
impl StaticLayout {
|
|
/// Serialize `row` from BFLATN to BSATN into `buf`.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// - `buf` must be at least `self.bsatn_length` long.
|
|
/// - `row` must store a valid, initialized instance of the BFLATN row type
|
|
/// for which `self` was computed.
|
|
/// As a consequence of this, for every `field` in `self.fields`,
|
|
/// `row[field.bflatn_offset .. field.bflatn_offset + length]` will be initialized.
|
|
unsafe fn serialize_row_into(&self, buf: &mut [MaybeUninit<Byte>], row: &Bytes) {
|
|
debug_assert!(buf.len() >= self.bsatn_length as usize);
|
|
for field in &*self.fields {
|
|
// SAFETY: forward caller requirements.
|
|
unsafe { field.copy_bflatn_to_bsatn(row, buf) };
|
|
}
|
|
}
|
|
|
|
/// Serialize `row` from BFLATN to BSATN into a `Vec<u8>`.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// - `row` must store a valid, initialized instance of the BFLATN row type
|
|
/// for which `self` was computed.
|
|
/// As a consequence of this, for every `field` in `self.fields`,
|
|
/// `row[field.bflatn_offset .. field.bflatn_offset + length]` will be initialized.
|
|
pub(crate) unsafe fn serialize_row_into_vec(&self, row: &Bytes) -> Vec<u8> {
|
|
let mut buf = Vec::new();
|
|
|
|
// SAFETY: Forward caller requirements.
|
|
unsafe { self.serialize_row_extend(&mut buf, row) };
|
|
|
|
buf
|
|
}
|
|
|
|
/// Serialize `row` from BFLATN to BSATN, appending the BSATN to `buf`.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// - `row` must store a valid, initialized instance of the BFLATN row type
|
|
/// for which `self` was computed.
|
|
/// As a consequence of this, for every `field` in `self.fields`,
|
|
/// `row[field.bflatn_offset .. field.bflatn_offset + length]` will be initialized.
|
|
pub(crate) unsafe fn serialize_row_extend(&self, buf: &mut impl BufReservedFill, row: &Bytes) {
|
|
let len = self.bsatn_length as usize;
|
|
// Writes the row into the slice using a series of `memcpy`s.
|
|
// SAFETY:
|
|
// - Caller promised that `row` is valid for `self`.
|
|
// - `sink` was constructed with exactly the correct length above.
|
|
let filler = |sink: &mut _| unsafe {
|
|
self.serialize_row_into(sink, row);
|
|
};
|
|
// SAFETY:
|
|
// The closure `filler` will write exactly `len` bytes.
|
|
unsafe { buf.reserve_and_fill(len, filler) };
|
|
}
|
|
|
|
#[allow(unused)]
|
|
/// Deserializes the BSATN-encoded `row` into the BFLATN-encoded `buf`.
|
|
///
|
|
/// - `row` must be at least `self.bsatn_length` long.
|
|
/// - `buf` must be ready to store an instance of the BFLATN row type
|
|
/// for which `self` was computed.
|
|
/// As a consequence of this, for every `field` in `self.fields`,
|
|
/// `field.bflatn_offset .. field.bflatn_offset + length` must be in-bounds of `buf`.
|
|
pub(crate) unsafe fn deserialize_row_into(&self, buf: &mut Bytes, row: &[u8]) {
|
|
for field in &*self.fields {
|
|
// SAFETY: forward caller requirements.
|
|
unsafe { field.copy_bsatn_to_bflatn(row, buf) };
|
|
}
|
|
}
|
|
|
|
/// Compares `row_a` for equality against `row_b`.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// - `row` must store a valid, initialized instance of the BFLATN row type
|
|
/// for which `self` was computed.
|
|
/// As a consequence of this, for every `field` in `self.fields`,
|
|
/// `row[field.bflatn_offset .. field.bflatn_offset + field.length]` will be initialized.
|
|
pub(crate) unsafe fn eq(&self, row_a: &Bytes, row_b: &Bytes) -> bool {
|
|
// No need to check the lengths.
|
|
// We assume they are of the same length.
|
|
self.fields.iter().all(|field| {
|
|
// SAFETY: The consequence of what the caller promised is that
|
|
// `row_(a/b).len() >= field.bflatn_offset + field.length >= field.bflatn_offset`.
|
|
unsafe { field.eq(row_a, row_b) }
|
|
})
|
|
}
|
|
|
|
/// Construct a `StaticLayout` for converting BFLATN rows of `row_type` <-> BSATN.
|
|
///
|
|
/// Returns `None` if `row_type` contains a column which does not have a constant length in BSATN,
|
|
/// either a [`VarLenType`]
|
|
/// or a [`SumTypeLayout`] whose variants do not have the same "live" unpadded length.
|
|
pub fn for_row_type(row_type: &RowTypeLayout) -> Option<Self> {
|
|
if !row_type.layout().fixed {
|
|
// Don't bother computing the static layout if there are variable components.
|
|
return None;
|
|
}
|
|
|
|
let mut builder = LayoutBuilder::new_builder();
|
|
builder.visit_product(row_type.product())?;
|
|
Some(builder.build())
|
|
}
|
|
}
|
|
|
|
/// An identifier for a series of bytes within a BFLATN row
|
|
/// which can be directly copied into an output BSATN buffer
|
|
/// with a known length and offset or vice versa.
|
|
///
|
|
/// Within the row type's BFLATN layout, `row[bflatn_offset .. (bflatn_offset + length)]`
|
|
/// must not contain any padding bytes,
|
|
/// i.e. all of those bytes must be fully initialized if the row is initialized.
|
|
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
|
struct MemcpyField {
|
|
/// Offset in the BFLATN row from which to begin `memcpy`ing, in bytes.
|
|
bflatn_offset: u16,
|
|
|
|
/// Offset in the BSATN buffer to which to begin `memcpy`ing, in bytes.
|
|
// TODO(perf): Could be a running counter, but this way we just have all the `memcpy` args in one place.
|
|
// Should bench; I (pgoldman 2024-03-25) suspect this allows more insn parallelism and is therefore better.
|
|
bsatn_offset: u16,
|
|
|
|
/// Length to `memcpy`, in bytes.
|
|
length: u16,
|
|
}
|
|
|
|
impl MemoryUsage for MemcpyField {}
|
|
|
|
impl MemcpyField {
|
|
/// Copies the bytes at `src[self.bflatn_offset .. self.bflatn_offset + self.length]`
|
|
/// into `dst[self.bsatn_offset .. self.bsatn_offset + self.length]`.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// 1. `src.len() >= self.bflatn_offset + self.length`.
|
|
/// 2. `dst.len() >= self.bsatn_offset + self.length`
|
|
unsafe fn copy_bflatn_to_bsatn(&self, src: &Bytes, dst: &mut [MaybeUninit<Byte>]) {
|
|
let src_offset = self.bflatn_offset as usize;
|
|
let dst_offset = self.bsatn_offset as usize;
|
|
|
|
let len = self.length as usize;
|
|
let src = src.as_ptr();
|
|
let dst = dst.as_mut_ptr();
|
|
// SAFETY: per 1., it follows that `src_offset` is in bounds of `src`.
|
|
let src = unsafe { src.add(src_offset) };
|
|
// SAFETY: per 2., it follows that `dst_offset` is in bounds of `dst`.
|
|
let dst = unsafe { dst.add(dst_offset) };
|
|
let dst = dst.cast();
|
|
|
|
// SAFETY:
|
|
// 1. `src` is valid for reads for `len` bytes per caller requirement 1.
|
|
// and because `src` was derived from a shared slice.
|
|
// 2. `dst` is valid for writes for `len` bytes per caller requirement 2.
|
|
// and because `dst` was derived from an exclusive slice.
|
|
// 3. Alignment for `u8` is trivially satisfied for any pointer.
|
|
// 4. As `src` and `dst` were derived from shared and exclusive slices, they cannot overlap.
|
|
unsafe { ptr::copy_nonoverlapping(src, dst, len) }
|
|
}
|
|
|
|
/// Copies the bytes at `src[self.bsatn_offset .. self.bsatn_offset + self.length]`
|
|
/// into `dst[self.bflatn_offset .. self.bflatn_offset + self.length]`.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// 1. `src.len() >= self.bsatn_offset + self.length`.
|
|
/// 2. `dst.len() >= self.bflatn_offset + self.length`
|
|
unsafe fn copy_bsatn_to_bflatn(&self, src: &Bytes, dst: &mut Bytes) {
|
|
let src_offset = self.bsatn_offset as usize;
|
|
let dst_offset = self.bflatn_offset as usize;
|
|
|
|
let len = self.length as usize;
|
|
let src = src.as_ptr();
|
|
let dst = dst.as_mut_ptr();
|
|
// SAFETY: per 1., it follows that `src_offset` is in bounds of `src`.
|
|
let src = unsafe { src.add(src_offset) };
|
|
// SAFETY: per 2., it follows that `dst_offset` is in bounds of `dst`.
|
|
let dst = unsafe { dst.add(dst_offset) };
|
|
|
|
// SAFETY:
|
|
// 1. `src` is valid for reads for `len` bytes per caller requirement 1.
|
|
// and because `src` was derived from a shared slice.
|
|
// 2. `dst` is valid for writes for `len` bytes per caller requirement 2.
|
|
// and because `dst` was derived from an exclusive slice.
|
|
// 3. Alignment for `u8` is trivially satisfied for any pointer.
|
|
// 4. As `src` and `dst` were derived from shared and exclusive slices, they cannot overlap.
|
|
unsafe { ptr::copy_nonoverlapping(src, dst, len) }
|
|
}
|
|
|
|
/// Compares `row_a` and `row_b` for equality in this field.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// - `row_a.len() >= self.bflatn_offset + self.length`
|
|
/// - `row_b.len() >= self.bflatn_offset + self.length`
|
|
unsafe fn eq(&self, row_a: &Bytes, row_b: &Bytes) -> bool {
|
|
let range = range_move(0..self.length as usize, self.bflatn_offset as usize);
|
|
let range2 = range.clone();
|
|
// SAFETY: The `range` is in bounds as
|
|
// `row_a.len() >= self.bflatn_offset + self.length >= self.bflatn_offset`.
|
|
let row_a_field = unsafe { row_a.get_unchecked(range) };
|
|
// SAFETY: The `range` is in bounds as
|
|
// `row_b.len() >= self.bflatn_offset + self.length >= self.bflatn_offset`.
|
|
let row_b_field = unsafe { row_b.get_unchecked(range2) };
|
|
row_a_field == row_b_field
|
|
}
|
|
|
|
fn is_empty(&self) -> bool {
|
|
self.length == 0
|
|
}
|
|
}
|
|
|
|
/// A builder for a [`StaticLayout`].
|
|
struct LayoutBuilder {
|
|
/// Always at least one element.
|
|
fields: Vec<MemcpyField>,
|
|
}
|
|
|
|
impl LayoutBuilder {
|
|
fn new_builder() -> Self {
|
|
Self {
|
|
fields: vec![MemcpyField {
|
|
bflatn_offset: 0,
|
|
bsatn_offset: 0,
|
|
length: 0,
|
|
}],
|
|
}
|
|
}
|
|
|
|
fn build(self) -> StaticLayout {
|
|
let LayoutBuilder { fields } = self;
|
|
let fields: SmallVec<[_; 3]> = fields.into_iter().filter(|field| !field.is_empty()).collect();
|
|
let fields: SlimSmallSliceBox<MemcpyField, 3> = fields.into();
|
|
let bsatn_length = fields.last().map(|last| last.bsatn_offset + last.length).unwrap_or(0);
|
|
|
|
StaticLayout { bsatn_length, fields }
|
|
}
|
|
|
|
fn current_field(&self) -> &MemcpyField {
|
|
self.fields.last().unwrap()
|
|
}
|
|
|
|
fn current_field_mut(&mut self) -> &mut MemcpyField {
|
|
self.fields.last_mut().unwrap()
|
|
}
|
|
|
|
fn next_bflatn_offset(&self) -> u16 {
|
|
let last = self.current_field();
|
|
last.bflatn_offset + last.length
|
|
}
|
|
|
|
fn next_bsatn_offset(&self) -> u16 {
|
|
let last = self.current_field();
|
|
last.bsatn_offset + last.length
|
|
}
|
|
|
|
fn visit_product(&mut self, product: ProductTypeLayoutView) -> Option<()> {
|
|
let base_bflatn_offset = self.next_bflatn_offset();
|
|
for elt in product.elements.iter() {
|
|
self.visit_product_element(elt, base_bflatn_offset)?;
|
|
}
|
|
Some(())
|
|
}
|
|
|
|
fn visit_product_element(&mut self, elt: &ProductTypeElementLayout, product_base_offset: u16) -> Option<()> {
|
|
let elt_offset = product_base_offset + elt.offset;
|
|
let next_bflatn_offset = self.next_bflatn_offset();
|
|
if next_bflatn_offset != elt_offset {
|
|
// Padding between previous element and this element,
|
|
// so start a new field.
|
|
//
|
|
// Note that this is the only place we have to reason about alignment and padding
|
|
// because the enclosing `ProductTypeLayout` has already computed valid aligned offsets
|
|
// for the elements.
|
|
|
|
let bsatn_offset = self.next_bsatn_offset();
|
|
self.fields.push(MemcpyField {
|
|
bsatn_offset,
|
|
bflatn_offset: elt_offset,
|
|
length: 0,
|
|
});
|
|
}
|
|
self.visit_value(&elt.ty)
|
|
}
|
|
|
|
fn visit_value(&mut self, val: &AlgebraicTypeLayout) -> Option<()> {
|
|
match val {
|
|
AlgebraicTypeLayout::Sum(sum) => self.visit_sum(sum),
|
|
AlgebraicTypeLayout::Product(prod) => self.visit_product(prod.view()),
|
|
AlgebraicTypeLayout::Primitive(prim) => {
|
|
self.visit_primitive(prim);
|
|
Some(())
|
|
}
|
|
|
|
// Var-len types (obviously) don't have a known BSATN length,
|
|
// so fail.
|
|
AlgebraicTypeLayout::VarLen(_) => None,
|
|
}
|
|
}
|
|
|
|
fn visit_sum(&mut self, sum: &SumTypeLayout) -> Option<()> {
|
|
// If the sum has no variants, it's the never type, so there's no point in computing a layout.
|
|
let first_variant = sum.variants.first()?;
|
|
|
|
let variant_layout = |variant: &SumTypeVariantLayout| {
|
|
let mut builder = LayoutBuilder::new_builder();
|
|
builder.visit_value(&variant.ty)?;
|
|
Some(builder.build())
|
|
};
|
|
|
|
// Check that the variants all have the same `StaticLayout`.
|
|
// If they don't, bail.
|
|
let first_variant_layout = variant_layout(first_variant)?;
|
|
for later_variant in &sum.variants[1..] {
|
|
let later_variant_layout = variant_layout(later_variant)?;
|
|
if later_variant_layout != first_variant_layout {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
if first_variant_layout.bsatn_length == 0 {
|
|
// For C-style enums (those without payloads),
|
|
// simply serialize the tag and move on.
|
|
self.current_field_mut().length += 1;
|
|
return Some(());
|
|
}
|
|
|
|
// Now that we've reached this point, we know that `first_variant_layout`
|
|
// applies to the values of all the variants.
|
|
|
|
let tag_bflatn_offset = self.next_bflatn_offset();
|
|
let payload_bflatn_offset = tag_bflatn_offset + sum.payload_offset;
|
|
|
|
let tag_bsatn_offset = self.next_bsatn_offset();
|
|
let payload_bsatn_offset = tag_bsatn_offset + 1;
|
|
|
|
// Serialize the tag, consolidating into the previous memcpy if possible.
|
|
self.visit_primitive(&PrimitiveType::U8);
|
|
|
|
if sum.payload_offset > 1 {
|
|
// Add an empty marker field to keep track of padding.
|
|
self.fields.push(MemcpyField {
|
|
bflatn_offset: payload_bflatn_offset,
|
|
bsatn_offset: payload_bsatn_offset,
|
|
length: 0,
|
|
});
|
|
} // Otherwise, nothing to do.
|
|
|
|
// Lay out the variants.
|
|
// Since all variants have the same layout, we just use the first one.
|
|
self.visit_value(&first_variant.ty)?;
|
|
|
|
Some(())
|
|
}
|
|
|
|
fn visit_primitive(&mut self, prim: &PrimitiveType) {
|
|
self.current_field_mut().length += prim.size() as u16
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use crate::{blob_store::HashMapBlobStore, page_pool::PagePool};
|
|
use proptest::prelude::*;
|
|
use spacetimedb_sats::{bsatn, proptest::generate_typed_row, AlgebraicType, ProductType};
|
|
|
|
fn assert_expected_layout(ty: ProductType, bsatn_length: u16, fields: &[(u16, u16, u16)]) {
|
|
let expected_layout = StaticLayout {
|
|
bsatn_length,
|
|
fields: fields
|
|
.iter()
|
|
.copied()
|
|
.map(|(bflatn_offset, bsatn_offset, length)| MemcpyField {
|
|
bflatn_offset,
|
|
bsatn_offset,
|
|
length,
|
|
})
|
|
.collect::<SmallVec<_>>()
|
|
.into(),
|
|
};
|
|
let row_type = RowTypeLayout::from(ty.clone());
|
|
let Some(computed_layout) = StaticLayout::for_row_type(&row_type) else {
|
|
panic!("assert_expected_layout: Computed `None` for row {row_type:#?}\nExpected:{expected_layout:#?}");
|
|
};
|
|
assert_eq!(
|
|
computed_layout, expected_layout,
|
|
"assert_expected_layout: Computed layout (left) doesn't match expected (right) for {ty:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn known_types_expected_layout_plain() {
|
|
for prim in [
|
|
AlgebraicType::Bool,
|
|
AlgebraicType::U8,
|
|
AlgebraicType::I8,
|
|
AlgebraicType::U16,
|
|
AlgebraicType::I16,
|
|
AlgebraicType::U32,
|
|
AlgebraicType::I32,
|
|
AlgebraicType::U64,
|
|
AlgebraicType::I64,
|
|
AlgebraicType::U128,
|
|
AlgebraicType::I128,
|
|
AlgebraicType::U256,
|
|
AlgebraicType::I256,
|
|
] {
|
|
let size = AlgebraicTypeLayout::from(prim.clone()).size() as u16;
|
|
assert_expected_layout(ProductType::from([prim]), size, &[(0, 0, size)]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn known_types_expected_layout_complex() {
|
|
for (ty, bsatn_length, fields) in [
|
|
(ProductType::new([].into()), 0, &[][..]),
|
|
(
|
|
ProductType::from([AlgebraicType::sum([
|
|
AlgebraicType::U8,
|
|
AlgebraicType::I8,
|
|
AlgebraicType::Bool,
|
|
])]),
|
|
2,
|
|
// In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
|
|
// In this case, 0 bytes of padding, because all payloads are aligned to 1.
|
|
// Since there's no padding, the memcpys can be consolidated.
|
|
&[(0, 0, 2)][..],
|
|
),
|
|
(
|
|
ProductType::from([AlgebraicType::sum([
|
|
AlgebraicType::product([
|
|
AlgebraicType::U8,
|
|
AlgebraicType::U8,
|
|
AlgebraicType::U8,
|
|
AlgebraicType::U8,
|
|
]),
|
|
AlgebraicType::product([AlgebraicType::U16, AlgebraicType::U16]),
|
|
AlgebraicType::U32,
|
|
])]),
|
|
5,
|
|
// In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
|
|
// In this case, 3 bytes of padding.
|
|
&[(0, 0, 1), (4, 1, 4)][..],
|
|
),
|
|
(
|
|
ProductType::from([
|
|
AlgebraicType::sum([AlgebraicType::U128, AlgebraicType::I128]),
|
|
AlgebraicType::U32,
|
|
]),
|
|
21,
|
|
// In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
|
|
// In this case, 15 bytes of padding.
|
|
&[(0, 0, 1), (16, 1, 20)][..],
|
|
),
|
|
(
|
|
ProductType::from([
|
|
AlgebraicType::sum([AlgebraicType::U256, AlgebraicType::I256]),
|
|
AlgebraicType::U32,
|
|
]),
|
|
37,
|
|
// In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
|
|
// In this case, 15 bytes of padding.
|
|
&[(0, 0, 1), (32, 1, 36)][..],
|
|
),
|
|
(
|
|
ProductType::from([
|
|
AlgebraicType::U256,
|
|
AlgebraicType::U128,
|
|
AlgebraicType::U64,
|
|
AlgebraicType::U32,
|
|
AlgebraicType::U16,
|
|
AlgebraicType::U8,
|
|
]),
|
|
63,
|
|
&[(0, 0, 63)][..],
|
|
),
|
|
(
|
|
ProductType::from([
|
|
AlgebraicType::U8,
|
|
AlgebraicType::U16,
|
|
AlgebraicType::U32,
|
|
AlgebraicType::U64,
|
|
AlgebraicType::U128,
|
|
]),
|
|
31,
|
|
&[(0, 0, 1), (2, 1, 30)][..],
|
|
),
|
|
// Make sure sums with no variant data are handled correctly.
|
|
(
|
|
ProductType::from([AlgebraicType::sum([AlgebraicType::product::<[AlgebraicType; 0]>([])])]),
|
|
1,
|
|
&[(0, 0, 1)][..],
|
|
),
|
|
(
|
|
ProductType::from([AlgebraicType::sum([
|
|
AlgebraicType::product::<[AlgebraicType; 0]>([]),
|
|
AlgebraicType::product::<[AlgebraicType; 0]>([]),
|
|
])]),
|
|
1,
|
|
&[(0, 0, 1)][..],
|
|
),
|
|
// Various experiments with 1-byte-aligned payloads.
|
|
// These are particularly nice for memcpy consolidation as there's no padding.
|
|
(
|
|
ProductType::from([AlgebraicType::sum([
|
|
AlgebraicType::product([AlgebraicType::U8, AlgebraicType::U8]),
|
|
AlgebraicType::product([AlgebraicType::Bool, AlgebraicType::Bool]),
|
|
])]),
|
|
3,
|
|
&[(0, 0, 3)][..],
|
|
),
|
|
(
|
|
ProductType::from([
|
|
AlgebraicType::sum([AlgebraicType::Bool, AlgebraicType::U8]),
|
|
AlgebraicType::sum([AlgebraicType::U8, AlgebraicType::Bool]),
|
|
]),
|
|
4,
|
|
&[(0, 0, 4)][..],
|
|
),
|
|
(
|
|
ProductType::from([
|
|
AlgebraicType::U16,
|
|
AlgebraicType::sum([AlgebraicType::U8, AlgebraicType::Bool]),
|
|
AlgebraicType::U16,
|
|
]),
|
|
6,
|
|
&[(0, 0, 6)][..],
|
|
),
|
|
(
|
|
ProductType::from([
|
|
AlgebraicType::U32,
|
|
AlgebraicType::sum([AlgebraicType::U16, AlgebraicType::I16]),
|
|
AlgebraicType::U32,
|
|
]),
|
|
11,
|
|
&[(0, 0, 5), (6, 5, 6)][..],
|
|
),
|
|
] {
|
|
assert_expected_layout(ty, bsatn_length, fields);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn known_types_not_applicable() {
|
|
for ty in [
|
|
AlgebraicType::String,
|
|
AlgebraicType::bytes(),
|
|
AlgebraicType::never(),
|
|
AlgebraicType::array(AlgebraicType::U16),
|
|
AlgebraicType::sum([AlgebraicType::U8, AlgebraicType::U16]),
|
|
] {
|
|
let layout = RowTypeLayout::from(ProductType::from([ty]));
|
|
if let Some(computed) = StaticLayout::for_row_type(&layout) {
|
|
panic!("Expected row type not to have a constant BSATN layout!\nRow type: {layout:#?}\nBSATN layout: {computed:#?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
// The tests `known_bsatn_same_as_bflatn_from`
|
|
// and `known_bflatn_same_as_pv_from` generate a lot of rejects,
|
|
// as a vast majority of the space of `ProductType` does not have a fixed BSATN length.
|
|
// Writing a proptest generator which produces only types that have a fixed BSATN length
|
|
// seems hard, because we'd have to generate sums with known matching layouts,
|
|
// so we just bump the `max_global_rejects` up as high as it'll go and move on with our lives.
|
|
//
|
|
// Note that I (pgoldman 2024-03-21) tried modifying `generate_typed_row`
|
|
// to not emit `String`, `Array` or `Map` types (the trivially var-len types),
|
|
// but did not see a meaningful decrease in the number of rejects.
|
|
// This is because a majority of the var-len BSATN types in the `generate_typed_row` space
|
|
// are due to sums with inconsistent payload layouts.
|
|
//
|
|
// We still include the test `known_bsatn_same_as_bsatn_from`
|
|
// because it tests row types not covered in `known_types_expected_layout`,
|
|
// especially larger types with unusual sequences of aligned fields.
|
|
#![proptest_config(ProptestConfig { max_global_rejects: 65536, ..Default::default()})]
|
|
|
|
#[test]
|
|
fn known_bsatn_same_as_bflatn_from((ty, val) in generate_typed_row()) {
|
|
let pool = PagePool::new_for_test();
|
|
let mut blob_store = HashMapBlobStore::default();
|
|
let mut table = crate::table::test::table(ty);
|
|
let Some(static_layout) = table.static_layout().cloned() else {
|
|
// `ty` has a var-len member or a sum with different payload lengths,
|
|
// so the fast path doesn't apply.
|
|
return Err(TestCaseError::reject("Var-length type"));
|
|
};
|
|
|
|
let (_, row_ref) = table.insert(&pool, &mut blob_store, &val).unwrap();
|
|
let bytes = row_ref.get_row_data();
|
|
|
|
let slow_path = bsatn::to_vec(&row_ref).unwrap();
|
|
|
|
let fast_path = unsafe {
|
|
static_layout.serialize_row_into_vec(bytes)
|
|
};
|
|
|
|
let mut fast_path2 = Vec::new();
|
|
unsafe {
|
|
static_layout.serialize_row_extend(&mut fast_path2, bytes)
|
|
};
|
|
|
|
assert_eq!(slow_path, fast_path);
|
|
assert_eq!(slow_path, fast_path2);
|
|
}
|
|
|
|
#[test]
|
|
fn known_bflatn_same_as_pv_from((ty, val) in generate_typed_row()) {
|
|
let pool = PagePool::new_for_test();
|
|
let mut blob_store = HashMapBlobStore::default();
|
|
let mut table = crate::table::test::table(ty);
|
|
let Some(static_layout) = table.static_layout().cloned() else {
|
|
// `ty` has a var-len member or a sum with different payload lengths,
|
|
// so the fast path doesn't apply.
|
|
return Err(TestCaseError::reject("Var-length type"));
|
|
};
|
|
let bsatn = bsatn::to_vec(&val).unwrap();
|
|
|
|
let (_, row_ref) = table.insert(&pool, &mut blob_store, &val).unwrap();
|
|
let slow_path = row_ref.get_row_data();
|
|
|
|
let mut fast_path = vec![0u8; slow_path.len()];
|
|
unsafe {
|
|
static_layout.deserialize_row_into(&mut fast_path, &bsatn);
|
|
};
|
|
|
|
assert_eq!(slow_path, fast_path);
|
|
}
|
|
}
|
|
}
|