From a47486696b4b776cf1a3201862621e104b836276 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 23 Oct 2023 17:08:41 +0300 Subject: [PATCH] Update gas benchmarks for some storage opcodes (#1408) Closes #1239. Closes #1255. --------- Co-authored-by: Green Baneling Co-authored-by: Brandon Vrooman --- CHANGELOG.md | 1 + benches/benches/vm_set/blockchain.rs | 258 +++++++++++-------- benches/src/lib.rs | 22 +- crates/fuel-core/src/database.rs | 24 +- crates/fuel-core/src/database/vm_database.rs | 7 + crates/fuel-core/src/state/rocks_db.rs | 5 + 6 files changed, 197 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a5abb65272..379fdbc57f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ Description of the upcoming release here. - [#1392](https://github.com/FuelLabs/fuel-core/pull/1392): Fixed an overflow in `message_proof`. - [#1393](https://github.com/FuelLabs/fuel-core/pull/1393): Increase heartbeat timeout from `2` to `60` seconds, as suggested in [this issue](https://github.com/FuelLabs/fuel-core/issues/1330). - [#1395](https://github.com/FuelLabs/fuel-core/pull/1395): Add DependentCost benchmarks for `k256`, `s256` and `mcpi` instructions. +- [#1408](https://github.com/FuelLabs/fuel-core/pull/1408): Update gas benchmarks for storage opcodes to use a pre-populated database to get more accurate worst-case costs. #### Breaking - [#1432](https://github.com/FuelLabs/fuel-core/pull/1432): All subscriptions and requests have a TTL now. So each subscription lifecycle is limited in time. If the subscription is closed because of TTL, it means that you subscribed after your transaction had been dropped by the network. diff --git a/benches/benches/vm_set/blockchain.rs b/benches/benches/vm_set/blockchain.rs index 5fb5b59da9f..f0cb9d63658 100644 --- a/benches/benches/vm_set/blockchain.rs +++ b/benches/benches/vm_set/blockchain.rs @@ -1,4 +1,9 @@ -use std::iter::successors; +use std::{ + env, + iter::successors, + path::PathBuf, + sync::Arc, +}; use super::run_group_ref; @@ -6,9 +11,11 @@ use criterion::{ Criterion, Throughput, }; -use fuel_core::database::vm_database::VmDatabase; +use fuel_core::{ + database::vm_database::VmDatabase, + state::rocks_db::RocksDb, +}; use fuel_core_benches::*; -use fuel_core_storage::ContractsAssetsStorage; use fuel_core_types::{ fuel_asm::{ op, @@ -16,15 +23,13 @@ use fuel_core_types::{ RegId, }, fuel_tx::{ + ContractIdExt, Input, Output, Word, }, fuel_types::*, - fuel_vm::{ - consts::*, - InterpreterStorage, - }, + fuel_vm::consts::*, }; use rand::{ rngs::StdRng, @@ -32,6 +37,87 @@ use rand::{ SeedableRng, }; +/// Reimplementation of `tempdir::TempDir` that allows creating a new +/// instance without actually creating a new directory on the filesystem. +/// This is needed since rocksdb requires empty directory for checkpoints. +pub struct ShallowTempDir { + path: PathBuf, +} +impl ShallowTempDir { + pub fn new() -> Self { + let mut rng = rand::thread_rng(); + let mut path = env::temp_dir(); + path.push(format!("fuel-core-bench-rocksdb-{}", rng.next_u64())); + Self { path } + } +} +impl Drop for ShallowTempDir { + fn drop(&mut self) { + // Ignore errors + let _ = std::fs::remove_dir_all(&self.path); + } +} + +pub struct BenchDb { + db: RocksDb, + /// Used for RAII cleanup. Contents of this directory are deleted on drop. + _tmp_dir: ShallowTempDir, +} + +impl BenchDb { + const STATE_SIZE: u64 = 10_000_000; + + fn new(contract: &ContractId) -> anyhow::Result { + let tmp_dir = ShallowTempDir::new(); + + let db = Arc::new(RocksDb::default_open(&tmp_dir.path, None).unwrap()); + + let mut database = Database::new(db.clone()); + database.init_contract_state( + contract, + (0..Self::STATE_SIZE).map(|k| { + let mut key = Bytes32::zeroed(); + key.as_mut()[..8].copy_from_slice(&k.to_be_bytes()); + (key, key) + }), + )?; + database.init_contract_balances( + &ContractId::zeroed(), + (0..Self::STATE_SIZE).map(|k| { + let key = k / 2; + let mut sub_id = Bytes32::zeroed(); + sub_id.as_mut()[..8].copy_from_slice(&key.to_be_bytes()); + + let asset = if k % 2 == 0 { + VmBench::CONTRACT.asset_id(&sub_id) + } else { + AssetId::new(*sub_id) + }; + (asset, key + 1_000) + }), + )?; + + drop(database); // Drops one reference to the db wrapper, but we still hold the last one + Ok(Self { + _tmp_dir: tmp_dir, + db: Arc::into_inner(db).expect("All other references must be dropped"), + }) + } + + /// Create a new separate database instance using a rocksdb checkpoint + fn checkpoint(&self) -> VmDatabase { + let tmp_dir = ShallowTempDir::new(); + self.db + .checkpoint(&tmp_dir.path) + .expect("Unable to create checkpoint"); + let db = RocksDb::default_open(&tmp_dir.path, None).unwrap(); + let database = Database::new(Arc::new(db)).with_drop(Box::new(move || { + drop(tmp_dir); + })); + VmDatabase::default_from_database(database) + } +} + pub fn run(c: &mut Criterion) { let rng = &mut StdRng::seed_from_u64(2322u64); @@ -42,9 +128,12 @@ pub fn run(c: &mut Criterion) { .collect::>(); l.sort_unstable(); linear.extend(l); + let asset: AssetId = rng.gen(); let contract: ContractId = rng.gen(); + let db = BenchDb::new(&contract).expect("Unable to fill contract storage"); + run_group_ref( &mut c.benchmark_group("bal"), "bal", @@ -54,45 +143,24 @@ pub fn run(c: &mut Criterion) { op::gtf_args(0x10, 0x00, GTFArgs::ScriptData), op::addi(0x11, 0x10, asset.len().try_into().unwrap()), ]) - .with_dummy_contract(contract) - .with_prepare_db(move |mut db| { - let mut asset_inc = AssetId::zeroed(); - - asset_inc.as_mut()[..8].copy_from_slice(&1_u64.to_be_bytes()); - - db.merkle_contract_asset_id_balance_insert(&contract, &asset_inc, 1)?; - - db.merkle_contract_asset_id_balance_insert(&contract, &asset, 100)?; - - Ok(db) - }), + .with_dummy_contract(contract), ); run_group_ref( &mut c.benchmark_group("sww"), "sww", - VmBench::contract(rng, op::sww(RegId::ZERO, 0x29, RegId::ONE)) - .expect("failed to prepare contract") - .with_prepare_db(move |mut db| { - let mut key = Bytes32::zeroed(); - - key.as_mut()[..8].copy_from_slice(&1_u64.to_be_bytes()); - - db.merkle_contract_state_insert(&contract, &key, &key)?; - - Ok(db) - }), + VmBench::contract_using_db( + rng, + db.checkpoint(), + op::sww(RegId::ZERO, 0x29, RegId::ONE), + ) + .expect("failed to prepare contract"), ); - { - let mut input = VmBench::contract(rng, op::srw(0x13, 0x14, 0x15)) - .expect("failed to prepare contract") - .with_prepare_db(move |mut db| { - let key = Bytes32::zeroed(); - db.merkle_contract_state_insert(&ContractId::zeroed(), &key, &key)?; - - Ok(db) - }); + { + let mut input = + VmBench::contract_using_db(rng, db.checkpoint(), op::srw(0x13, 0x14, 0x15)) + .expect("failed to prepare contract"); input.prepare_script.extend(vec![op::movi(0x15, 2000)]); run_group_ref(&mut c.benchmark_group("srw"), "srw", input); } @@ -110,21 +178,10 @@ pub fn run(c: &mut Criterion) { op::addi(0x11, 0x11, WORD_SIZE.try_into().unwrap()), op::movi(0x12, i as u32), ]; - let mut bench = VmBench::contract(rng, op::scwq(0x11, 0x29, 0x12)) - .expect("failed to prepare contract") - .with_post_call(post_call) - .with_prepare_db(move |mut db| { - let slots = (0u64..i).map(|key_number| { - let mut key = Bytes32::zeroed(); - key.as_mut()[..8].copy_from_slice(&key_number.to_be_bytes()); - (key, key) - }); - db.database_mut() - .init_contract_state(&contract, slots) - .unwrap(); - - Ok(db) - }); + let mut bench = + VmBench::contract_using_db(rng, db.checkpoint(), op::scwq(0x11, 0x29, 0x12)) + .expect("failed to prepare contract") + .with_post_call(post_call); bench.data.extend(data); scwq.throughput(Throughput::Bytes(i)); @@ -147,21 +204,13 @@ pub fn run(c: &mut Criterion) { op::addi(0x11, 0x11, WORD_SIZE.try_into().unwrap()), op::movi(0x12, i as u32), ]; - let mut bench = VmBench::contract(rng, op::swwq(0x10, 0x11, 0x20, 0x12)) - .expect("failed to prepare contract") - .with_post_call(post_call) - .with_prepare_db(move |mut db| { - let slots = (0u64..i).map(|key_number| { - let mut key = Bytes32::zeroed(); - key.as_mut()[..8].copy_from_slice(&key_number.to_be_bytes()); - (key, key) - }); - db.database_mut() - .init_contract_state(&contract, slots) - .unwrap(); - - Ok(db) - }); + let mut bench = VmBench::contract_using_db( + rng, + db.checkpoint(), + op::swwq(0x10, 0x11, 0x20, 0x12), + ) + .expect("failed to prepare contract") + .with_post_call(post_call); bench.data.extend(data); swwq.throughput(Throughput::Bytes(i)); @@ -203,6 +252,7 @@ pub fn run(c: &mut Criterion) { &mut call, format!("{i}"), VmBench::new(op::call(0x10, RegId::ZERO, 0x11, 0x12)) + .with_db(db.checkpoint()) .with_contract_code(code) .with_data(data) .with_prepare_script(prepare_script), @@ -346,15 +396,20 @@ pub fn run(c: &mut Criterion) { run_group_ref( &mut c.benchmark_group("mint"), "mint", - VmBench::contract(rng, op::mint(RegId::ZERO, RegId::ZERO)) - .expect("failed to prepare contract"), + VmBench::contract_using_db( + rng, + db.checkpoint(), + op::mint(RegId::ONE, RegId::ZERO), + ) + .expect("failed to prepare contract"), ); run_group_ref( &mut c.benchmark_group("burn"), "burn", - VmBench::contract(rng, op::mint(RegId::ZERO, RegId::ZERO)) - .expect("failed to prepare contract"), + VmBench::contract_using_db(rng, db.checkpoint(), op::burn(RegId::ONE, RegId::HP)) + .expect("failed to prepare contract") + .prepend_prepare_script(vec![op::movi(0x10, 32), op::aloc(0x10)]), ); run_group_ref( @@ -368,17 +423,9 @@ pub fn run(c: &mut Criterion) { ); { - let mut input = VmBench::contract(rng, op::tr(0x15, 0x14, 0x15)) - .expect("failed to prepare contract") - .with_prepare_db(move |mut db| { - db.merkle_contract_asset_id_balance_insert( - &ContractId::zeroed(), - &AssetId::zeroed(), - 200, - )?; - - Ok(db) - }); + let mut input = + VmBench::contract_using_db(rng, db.checkpoint(), op::tr(0x15, 0x14, 0x15)) + .expect("failed to prepare contract"); input .prepare_script .extend(vec![op::movi(0x15, 2000), op::movi(0x14, 100)]); @@ -386,17 +433,12 @@ pub fn run(c: &mut Criterion) { } { - let mut input = VmBench::contract(rng, op::tro(0x15, 0x16, 0x14, 0x15)) - .expect("failed to prepare contract") - .with_prepare_db(move |mut db| { - db.merkle_contract_asset_id_balance_insert( - &ContractId::zeroed(), - &AssetId::zeroed(), - 200, - )?; - - Ok(db) - }); + let mut input = VmBench::contract_using_db( + rng, + db.checkpoint(), + op::tro(RegId::ZERO, 0x15, 0x14, RegId::HP), + ) + .expect("failed to prepare contract"); let coin_output = Output::variable(Address::zeroed(), 100, AssetId::zeroed()); input.outputs.push(coin_output); let predicate = op::ret(RegId::ONE).to_bytes().to_vec(); @@ -416,10 +458,16 @@ pub fn run(c: &mut Criterion) { let index = input.outputs.len() - 1; input.prepare_script.extend(vec![ - op::movi(0x15, 2000), op::movi(0x14, 100), - op::movi(0x16, index.try_into().unwrap()), + op::movi(0x15, index.try_into().unwrap()), + op::movi(0x20, 32), + op::aloc(0x20), ]); + for (i, v) in (*AssetId::zeroed()).into_iter().enumerate() { + input.prepare_script.push(op::movi(0x20, v as u32)); + input.prepare_script.push(op::sb(RegId::HP, 0x20, i as u16)); + } + run_group_ref(&mut c.benchmark_group("tro"), "tro", input); } @@ -456,16 +504,12 @@ pub fn run(c: &mut Criterion) { let mut smo = c.benchmark_group("smo"); for i in linear.clone() { - let mut input = VmBench::contract(rng, op::smo(0x15, 0x16, 0x17, 0x18)) - .expect("failed to prepare contract"); - input.prepare_db = Some(Box::new(|mut db: VmDatabase| { - db.merkle_contract_asset_id_balance_insert( - &ContractId::default(), - &AssetId::default(), - Word::MAX, - )?; - Ok(db) - })); + let mut input = VmBench::contract_using_db( + rng, + db.checkpoint(), + op::smo(0x15, 0x16, 0x17, 0x18), + ) + .expect("failed to prepare contract"); input.post_call.extend(vec![ op::gtf_args(0x15, 0x00, GTFArgs::ScriptData), // Offset 32 + 8 + 8 + 32 diff --git a/benches/src/lib.rs b/benches/src/lib.rs index 72738c67af7..38b7c9c0382 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -136,6 +136,17 @@ impl VmBench { } pub fn contract(rng: &mut R, instruction: Instruction) -> anyhow::Result + where + R: Rng, + { + Self::contract_using_db(rng, new_db(), instruction) + } + + pub fn contract_using_db( + rng: &mut R, + mut db: VmDatabase, + instruction: Instruction, + ) -> anyhow::Result where R: Rng, { @@ -160,8 +171,6 @@ impl VmBench { let input = Input::contract(utxo_id, balance_root, state_root, tx_pointer, id); let output = Output::contract(0, rng.gen(), rng.gen()); - let mut db = new_db(); - db.deploy_contract_with_id(&salt, &[], &contract, &state_root, &id)?; let data = id @@ -226,11 +235,20 @@ impl VmBench { self } + /// Replaces the current prepare script with the given one. + /// Not that if you've constructed this instance with `contract` or `using_contract_db`, + /// then this will remove the script added by it. Use `extend_prepare_script` instead. pub fn with_prepare_script(mut self, prepare_script: Vec) -> Self { self.prepare_script = prepare_script; self } + /// Adds more instructions before the current prepare script. + pub fn prepend_prepare_script(mut self, prepare_script: Vec) -> Self { + self.prepare_script.extend(prepare_script); + self + } + pub fn with_post_call(mut self, post_call: Vec) -> Self { self.post_call = post_call; self diff --git a/crates/fuel-core/src/database.rs b/crates/fuel-core/src/database.rs index 7059d92ba46..5f70bd7ab38 100644 --- a/crates/fuel-core/src/database.rs +++ b/crates/fuel-core/src/database.rs @@ -151,22 +151,19 @@ pub struct Database { _drop: Arc, } -trait DropFnTrait: FnOnce() + Send + Sync {} -impl DropFnTrait for F where F: FnOnce() + Send + Sync {} -type DropFn = Box; - -impl fmt::Debug for DropFn { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "DropFn") - } -} - -#[derive(Debug, Default)] +type DropFn = Box; +#[derive(Default)] struct DropResources { // move resources into this closure to have them dropped when db drops drop: Option, } +impl fmt::Debug for DropResources { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "DropResources") + } +} + impl From for DropResources { fn from(closure: F) -> Self { Self { @@ -191,6 +188,11 @@ impl Database { } } + pub fn with_drop(mut self, drop: DropFn) -> Self { + self._drop = Arc::new(drop.into()); + self + } + #[cfg(feature = "rocksdb")] pub fn open(path: &Path, capacity: impl Into>) -> DatabaseResult { use anyhow::Context; diff --git a/crates/fuel-core/src/database/vm_database.rs b/crates/fuel-core/src/database/vm_database.rs index e881093d03f..df4ecf3ebdb 100644 --- a/crates/fuel-core/src/database/vm_database.rs +++ b/crates/fuel-core/src/database/vm_database.rs @@ -86,6 +86,13 @@ impl VmDatabase { } } + pub fn default_from_database(database: Database) -> Self { + Self { + database, + ..Default::default() + } + } + pub fn database_mut(&mut self) -> &mut Database { &mut self.database } diff --git a/crates/fuel-core/src/state/rocks_db.rs b/crates/fuel-core/src/state/rocks_db.rs index 889b6aa63ed..f0dd9074fd4 100644 --- a/crates/fuel-core/src/state/rocks_db.rs +++ b/crates/fuel-core/src/state/rocks_db.rs @@ -21,6 +21,7 @@ use fuel_core_storage::iter::{ IntoBoxedIter, }; use rocksdb::{ + checkpoint::Checkpoint, BoundColumnFamily, Cache, ColumnFamilyDescriptor, @@ -98,6 +99,10 @@ impl RocksDb { Ok(rocks_db) } + pub fn checkpoint>(&self, path: P) -> Result<(), rocksdb::Error> { + Checkpoint::new(&self.db)?.create_checkpoint(path) + } + fn cf(&self, column: Column) -> Arc { self.db .cf_handle(&RocksDb::col_name(column))