//! Benchmarks of QMDB variants on fixed-size values. //! //! While this benchmark involves updating a database with fixed-size values, we also include the db //! variants capable of handling variable-size values to gauge the impact of the extra indirection //! they perform. use commonware_cryptography::{Hasher, Sha256}; use commonware_runtime::{buffer::paged::CacheRef, tokio::Context, BufferPooler, ThreadPooler}; use commonware_storage::{ qmdb::{ any::{ ordered::{fixed::Db as OFixed, variable::Db as OVariable}, traits::{DbAny, MerkleizedBatch as _, UnmerkleizedBatch as _}, unordered::{fixed::Db as UFixed, variable::Db as UVariable}, FixedConfig as AConfig, VariableConfig as VariableAnyConfig, }, current::{ ordered::{fixed::Db as OCurrent, variable::Db as OVCurrent}, unordered::{fixed::Db as UCurrent, variable::Db as UVCurrent}, FixedConfig as CConfig, VariableConfig as VariableCurrentConfig, }, store::LogStore, }, translator::EightCap, }; use commonware_utils::{NZUsize, NZU16, NZU64}; use rand::{rngs::StdRng, RngCore, SeedableRng}; use std::num::{NonZeroU16, NonZeroU64, NonZeroUsize}; pub mod generate; pub mod init; pub type Digest = ::Digest; #[derive(Debug, Clone, Copy)] enum Variant { AnyUnorderedFixed, AnyOrderedFixed, AnyUnorderedVariable, AnyOrderedVariable, CurrentUnorderedFixed, CurrentOrderedFixed, CurrentUnorderedVariable, CurrentOrderedVariable, } impl Variant { pub const fn name(&self) -> &'static str { match self { Self::AnyUnorderedFixed => "any::unordered::fixed", Self::AnyOrderedFixed => "any::ordered::fixed", Self::AnyUnorderedVariable => "any::unordered::variable", Self::AnyOrderedVariable => "any::ordered::variable", Self::CurrentUnorderedFixed => "current::unordered::fixed", Self::CurrentOrderedFixed => "current::ordered::fixed", Self::CurrentUnorderedVariable => "current::unordered::variable", Self::CurrentOrderedVariable => "current::ordered::variable", } } } const VARIANTS: [Variant; 8] = [ Variant::AnyUnorderedFixed, Variant::AnyOrderedFixed, Variant::AnyUnorderedVariable, Variant::AnyOrderedVariable, Variant::CurrentUnorderedFixed, Variant::CurrentOrderedFixed, Variant::CurrentUnorderedVariable, Variant::CurrentOrderedVariable, ]; const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(50_000); const PARTITION_SUFFIX: &str = "any-fixed-bench-partition"; /// Chunk size for the current QMDB bitmap - must be a power of 2 (as assumed in /// current::grafting_height()) and a multiple of digest size. const CHUNK_SIZE: usize = 32; /// Threads (cores) to use for parallelization. We pick 8 since our benchmarking pipeline is /// configured to provide 8 cores. const THREADS: NonZeroUsize = NZUsize!(8); /// Use a "prod sized" page size to test the performance of the journal. const PAGE_SIZE: NonZeroU16 = NZU16!(16384); /// The number of pages to cache in the page cache. const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10_000); /// Default delete frequency (1/10th of the updates will be deletes). const DELETE_FREQUENCY: u32 = 10; /// Default write buffer size. const WRITE_BUFFER_SIZE: NonZeroUsize = NZUsize!(1024); /// Db type aliases for Any databases. type UFixedDb = UFixed; type OFixedDb = OFixed; type UVAnyDb = UVariable; type OVAnyDb = OVariable; type UCurrentDb = UCurrent; type OCurrentDb = OCurrent; type UVCurrentDb = UVCurrent; type OVCurrentDb = OVCurrent; /// Configuration for any QMDB. fn any_cfg(context: &(impl BufferPooler + ThreadPooler)) -> AConfig { AConfig:: { mmr_journal_partition: format!("journal-{PARTITION_SUFFIX}"), mmr_metadata_partition: format!("metadata-{PARTITION_SUFFIX}"), mmr_items_per_blob: ITEMS_PER_BLOB, mmr_write_buffer: WRITE_BUFFER_SIZE, log_journal_partition: format!("log-journal-{PARTITION_SUFFIX}"), log_items_per_blob: ITEMS_PER_BLOB, log_write_buffer: WRITE_BUFFER_SIZE, translator: EightCap, thread_pool: Some(context.create_thread_pool(THREADS).unwrap()), page_cache: CacheRef::from_pooler(context, PAGE_SIZE, PAGE_CACHE_SIZE), } } /// Configuration for current QMDB. fn current_cfg(context: &(impl BufferPooler + ThreadPooler)) -> CConfig { CConfig:: { mmr_journal_partition: format!("journal-{PARTITION_SUFFIX}"), mmr_metadata_partition: format!("metadata-{PARTITION_SUFFIX}"), mmr_items_per_blob: ITEMS_PER_BLOB, mmr_write_buffer: WRITE_BUFFER_SIZE, log_journal_partition: format!("log-journal-{PARTITION_SUFFIX}"), log_items_per_blob: ITEMS_PER_BLOB, log_write_buffer: WRITE_BUFFER_SIZE, grafted_mmr_metadata_partition: format!("grafted-mmr-metadata-{PARTITION_SUFFIX}"), translator: EightCap, thread_pool: Some(context.create_thread_pool(THREADS).unwrap()), page_cache: CacheRef::from_pooler(context, PAGE_SIZE, PAGE_CACHE_SIZE), } } fn variable_any_cfg( context: &(impl BufferPooler + ThreadPooler), ) -> VariableAnyConfig { VariableAnyConfig:: { mmr_journal_partition: format!("journal-{PARTITION_SUFFIX}"), mmr_metadata_partition: format!("metadata-{PARTITION_SUFFIX}"), mmr_items_per_blob: ITEMS_PER_BLOB, mmr_write_buffer: WRITE_BUFFER_SIZE, log_partition: format!("log-journal-{PARTITION_SUFFIX}"), log_codec_config: ((), ()), log_items_per_blob: ITEMS_PER_BLOB, log_write_buffer: WRITE_BUFFER_SIZE, log_compression: None, translator: EightCap, thread_pool: Some(context.create_thread_pool(THREADS).unwrap()), page_cache: CacheRef::from_pooler(context, PAGE_SIZE, PAGE_CACHE_SIZE), } } /// Configuration for variable current QMDB. fn variable_current_cfg( context: &(impl BufferPooler + ThreadPooler), ) -> VariableCurrentConfig { VariableCurrentConfig:: { mmr_journal_partition: format!("journal-{PARTITION_SUFFIX}"), mmr_metadata_partition: format!("metadata-{PARTITION_SUFFIX}"), mmr_items_per_blob: ITEMS_PER_BLOB, mmr_write_buffer: WRITE_BUFFER_SIZE, log_partition: format!("log-journal-{PARTITION_SUFFIX}"), log_codec_config: ((), ()), log_items_per_blob: ITEMS_PER_BLOB, log_write_buffer: WRITE_BUFFER_SIZE, log_compression: None, grafted_mmr_metadata_partition: format!("grafted-mmr-metadata-{PARTITION_SUFFIX}"), translator: EightCap, thread_pool: Some(context.create_thread_pool(THREADS).unwrap()), page_cache: CacheRef::from_pooler(context, PAGE_SIZE, PAGE_CACHE_SIZE), } } /// Get an unordered fixed Any QMDB instance instance. async fn get_any_unordered_fixed(ctx: Context) -> UFixedDb { let any_cfg = any_cfg(&ctx); UFixedDb::init(ctx, any_cfg).await.unwrap() } /// Get an ordered fixed Any QMDB instance instance. async fn get_any_ordered_fixed(ctx: Context) -> OFixedDb { let any_cfg = any_cfg(&ctx); OFixedDb::init(ctx, any_cfg).await.unwrap() } /// Get an unordered variable Any QMDB instance instance. async fn get_any_unordered_variable(ctx: Context) -> UVAnyDb { let variable_any_cfg = variable_any_cfg(&ctx); UVAnyDb::init(ctx, variable_any_cfg).await.unwrap() } /// Get an ordered variable Any QMDB instance instance. async fn get_any_ordered_variable(ctx: Context) -> OVAnyDb { let variable_any_cfg = variable_any_cfg(&ctx); OVAnyDb::init(ctx, variable_any_cfg).await.unwrap() } /// Get an unordered current QMDB instance. async fn get_current_unordered_fixed(ctx: Context) -> UCurrentDb { let current_cfg = current_cfg(&ctx); UCurrent::<_, _, _, Sha256, EightCap, CHUNK_SIZE>::init(ctx, current_cfg) .await .unwrap() } /// Get an ordered current QMDB instance. async fn get_current_ordered_fixed(ctx: Context) -> OCurrentDb { let current_cfg = current_cfg(&ctx); OCurrent::<_, _, _, Sha256, EightCap, CHUNK_SIZE>::init(ctx, current_cfg) .await .unwrap() } /// Get an unordered variable current QMDB instance. async fn get_current_unordered_variable(ctx: Context) -> UVCurrentDb { let variable_current_cfg = variable_current_cfg(&ctx); UVCurrent::<_, _, _, Sha256, EightCap, CHUNK_SIZE>::init(ctx, variable_current_cfg) .await .unwrap() } /// Get an ordered variable current QMDB instance. async fn get_current_ordered_variable(ctx: Context) -> OVCurrentDb { let variable_current_cfg = variable_current_cfg(&ctx); OVCurrent::<_, _, _, Sha256, EightCap, CHUNK_SIZE>::init(ctx, variable_current_cfg) .await .unwrap() } /// Generate a large db with random data. The function seeds the db with exactly `num_elements` /// elements by inserting them in order, each with a new random value. Then, it performs /// `num_operations` over these elements, each selected uniformly at random for each operation. The /// ratio of updates to deletes is configured with `DELETE_FREQUENCY`. The database is committed /// after every `commit_frequency` operations (if Some), or at the end (if None). async fn gen_random_kv( db: &mut M, num_elements: u64, num_operations: u64, commit_frequency: Option, ) where M: DbAny + LogStore, { let mut rng = StdRng::seed_from_u64(42); // Seed the db with `num_elements` entries. { let mut batch = db.new_batch(); for i in 0u64..num_elements { let k = Sha256::hash(&i.to_be_bytes()); let v = Sha256::hash(&rng.next_u32().to_be_bytes()); batch.write(k, Some(v)); } let finalized = batch.merkleize(None).await.unwrap().finalize(); db.apply_batch(finalized).await.unwrap(); } // Perform `num_operations` random updates/deletes, committing periodically. { let mut batch = db.new_batch(); for _ in 0u64..num_operations { let rand_key = Sha256::hash(&(rng.next_u64() % num_elements).to_be_bytes()); if rng.next_u32() % DELETE_FREQUENCY == 0 { batch.write(rand_key, None); continue; } let v = Sha256::hash(&rng.next_u32().to_be_bytes()); batch.write(rand_key, Some(v)); if let Some(freq) = commit_frequency { if rng.next_u32() % freq == 0 { let finalized = batch.merkleize(None).await.unwrap().finalize(); db.apply_batch(finalized).await.unwrap(); batch = db.new_batch(); } } } let finalized = batch.merkleize(None).await.unwrap().finalize(); db.apply_batch(finalized).await.unwrap(); } }