Skip to content

Commit

Permalink
Optimize integrate_generated_columns (#1895)
Browse files Browse the repository at this point in the history
  • Loading branch information
coolreader18 authored Nov 12, 2024
1 parent 57fb9ad commit 97bff92
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 67 deletions.
7 changes: 2 additions & 5 deletions crates/bindings-macro/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,14 +526,11 @@ pub(crate) fn table_impl(mut args: TableArgs, mut item: MutItem<syn::DeriveInput
let integrate_gen_col = sequenced_columns.iter().map(|col| {
let field = col.field.ident.unwrap();
quote_spanned!(field.span()=>
if spacetimedb::table::IsSequenceTrigger::is_sequence_trigger(&_row.#field) {
_row.#field = spacetimedb::sats::bsatn::from_reader(_in).unwrap();
}
spacetimedb::table::SequenceTrigger::maybe_decode_into(&mut __row.#field, &mut __generated_cols);
)
});
let integrate_generated_columns = quote_spanned!(item.span() =>
fn integrate_generated_columns(_row: &mut #row_type, mut _generated_cols: &[u8]) {
let mut _in = &mut _generated_cols;
fn integrate_generated_columns(__row: &mut #row_type, mut __generated_cols: &[u8]) {
#(#integrate_gen_col)*
}
);
Expand Down
61 changes: 50 additions & 11 deletions crates/bindings/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::convert::Infallible;
use std::marker::PhantomData;
use std::{fmt, ops};

use spacetimedb_lib::buffer::{BufReader, Cursor};
use spacetimedb_lib::buffer::{BufReader, Cursor, DecodeError};
use spacetimedb_lib::sats::{i256, u256};

pub use spacetimedb_lib::db::raw_def::v9::TableAccess;
Expand Down Expand Up @@ -755,39 +755,78 @@ impl_terminator!(
// impl<T, U, V> BTreeIndexBounds<(T, U, V)> for (T, U, Range<V>) {}
// impl<T, U, V> BTreeIndexBounds<(T, U, V)> for (T, U, V) {}

/// A trait for types that know if their value will trigger a sequence.
/// A trait for types that can have a sequence based on them.
/// This is used for auto-inc columns to determine if an insertion of a row
/// will require the column to be updated in the row.
///
/// For now, this is equivalent to a "is zero" test.
pub trait IsSequenceTrigger {
pub trait SequenceTrigger: Sized {
/// Is this value one that will trigger a sequence, if any,
/// when used as a column value.
fn is_sequence_trigger(&self) -> bool;
/// BufReader::get_[< self >]
fn decode(reader: &mut &[u8]) -> Result<Self, DecodeError>;
/// Read a generated column from the slice, if this row was a sequence trigger.
#[inline(always)]
fn maybe_decode_into(&mut self, gen_cols: &mut &[u8]) {
if self.is_sequence_trigger() {
*self = Self::decode(gen_cols).unwrap_or_else(|_| sequence_decode_error())
}
}
}

#[cold]
#[inline(never)]
fn sequence_decode_error() -> ! {
unreachable!("a row was a sequence trigger but there was no generated column for it.")
}

macro_rules! impl_is_seq_trigger {
($($t:ty),*) => {
macro_rules! impl_seq_trigger {
($($get:ident($t:ty),)*) => {
$(
impl IsSequenceTrigger for $t {
impl SequenceTrigger for $t {
#[inline(always)]
fn is_sequence_trigger(&self) -> bool { *self == 0 }
#[inline(always)]
fn decode(reader: &mut &[u8]) -> Result<Self, DecodeError> {
reader.$get()
}
}
)*
};
}

impl_is_seq_trigger![u8, i8, u16, i16, u32, i32, u64, i64, u128, i128];
impl_seq_trigger!(
get_u8(u8),
get_i8(i8),
get_u16(u16),
get_i16(i16),
get_u32(u32),
get_i32(i32),
get_u64(u64),
get_i64(i64),
get_u128(u128),
get_i128(i128),
);

impl IsSequenceTrigger for crate::sats::i256 {
impl SequenceTrigger for crate::sats::i256 {
#[inline(always)]
fn is_sequence_trigger(&self) -> bool {
*self == Self::ZERO
}
#[inline(always)]
fn decode(reader: &mut &[u8]) -> Result<Self, DecodeError> {
reader.get_i256()
}
}

impl IsSequenceTrigger for crate::sats::u256 {
impl SequenceTrigger for crate::sats::u256 {
#[inline(always)]
fn is_sequence_trigger(&self) -> bool {
*self == Self::ZERO
}
#[inline(always)]
fn decode(reader: &mut &[u8]) -> Result<Self, DecodeError> {
reader.get_u256()
}
}

/// Insert a row of type `T` into the table identified by `table_id`.
Expand Down
2 changes: 1 addition & 1 deletion crates/commitlog/src/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ impl<const N: usize> Decoder for ArrayDecoder<N> {
_tx_offset: u64,
reader: &mut R,
) -> Result<Self::Record, Self::Error> {
Ok(reader.get_array()?)
Ok(*reader.get_array()?)
}

fn skip_record<'a, R: BufReader<'a>>(
Expand Down
155 changes: 107 additions & 48 deletions crates/sats/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use core::str::Utf8Error;
pub enum DecodeError {
/// Not enough data was provided in the input.
BufferLength {
for_type: String,
for_type: &'static str,
expected: usize,
given: usize,
},
Expand Down Expand Up @@ -126,117 +126,161 @@ pub trait BufWriter {
}
}

macro_rules! get_int {
($self:ident, $int:ident) => {
match $self.get_array_chunk() {
Some(&arr) => Ok($int::from_le_bytes(arr)),
None => Err(DecodeError::BufferLength {
for_type: stringify!($int),
expected: std::mem::size_of::<$int>(),
given: $self.remaining(),
}),
}
};
}

/// A buffered reader of some kind.
///
/// The lifetime `'de` allows the output of deserialization to borrow from the input.
pub trait BufReader<'de> {
/// Reads and returns a byte slice of `.len() = size` advancing the cursor.
fn get_slice(&mut self, size: usize) -> Result<&'de [u8], DecodeError>;
/// Reads and returns a chunk of `.len() = size` advancing the cursor iff `self.remaining() >= size`.
fn get_chunk(&mut self, size: usize) -> Option<&'de [u8]>;

/// Returns the number of bytes left to read in the input.
fn remaining(&self) -> usize;

/// Reads and returns a chunk of `.len() = N` as an array, advancing the cursor.
#[inline]
fn get_array_chunk<const N: usize>(&mut self) -> Option<&'de [u8; N]> {
self.get_chunk(N)?.try_into().ok()
}

/// Reads and returns a byte slice of `.len() = size` advancing the cursor.
#[inline]
fn get_slice(&mut self, size: usize) -> Result<&'de [u8], DecodeError> {
self.get_chunk(size).ok_or(DecodeError::BufferLength {
for_type: "[u8]",
expected: size,
given: self.remaining(),
})
}

/// Reads an array of type `[u8; N]` from the input.
#[inline]
fn get_array<const N: usize>(&mut self) -> Result<&'de [u8; N], DecodeError> {
self.get_array_chunk().ok_or(DecodeError::BufferLength {
for_type: "[u8; _]",
expected: N,
given: self.remaining(),
})
}

/// Reads a `u8` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_u8(&mut self) -> Result<u8, DecodeError> {
self.get_array().map(u8::from_le_bytes)
get_int!(self, u8)
}

/// Reads a `u16` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_u16(&mut self) -> Result<u16, DecodeError> {
self.get_array().map(u16::from_le_bytes)
get_int!(self, u16)
}

/// Reads a `u32` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_u32(&mut self) -> Result<u32, DecodeError> {
self.get_array().map(u32::from_le_bytes)
get_int!(self, u32)
}

/// Reads a `u64` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_u64(&mut self) -> Result<u64, DecodeError> {
self.get_array().map(u64::from_le_bytes)
get_int!(self, u64)
}

/// Reads a `u128` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_u128(&mut self) -> Result<u128, DecodeError> {
self.get_array().map(u128::from_le_bytes)
get_int!(self, u128)
}

/// Reads a `u256` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_u256(&mut self) -> Result<u256, DecodeError> {
self.get_array().map(u256::from_le_bytes)
get_int!(self, u256)
}

/// Reads an `i8` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_i8(&mut self) -> Result<i8, DecodeError> {
self.get_array().map(i8::from_le_bytes)
get_int!(self, i8)
}

/// Reads an `i16` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_i16(&mut self) -> Result<i16, DecodeError> {
self.get_array().map(i16::from_le_bytes)
get_int!(self, i16)
}

/// Reads an `i32` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_i32(&mut self) -> Result<i32, DecodeError> {
self.get_array().map(i32::from_le_bytes)
get_int!(self, i32)
}

/// Reads an `i64` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_i64(&mut self) -> Result<i64, DecodeError> {
self.get_array().map(i64::from_le_bytes)
get_int!(self, i64)
}

/// Reads an `i128` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_i128(&mut self) -> Result<i128, DecodeError> {
self.get_array().map(i128::from_le_bytes)
get_int!(self, i128)
}

/// Reads an `i256` in little endian (LE) encoding from the input.
///
/// This method is provided for convenience
/// and is derived from [`get_slice`](BufReader::get_slice)'s definition.
/// and is derived from [`get_chunk`](BufReader::get_chunk)'s definition.
#[inline]
fn get_i256(&mut self) -> Result<i256, DecodeError> {
self.get_array().map(i256::from_le_bytes)
}

/// Reads an array of type `[u8; C]` from the input.
fn get_array<const C: usize>(&mut self) -> Result<[u8; C], DecodeError> {
let mut buf: [u8; C] = [0; C];
buf.copy_from_slice(self.get_slice(C)?);
Ok(buf)
get_int!(self, i256)
}
}

Expand Down Expand Up @@ -297,19 +341,25 @@ impl<W1: BufWriter, W2: BufWriter> BufWriter for TeeWriter<W1, W2> {
}

impl<'de> BufReader<'de> for &'de [u8] {
fn get_slice(&mut self, size: usize) -> Result<&'de [u8], DecodeError> {
#[inline]
fn get_chunk(&mut self, size: usize) -> Option<&'de [u8]> {
// TODO: split_at_checked once our msrv >= 1.80
if self.len() < size {
return Err(DecodeError::BufferLength {
for_type: "[u8]".into(),
expected: size,
given: self.len(),
});
return None;
}
let (ret, rest) = self.split_at(size);
*self = rest;
Ok(ret)
Some(ret)
}

#[inline]
fn get_array_chunk<const N: usize>(&mut self) -> Option<&'de [u8; N]> {
let (ret, rest) = self.split_first_chunk()?;
*self = rest;
Some(ret)
}

#[inline(always)]
fn remaining(&self) -> usize {
self.len()
}
Expand All @@ -334,19 +384,28 @@ impl<I> Cursor<I> {
}

impl<'de, I: AsRef<[u8]>> BufReader<'de> for &'de Cursor<I> {
fn get_slice(&mut self, size: usize) -> Result<&'de [u8], DecodeError> {
#[inline]
fn get_chunk(&mut self, size: usize) -> Option<&'de [u8]> {
// "Read" the slice `buf[pos..size]`.
let buf = &self.buf.as_ref()[self.pos.get()..];
let ret = buf.get(..size).ok_or_else(|| DecodeError::BufferLength {
for_type: "Cursor".into(),
expected: size,
given: buf.len(),
})?;
let ret = buf.get(..size)?;

// Advance the cursor by `size` bytes.
self.pos.set(self.pos.get() + size);

Ok(ret)
Some(ret)
}

#[inline]
fn get_array_chunk<const N: usize>(&mut self) -> Option<&'de [u8; N]> {
// "Read" the slice `buf[pos..size]`.
let buf = &self.buf.as_ref()[self.pos.get()..];
let ret = buf.first_chunk()?;

// Advance the cursor by `size` bytes.
self.pos.set(self.pos.get() + N);

Some(ret)
}

fn remaining(&self) -> usize {
Expand Down
Loading

2 comments on commit 97bff92

@github-actions
Copy link

@github-actions github-actions bot commented on 97bff92 Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Criterion benchmark results

Criterion benchmark report

YOU SHOULD PROBABLY IGNORE THESE RESULTS.

Criterion is a wall time based benchmarking system that is extremely noisy when run on CI. We collect these results for longitudinal analysis, but they are not reliable for comparing individual PRs.

Go look at the callgrind report instead.

empty

db on disk new latency old latency new throughput old throughput
sqlite 💿 410.9±2.66ns 421.3±1.80ns - -
sqlite 🧠 402.2±1.31ns 413.6±1.18ns - -
stdb_raw 💿 768.7±1.04ns 779.3±3.76ns - -
stdb_raw 🧠 768.6±1.13ns 777.7±1.16ns - -

insert_1

db on disk schema indices preload new latency old latency new throughput old throughput

insert_bulk

db on disk schema indices preload count new latency old latency new throughput old throughput
sqlite 💿 u32_u64_str btree_each_column 2048 256 589.4±1.15µs 584.6±0.45µs 1696 tx/sec 1710 tx/sec
sqlite 💿 u32_u64_str unique_0 2048 256 144.5±0.36µs 150.0±0.52µs 6.8 Ktx/sec 6.5 Ktx/sec
sqlite 💿 u32_u64_u64 btree_each_column 2048 256 468.8±0.80µs 480.0±42.66µs 2.1 Ktx/sec 2.0 Ktx/sec
sqlite 💿 u32_u64_u64 unique_0 2048 256 136.1±0.43µs 136.6±1.01µs 7.2 Ktx/sec 7.1 Ktx/sec
sqlite 🧠 u32_u64_str btree_each_column 2048 256 451.7±3.21µs 447.6±3.58µs 2.2 Ktx/sec 2.2 Ktx/sec
sqlite 🧠 u32_u64_str unique_0 2048 256 118.7±2.03µs 126.9±0.36µs 8.2 Ktx/sec 7.7 Ktx/sec
sqlite 🧠 u32_u64_u64 btree_each_column 2048 256 367.6±0.32µs 368.0±0.24µs 2.7 Ktx/sec 2.7 Ktx/sec
sqlite 🧠 u32_u64_u64 unique_0 2048 256 106.2±0.28µs 104.6±0.54µs 9.2 Ktx/sec 9.3 Ktx/sec
stdb_raw 💿 u32_u64_str btree_each_column 2048 256 598.2±27.53µs 601.7±20.50µs 1671 tx/sec 1661 tx/sec
stdb_raw 💿 u32_u64_str unique_0 2048 256 488.7±23.23µs 473.7±19.44µs 2046 tx/sec 2.1 Ktx/sec
stdb_raw 💿 u32_u64_u64 btree_each_column 2048 256 378.8±7.32µs 376.0±9.37µs 2.6 Ktx/sec 2.6 Ktx/sec
stdb_raw 💿 u32_u64_u64 unique_0 2048 256 349.6±11.26µs 343.8±8.88µs 2.8 Ktx/sec 2.8 Ktx/sec
stdb_raw 🧠 u32_u64_str btree_each_column 2048 256 299.5±0.31µs 297.9±0.21µs 3.3 Ktx/sec 3.3 Ktx/sec
stdb_raw 🧠 u32_u64_str unique_0 2048 256 231.5±0.60µs 228.8±0.41µs 4.2 Ktx/sec 4.3 Ktx/sec
stdb_raw 🧠 u32_u64_u64 btree_each_column 2048 256 240.2±0.39µs 239.1±0.20µs 4.1 Ktx/sec 4.1 Ktx/sec
stdb_raw 🧠 u32_u64_u64 unique_0 2048 256 209.2±0.39µs 207.0±0.25µs 4.7 Ktx/sec 4.7 Ktx/sec

iterate

db on disk schema indices new latency old latency new throughput old throughput
sqlite 💿 u32_u64_str unique_0 22.9±0.12µs 23.4±0.11µs 42.7 Ktx/sec 41.8 Ktx/sec
sqlite 💿 u32_u64_u64 unique_0 21.9±0.13µs 22.1±0.06µs 44.7 Ktx/sec 44.2 Ktx/sec
sqlite 🧠 u32_u64_str unique_0 20.1±0.15µs 20.9±0.08µs 48.6 Ktx/sec 46.6 Ktx/sec
sqlite 🧠 u32_u64_u64 unique_0 19.1±0.10µs 19.5±0.08µs 51.0 Ktx/sec 50.2 Ktx/sec
stdb_raw 💿 u32_u64_str unique_0 4.9±0.00µs 4.0±0.00µs 200.1 Ktx/sec 245.9 Ktx/sec
stdb_raw 💿 u32_u64_u64 unique_0 4.8±0.00µs 3.9±0.00µs 204.4 Ktx/sec 251.8 Ktx/sec
stdb_raw 🧠 u32_u64_str unique_0 4.9±0.00µs 4.0±0.00µs 199.7 Ktx/sec 246.0 Ktx/sec
stdb_raw 🧠 u32_u64_u64 unique_0 4.8±0.00µs 3.9±0.00µs 204.4 Ktx/sec 251.8 Ktx/sec

find_unique

db on disk key type preload new latency old latency new throughput old throughput

filter

db on disk key type index strategy load count new latency old latency new throughput old throughput
sqlite 💿 string index 2048 256 72.5±0.15µs 70.4±0.35µs 13.5 Ktx/sec 13.9 Ktx/sec
sqlite 💿 u64 index 2048 256 67.9±0.25µs 66.9±0.12µs 14.4 Ktx/sec 14.6 Ktx/sec
sqlite 🧠 string index 2048 256 68.3±0.18µs 66.0±0.16µs 14.3 Ktx/sec 14.8 Ktx/sec
sqlite 🧠 u64 index 2048 256 61.4±0.25µs 60.0±0.28µs 15.9 Ktx/sec 16.3 Ktx/sec
stdb_raw 💿 string index 2048 256 4.9±0.00µs 5.0±0.00µs 198.2 Ktx/sec 195.7 Ktx/sec
stdb_raw 💿 u64 index 2048 256 4.9±0.00µs 4.9±0.00µs 199.9 Ktx/sec 198.8 Ktx/sec
stdb_raw 🧠 string index 2048 256 4.9±0.00µs 5.0±0.00µs 198.4 Ktx/sec 195.5 Ktx/sec
stdb_raw 🧠 u64 index 2048 256 4.9±0.00µs 4.9±0.00µs 200.1 Ktx/sec 198.7 Ktx/sec

serialize

schema format count new latency old latency new throughput old throughput
u32_u64_str bflatn_to_bsatn_fast_path 100 3.6±0.00µs 3.9±0.01µs 26.6 Mtx/sec 24.7 Mtx/sec
u32_u64_str bflatn_to_bsatn_slow_path 100 3.2±0.00µs 3.5±0.01µs 30.2 Mtx/sec 27.0 Mtx/sec
u32_u64_str bsatn 100 15.3±0.05ns 15.6±0.17ns 6.1 Gtx/sec 6.0 Gtx/sec
u32_u64_str bsatn 100 2.2±0.03µs 2.4±0.00µs 43.9 Mtx/sec 40.4 Mtx/sec
u32_u64_str json 100 5.0±0.03µs 5.0±0.03µs 19.1 Mtx/sec 19.2 Mtx/sec
u32_u64_str json 100 8.9±0.10µs 9.6±0.02µs 10.8 Mtx/sec 10.0 Mtx/sec
u32_u64_str product_value 100 1021.6±0.61ns 1019.1±1.23ns 93.3 Mtx/sec 93.6 Mtx/sec
u32_u64_u64 bflatn_to_bsatn_fast_path 100 946.1±8.58ns 1010.7±8.08ns 100.8 Mtx/sec 94.4 Mtx/sec
u32_u64_u64 bflatn_to_bsatn_slow_path 100 2.4±0.00µs 2.8±0.00µs 39.6 Mtx/sec 34.5 Mtx/sec
u32_u64_u64 bsatn 100 15.1±0.11ns 14.8±0.01ns 6.2 Gtx/sec 6.3 Gtx/sec
u32_u64_u64 bsatn 100 1658.1±62.51ns 1826.3±19.44ns 57.5 Mtx/sec 52.2 Mtx/sec
u32_u64_u64 json 100 3.2±0.08µs 3.4±0.04µs 29.9 Mtx/sec 27.8 Mtx/sec
u32_u64_u64 json 100 6.3±0.11µs 5.9±0.08µs 15.2 Mtx/sec 16.3 Mtx/sec
u32_u64_u64 product_value 100 1015.5±0.70ns 1015.7±0.71ns 93.9 Mtx/sec 93.9 Mtx/sec
u64_u64_u32 bflatn_to_bsatn_fast_path 100 717.8±4.24ns 753.0±0.32ns 132.9 Mtx/sec 126.6 Mtx/sec
u64_u64_u32 bflatn_to_bsatn_slow_path 100 2.4±0.00µs 2.8±0.00µs 39.6 Mtx/sec 34.3 Mtx/sec
u64_u64_u32 bsatn 100 1593.4±21.76ns 1715.6±19.68ns 59.9 Mtx/sec 55.6 Mtx/sec
u64_u64_u32 bsatn 100 643.6±2.58ns 923.9±3.27ns 148.2 Mtx/sec 103.2 Mtx/sec
u64_u64_u32 json 100 3.4±0.04µs 3.3±0.17µs 28.3 Mtx/sec 29.0 Mtx/sec
u64_u64_u32 json 100 6.2±0.14µs 5.8±0.01µs 15.4 Mtx/sec 16.4 Mtx/sec
u64_u64_u32 product_value 100 1015.5±0.40ns 1014.6±0.68ns 93.9 Mtx/sec 94.0 Mtx/sec

stdb_module_large_arguments

arg size new latency old latency new throughput old throughput
64KiB 103.9±7.26µs 102.2±10.40µs - -

stdb_module_print_bulk

line count new latency old latency new throughput old throughput
1 55.7±7.50µs 52.4±6.81µs - -
100 603.2±6.21µs 598.3±12.12µs - -
1000 5.4±0.12ms 4.7±0.80ms - -

remaining

name new latency old latency new throughput old throughput
special/db_game/circles/load=10 44.2±6.05ms 51.3±2.38ms - -
special/db_game/circles/load=100 54.3±3.87ms 36.1±1.22ms - -
special/db_game/ia_loop/load=500 147.2±3.03ms 148.1±0.81ms - -
special/db_game/ia_loop/load=5000 5.4±0.02s 5.3±0.05s - -
sqlite/💿/update_bulk/u32_u64_str/unique_0/load=2048/count=256 58.9±6.87µs 54.3±0.20µs 16.6 Ktx/sec 18.0 Ktx/sec
sqlite/💿/update_bulk/u32_u64_u64/unique_0/load=2048/count=256 49.6±0.19µs 46.3±0.09µs 19.7 Ktx/sec 21.1 Ktx/sec
sqlite/🧠/update_bulk/u32_u64_str/unique_0/load=2048/count=256 42.1±0.48µs 39.6±0.13µs 23.2 Ktx/sec 24.7 Ktx/sec
sqlite/🧠/update_bulk/u32_u64_u64/unique_0/load=2048/count=256 38.6±0.17µs 34.7±0.11µs 25.3 Ktx/sec 28.1 Ktx/sec
stdb_module/💿/update_bulk/u32_u64_str/unique_0/load=2048/count=256 1267.5±45.16µs 1242.6±13.61µs 788 tx/sec 804 tx/sec
stdb_module/💿/update_bulk/u32_u64_u64/unique_0/load=2048/count=256 995.2±4.01µs 998.1±5.79µs 1004 tx/sec 1001 tx/sec
stdb_raw/💿/update_bulk/u32_u64_str/unique_0/load=2048/count=256 622.7±21.94µs 615.3±41.19µs 1605 tx/sec 1625 tx/sec
stdb_raw/💿/update_bulk/u32_u64_u64/unique_0/load=2048/count=256 465.7±30.44µs 480.8±7.76µs 2.1 Ktx/sec 2.0 Ktx/sec
stdb_raw/🧠/update_bulk/u32_u64_str/unique_0/load=2048/count=256 371.9±0.31µs 363.1±0.20µs 2.6 Ktx/sec 2.7 Ktx/sec
stdb_raw/🧠/update_bulk/u32_u64_u64/unique_0/load=2048/count=256 335.0±0.53µs 323.5±0.46µs 2.9 Ktx/sec 3.0 Ktx/sec

@github-actions
Copy link

@github-actions github-actions bot commented on 97bff92 Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callgrind benchmark results

Callgrind Benchmark Report

These benchmarks were run using callgrind,
an instruction-level profiler. They allow comparisons between sqlite (sqlite), SpacetimeDB running through a module (stdb_module), and the underlying SpacetimeDB data storage engine (stdb_raw). Callgrind emulates a CPU to collect the below estimates.

Measurement changes larger than five percent are in bold.

In-memory benchmarks

callgrind: empty transaction

db total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw 6397 6397 0.00% 6493 6527 -0.52%
sqlite 5579 5589 -0.18% 5993 6117 -2.03%

callgrind: filter

db schema indices count preload _column data_type total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str no_index 64 128 1 u64 76591 76591 0.00% 77063 77031 0.04%
stdb_raw u32_u64_str no_index 64 128 2 string 119089 120178 -0.91% 119799 120932 -0.94%
stdb_raw u32_u64_str btree_each_column 64 128 2 string 25081 25081 0.00% 25631 25735 -0.40%
stdb_raw u32_u64_str btree_each_column 64 128 1 u64 24049 24049 0.00% 24489 24609 -0.49%
sqlite u32_u64_str no_index 64 128 2 string 144695 144695 0.00% 146111 146181 -0.05%
sqlite u32_u64_str no_index 64 128 1 u64 124044 124044 0.00% 125254 125272 -0.01%
sqlite u32_u64_str btree_each_column 64 128 1 u64 131361 131361 0.00% 132787 132857 -0.05%
sqlite u32_u64_str btree_each_column 64 128 2 string 134494 134494 0.00% 136058 136176 -0.09%

callgrind: insert bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 64 128 874291 878993 -0.53% 921185 934437 -1.42%
stdb_raw u32_u64_str btree_each_column 64 128 1020301 1031446 -1.08% 1076833 1098746 -1.99%
sqlite u32_u64_str unique_0 64 128 398320 398320 0.00% 415524 414994 0.13%
sqlite u32_u64_str btree_each_column 64 128 983637 983637 0.00% 1022671 1021691 0.10%

callgrind: iterate

db schema indices count total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 153724 153724 0.00% 153840 153866 -0.02%
stdb_raw u32_u64_str unique_0 64 16749 16749 0.00% 16845 16871 -0.15%
sqlite u32_u64_str unique_0 1024 1067273 1067255 0.00% 1070569 1070691 -0.01%
sqlite u32_u64_str unique_0 64 76201 76201 0.00% 77195 77335 -0.18%

callgrind: serialize_product_value

count format total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
64 json 47528 47528 0.00% 50286 50180 0.21%
64 bsatn 25509 25509 0.00% 27787 27719 0.25%
16 bsatn 8200 8200 0.00% 9594 9526 0.71%
16 json 12188 12188 0.00% 14194 14092 0.72%

callgrind: update bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 1024 20011681 20120482 -0.54% 20483199 20703592 -1.06%
stdb_raw u32_u64_str unique_0 64 128 1281611 1287068 -0.42% 1347429 1329642 1.34%
sqlite u32_u64_str unique_0 1024 1024 1802150 1802182 -0.00% 1811422 1811474 -0.00%
sqlite u32_u64_str unique_0 64 128 128496 128528 -0.02% 131432 131416 0.01%
On-disk benchmarks

callgrind: empty transaction

db total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw 6402 6402 0.00% 6518 6536 -0.28%
sqlite 5621 5621 0.00% 6103 6175 -1.17%

callgrind: filter

db schema indices count preload _column data_type total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str no_index 64 128 1 u64 76596 76596 0.00% 77068 77012 0.07%
stdb_raw u32_u64_str no_index 64 128 2 string 119094 119094 0.00% 119752 119764 -0.01%
stdb_raw u32_u64_str btree_each_column 64 128 2 string 25087 25086 0.00% 25621 25728 -0.42%
stdb_raw u32_u64_str btree_each_column 64 128 1 u64 24054 24054 0.00% 24462 24582 -0.49%
sqlite u32_u64_str no_index 64 128 1 u64 125965 125971 -0.00% 127523 127555 -0.03%
sqlite u32_u64_str no_index 64 128 2 string 146616 146616 0.00% 148376 148458 -0.06%
sqlite u32_u64_str btree_each_column 64 128 2 string 136616 136616 0.00% 138690 138824 -0.10%
sqlite u32_u64_str btree_each_column 64 128 1 u64 133457 133457 0.00% 135293 135343 -0.04%

callgrind: insert bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 64 128 823495 828159 -0.56% 869571 882163 -1.43%
stdb_raw u32_u64_str btree_each_column 64 128 970897 978051 -0.73% 1027081 1042435 -1.47%
sqlite u32_u64_str unique_0 64 128 415857 415857 0.00% 432359 431843 0.12%
sqlite u32_u64_str btree_each_column 64 128 1021898 1021898 0.00% 1059248 1059298 -0.00%

callgrind: iterate

db schema indices count total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 153729 153729 0.00% 153821 153851 -0.02%
stdb_raw u32_u64_str unique_0 64 16754 16754 0.00% 16846 16872 -0.15%
sqlite u32_u64_str unique_0 1024 1070323 1070323 0.00% 1074061 1074105 -0.00%
sqlite u32_u64_str unique_0 64 77973 77973 0.00% 79167 79263 -0.12%

callgrind: serialize_product_value

count format total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
64 json 47528 47528 0.00% 50286 50180 0.21%
64 bsatn 25509 25509 0.00% 27787 27719 0.25%
16 bsatn 8200 8200 0.00% 9594 9526 0.71%
16 json 12188 12188 0.00% 14194 14092 0.72%

callgrind: update bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 1024 18942249 19035674 -0.49% 19502989 19691388 -0.96%
stdb_raw u32_u64_str unique_0 64 128 1234531 1239755 -0.42% 1299567 1312385 -0.98%
sqlite u32_u64_str unique_0 1024 1024 1809711 1809743 -0.00% 1818439 1818423 0.00%
sqlite u32_u64_str unique_0 64 128 132622 132654 -0.02% 135634 135606 0.02%

Please sign in to comment.