From a70da3094eee7df0b4b3221cd15f476fa74fddc3 Mon Sep 17 00:00:00 2001 From: Dmitry Murzin Date: Mon, 16 Sep 2024 23:59:02 +0300 Subject: [PATCH] perf: Persistent executor Signed-off-by: Dmitry Murzin --- crates/iroha_core/benches/validation.rs | 13 +- crates/iroha_core/src/block.rs | 14 +- crates/iroha_core/src/executor.rs | 19 +- crates/iroha_core/src/smartcontracts/wasm.rs | 196 ++++++++++++++---- .../src/smartcontracts/wasm/cache.rs | 85 ++++++++ crates/iroha_core/src/tx.rs | 13 +- 6 files changed, 275 insertions(+), 65 deletions(-) create mode 100644 crates/iroha_core/src/smartcontracts/wasm/cache.rs diff --git a/crates/iroha_core/benches/validation.rs b/crates/iroha_core/benches/validation.rs index eaad96d894..915f9618af 100644 --- a/crates/iroha_core/benches/validation.rs +++ b/crates/iroha_core/benches/validation.rs @@ -6,7 +6,7 @@ use iroha_core::{ block::*, prelude::*, query::store::LiveQueryStore, - smartcontracts::{isi::Registrable as _, Execute}, + smartcontracts::{isi::Registrable as _, wasm::cache::WasmCache, Execute}, state::{State, World}, }; use iroha_data_model::{ @@ -165,12 +165,15 @@ fn validate_transaction(criterion: &mut Criterion) { .expect("Failed to accept transaction."); let mut success_count = 0; let mut failure_count = 0; + let mut wasm_cache = WasmCache::new(); let mut state_block = state.block(unverified_block.header()); let _ = criterion.bench_function("validate", |b| { - b.iter(|| match state_block.validate(transaction.clone()) { - Ok(_) => success_count += 1, - Err(_) => failure_count += 1, - }); + b.iter( + || match state_block.validate(transaction.clone(), &mut wasm_cache) { + Ok(_) => success_count += 1, + Err(_) => failure_count += 1, + }, + ); }); state_block.commit(); println!("Success count: {success_count}, Failure count: {failure_count}"); diff --git a/crates/iroha_core/src/block.rs b/crates/iroha_core/src/block.rs index 3de376e638..da32ec7284 100644 --- a/crates/iroha_core/src/block.rs +++ b/crates/iroha_core/src/block.rs @@ -251,7 +251,7 @@ mod new { use std::collections::BTreeMap; use super::*; - use crate::state::StateBlock; + use crate::{smartcontracts::wasm::cache::WasmCache, state::StateBlock}; /// First stage in the life-cycle of a [`Block`]. /// @@ -266,6 +266,7 @@ mod new { impl NewBlock { /// Categorize transactions of this block to produce a [`ValidBlock`] pub fn categorize(self, state_block: &mut StateBlock<'_>) -> WithEvents { + let mut wasm_cache = WasmCache::new(); let errors = self .transactions .iter() @@ -273,7 +274,7 @@ mod new { .cloned() .enumerate() .fold(BTreeMap::new(), |mut acc, (idx, tx)| { - if let Err((rejected_tx, error)) = state_block.validate(tx) { + if let Err((rejected_tx, error)) = state_block.validate(tx, &mut wasm_cache) { iroha_logger::debug!( block=%self.header.hash(), tx=%rejected_tx.hash(), @@ -338,7 +339,9 @@ mod valid { use mv::storage::StorageReadOnly; use super::*; - use crate::{state::StateBlock, sumeragi::network_topology::Role}; + use crate::{ + smartcontracts::wasm::cache::WasmCache, state::StateBlock, sumeragi::network_topology::Role, + }; /// Block that was validated and accepted #[derive(Debug, Clone)] @@ -604,6 +607,7 @@ mod valid { (params.sumeragi().max_clock_drift(), params.transaction) }; + let mut wasm_cache = WasmCache::new(); let errors = block .transactions() // FIXME: Redundant clone @@ -626,7 +630,9 @@ mod valid { ) }?; - if let Err((rejected_tx, error)) = state_block.validate(accepted_tx) { + if let Err((rejected_tx, error)) = + state_block.validate(accepted_tx, &mut wasm_cache) + { iroha_logger::debug!( tx=%rejected_tx.hash(), block=%block.hash(), diff --git a/crates/iroha_core/src/executor.rs b/crates/iroha_core/src/executor.rs index 6946990100..3ed33e6a4b 100644 --- a/crates/iroha_core/src/executor.rs +++ b/crates/iroha_core/src/executor.rs @@ -18,7 +18,7 @@ use serde::{ }; use crate::{ - smartcontracts::{wasm, Execute as _}, + smartcontracts::{wasm, wasm::cache::WasmCache, Execute as _}, state::{deserialize::WasmSeed, StateReadOnly, StateTransaction}, WorldReadOnly as _, }; @@ -122,6 +122,7 @@ impl Executor { state_transaction: &mut StateTransaction<'_, '_>, authority: &AccountId, transaction: SignedTransaction, + wasm_cache: &mut WasmCache<'_, '_, '_>, ) -> Result<(), ValidationFail> { trace!("Running transaction execution"); @@ -140,18 +141,16 @@ impl Executor { Ok(()) } Self::UserProvided(loaded_executor) => { - let runtime = - wasm::RuntimeBuilder::::new() - .with_engine(state_transaction.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_config(state_transaction.world.parameters().executor) - .build()?; - - runtime.execute_executor_execute_transaction( + let wasm_cache = WasmCache::change_lifetime(wasm_cache); + let mut runtime = wasm_cache + .take_or_create_cached_runtime(state_transaction, &loaded_executor.module)?; + let result = runtime.execute_executor_execute_transaction( state_transaction, authority, - &loaded_executor.module, transaction, - )? + )?; + wasm_cache.put_cached_runtime(runtime); + result } } } diff --git a/crates/iroha_core/src/smartcontracts/wasm.rs b/crates/iroha_core/src/smartcontracts/wasm.rs index b04278363f..f4a6feddeb 100644 --- a/crates/iroha_core/src/smartcontracts/wasm.rs +++ b/crates/iroha_core/src/smartcontracts/wasm.rs @@ -40,6 +40,9 @@ use crate::{ state::{StateReadOnly, StateTransaction, WorldReadOnly}, }; +/// Cache for WASM Runtime +pub mod cache; + /// Name of the exported memory const WASM_MEMORY: &str = "memory"; const WASM_MODULE: &str = "iroha"; @@ -527,7 +530,9 @@ pub mod state { use super::*; /// State for executing `execute_transaction()` entrypoint - pub type ExecuteTransaction<'wrld, 'block, 'state> = CommonState< + pub type ExecuteTransaction<'wrld, 'block, 'state> = + Option>; + type ExecuteTransactionInner<'wrld, 'block, 'state> = CommonState< chain_state::WithMut<'wrld, 'block, 'state>, specific::executor::ExecuteTransaction, >; @@ -561,7 +566,7 @@ pub mod state { } impl_blank_validate_operations!( - ExecuteTransaction<'_, '_, '_>, + ExecuteTransactionInner<'_, '_, '_>, ExecuteInstruction<'_, '_, '_>, Migrate<'_, '_, '_>, ); @@ -585,6 +590,14 @@ pub struct Runtime { config: Config, } +/// `Runtime` with instantiated module. +/// Needed to reuse `instance` for multiple transactions during validation. +pub struct RuntimeFull { + runtime: Runtime, + store: Store, + instance: Instance, +} + impl Runtime { fn get_memory(caller: &mut impl GetExport) -> Result { caller @@ -754,6 +767,17 @@ impl Runtime> { } } +impl Runtime>> { + #[codec::wrap] + fn log( + (log_level, msg): (u8, String), + state: &Option>, + ) -> Result<(), WasmtimeError> { + let state = state.as_ref().unwrap(); + Runtime::>::__log_inner((log_level, msg), state) + } +} + impl Runtime>> where payloads::Validate: Encode, @@ -764,49 +788,105 @@ where state: state::CommonState>, validate_fn_name: &'static str, ) -> Result { + let context = create_validate_context(&state); let mut store = self.create_store(state); let instance = self.instantiate_module(module, &mut store)?; - let validate_fn = Self::get_typed_func(&instance, &mut store, validate_fn_name)?; - let context = Self::get_validate_context(&instance, &mut store); + let validation_res = + execute_executor_validate_part1(&mut store, &instance, context, validate_fn_name)?; - // NOTE: This function takes ownership of the pointer - let offset = validate_fn - .call(&mut store, context) - .map_err(ExportFnCallError::from)?; + let state = store.into_data(); + execute_executor_validate_part2(state); - let memory = - Self::get_memory(&mut (&instance, &mut store)).expect("Checked at instantiation step"); - let dealloc_fn = - Self::get_typed_func(&instance, &mut store, import::SMART_CONTRACT_DEALLOC) - .expect("Checked at instantiation step"); - let validation_res = - codec::decode_with_length_prefix_from_memory(&memory, &dealloc_fn, &mut store, offset) - .map_err(Error::Decode)?; + Ok(validation_res) + } +} - let mut state = store.into_data(); - let executed_queries = state.take_executed_queries(); - forget_all_executed_queries( - state.state.state().borrow().query_handle(), - executed_queries, - ); +impl RuntimeFull>>> +where + payloads::Validate: Encode, +{ + fn execute_executor_execute_internal( + &mut self, + state: CommonState>, + validate_fn_name: &'static str, + ) -> Result { + let context = create_validate_context(&state); + self.set_store_state(state); + + let validation_res = execute_executor_validate_part1( + &mut self.store, + &self.instance, + context, + validate_fn_name, + )?; + + let state = + self.store.data_mut().take().expect( + "Store data was set at the beginning of execute_executor_validate_internal", + ); + execute_executor_validate_part2(state); Ok(validation_res) } - fn get_validate_context( - instance: &Instance, - store: &mut Store>>, - ) -> WasmUsize { - let state = store.data(); - let context = payloads::Validate { - context: payloads::ExecutorContext { - authority: state.authority.clone(), - curr_block: state.specific_state.curr_block, - }, - target: state.specific_state.to_validate.clone(), - }; - Self::encode_payload(instance, store, context) + fn set_store_state(&mut self, state: CommonState>) { + *self.store.data_mut() = Some(state); + + self.store + .limiter(|s| &mut s.as_mut().unwrap().store_limits); + + // Need to set fuel again for each transaction since store is shared across transactions + self.store + .set_fuel(self.runtime.config.fuel.get()) + .expect("Fuel consumption is enabled"); + } +} + +fn execute_executor_validate_part1( + store: &mut Store, + instance: &Instance, + context: payloads::Validate, + validate_fn_name: &'static str, +) -> Result +where + payloads::Validate: Encode, +{ + let validate_fn = Runtime::get_typed_func(instance, &mut *store, validate_fn_name)?; + let context = Runtime::encode_payload(instance, &mut *store, context); + + // NOTE: This function takes ownership of the pointer + let offset = validate_fn + .call(&mut *store, context) + .map_err(ExportFnCallError::from)?; + + let memory = Runtime::::get_memory(&mut (instance, &mut *store)) + .expect("Checked at instantiation step"); + let dealloc_fn = Runtime::get_typed_func(instance, &mut *store, import::SMART_CONTRACT_DEALLOC) + .expect("Checked at instantiation step"); + codec::decode_with_length_prefix_from_memory(&memory, &dealloc_fn, &mut *store, offset) + .map_err(Error::Decode) +} + +fn execute_executor_validate_part2( + mut state: CommonState, +) { + let executed_queries = state.take_executed_queries(); + forget_all_executed_queries( + state.state.state().borrow().query_handle(), + executed_queries, + ); +} + +fn create_validate_context( + state: &CommonState>, +) -> payloads::Validate { + payloads::Validate { + context: payloads::ExecutorContext { + authority: state.authority.clone(), + curr_block: state.specific_state.curr_block, + }, + target: state.specific_state.to_validate.clone(), } } @@ -1110,6 +1190,37 @@ where } } +impl<'wrld, 'block, 'state, R, S> + import::traits::ExecuteOperations, S>>> for R +where + R: ExecuteOperationsAsExecutorMut, S>>>, + CommonState, S>: state::ValidateQueryOperation, +{ + #[codec::wrap] + fn execute_query( + query_request: QueryRequest, + state: &mut Option, S>>, + ) -> Result { + debug!(?query_request, "Executing as executor"); + + let state = state.as_mut().unwrap(); + Runtime::default_execute_query(query_request, state) + } + + #[codec::wrap] + fn execute_instruction( + instruction: InstructionBox, + state: &mut Option, S>>, + ) -> Result<(), ValidationFail> { + debug!(%instruction, "Executing as executor"); + + let state = state.as_mut().unwrap(); + instruction + .execute(&state.authority.clone(), state.state.0) + .map_err(Into::into) + } +} + /// Marker trait to auto-implement [`import_traits::SetExecutorDataModel`] for a concrete [`Runtime`]. /// /// Useful because *Executor* exposes more entrypoints than just `migrate()` which is the @@ -1132,7 +1243,9 @@ where } } -impl<'wrld, 'block, 'state> Runtime> { +impl<'wrld, 'block, 'state> + RuntimeFull> +{ /// Execute `execute_transaction()` entrypoint of the given module of runtime executor /// /// # Errors @@ -1142,24 +1255,23 @@ impl<'wrld, 'block, 'state> Runtime, authority: &AccountId, - module: &wasmtime::Module, transaction: SignedTransaction, ) -> Result { let span = wasm_log_span!("Running `execute_transaction()`"); let curr_block = state_transaction.curr_block; - let state = state::executor::ExecuteTransaction::new( + let state = CommonState::new( authority.clone(), - self.config, + self.runtime.config, span, state::chain_state::WithMut(state_transaction), state::specific::executor::ExecuteTransaction::new(transaction, curr_block), ); - self.execute_executor_execute_internal(module, state, import::EXECUTOR_EXECUTE_TRANSACTION) + self.execute_executor_execute_internal(state, import::EXECUTOR_EXECUTE_TRANSACTION) } } @@ -1410,7 +1522,7 @@ macro_rules! create_imports { $linker.func_wrap( WASM_MODULE, export::LOG, - |caller: ::wasmtime::Caller<$ty>, offset, len| Runtime::log(caller, offset, len), + |caller: ::wasmtime::Caller<$ty>, offset, len| Runtime::<$ty>::log(caller, offset, len), ) .and_then(|l| { l.func_wrap( diff --git a/crates/iroha_core/src/smartcontracts/wasm/cache.rs b/crates/iroha_core/src/smartcontracts/wasm/cache.rs new file mode 100644 index 0000000000..c3cf72652a --- /dev/null +++ b/crates/iroha_core/src/smartcontracts/wasm/cache.rs @@ -0,0 +1,85 @@ +use iroha_data_model::parameter::SmartContractParameters; +use wasmtime::{Engine, Module, Store}; + +use crate::{ + prelude::WorldReadOnly, + smartcontracts::{ + wasm, + wasm::{state::executor::ExecuteTransaction, RuntimeFull}, + }, + state::StateTransaction, +}; + +/// Executor related things (linker initialization, module instantiation, memory free) +/// takes significant amount of time in case of single peer transactions handling. +/// (https://github.com/hyperledger/iroha/issues/3716#issuecomment-2348417005). +/// So this cache is used to share `Store` and `Instance` for different transaction validation. +#[derive(Default)] +pub struct WasmCache<'world, 'block, 'state> { + cache: Option>>, +} + +impl<'world, 'block, 'state> WasmCache<'world, 'block, 'state> { + /// Constructor + pub fn new() -> Self { + Self { cache: None } + } + + /// Hack to pass borrow checker. Should be used only when there is no data in `Store`. + #[allow(unsafe_code)] + pub fn change_lifetime<'l>(wasm_cache: &'l mut WasmCache) -> &'l mut Self { + if let Some(cache) = wasm_cache.cache.as_ref() { + assert!(cache.store.data().is_none()); + } + // SAFETY: since we have ensured that `cache.store.data()` is `None`, + // the lifetime parameters we are transmuting are not used by any references. + unsafe { std::mem::transmute::<&mut WasmCache, &mut WasmCache>(wasm_cache) } + } + + /// Returns cached saved runtime, or creates a new one. + /// + /// # Errors + /// If failed to create runtime + pub fn take_or_create_cached_runtime( + &mut self, + state_transaction: &StateTransaction<'_, '_>, + module: &Module, + ) -> Result>, wasm::Error> { + let parameters = state_transaction.world.parameters().executor; + if let Some(cached_runtime) = self.cache.take() { + if cached_runtime.runtime.config == parameters { + return Ok(cached_runtime); + } + } + + Self::create_runtime(state_transaction.engine.clone(), module, parameters) + } + + fn create_runtime( + engine: Engine, + module: &'_ Module, + parameters: SmartContractParameters, + ) -> Result>, wasm::Error> { + let runtime = wasm::RuntimeBuilder::::new() + .with_engine(engine) + .with_config(parameters) + .build()?; + let mut store = Store::new(&runtime.engine, None); + let instance = runtime.instantiate_module(module, &mut store)?; + let runtime_full = RuntimeFull { + runtime, + store, + instance, + }; + Ok(runtime_full) + } + + /// Saves runtime to be reused later. + pub fn put_cached_runtime( + &mut self, + runtime: RuntimeFull>, + ) { + assert!(runtime.store.data().is_none()); + self.cache = Some(runtime); + } +} diff --git a/crates/iroha_core/src/tx.rs b/crates/iroha_core/src/tx.rs index 2d6e672f41..e78b970b1d 100644 --- a/crates/iroha_core/src/tx.rs +++ b/crates/iroha_core/src/tx.rs @@ -23,7 +23,7 @@ use iroha_macro::FromVariant; use mv::storage::StorageReadOnly; use crate::{ - smartcontracts::wasm, + smartcontracts::{wasm, wasm::cache::WasmCache}, state::{StateBlock, StateTransaction}, }; @@ -204,9 +204,12 @@ impl StateBlock<'_> { pub fn validate( &mut self, tx: AcceptedTransaction, + wasm_cache: &mut WasmCache<'_, '_, '_>, ) -> Result { let mut state_transaction = self.transaction(); - if let Err(rejection_reason) = Self::validate_internal(tx.clone(), &mut state_transaction) { + if let Err(rejection_reason) = + Self::validate_internal(tx.clone(), &mut state_transaction, wasm_cache) + { return Err((tx.0, rejection_reason)); } state_transaction.apply(); @@ -217,6 +220,7 @@ impl StateBlock<'_> { fn validate_internal( tx: AcceptedTransaction, state_transaction: &mut StateTransaction<'_, '_>, + wasm_cache: &mut WasmCache<'_, '_, '_>, ) -> Result<(), TransactionRejectionReason> { let authority = tx.as_ref().authority(); @@ -227,7 +231,7 @@ impl StateBlock<'_> { } debug!(tx=%tx.as_ref().hash(), "Validating transaction"); - Self::validate_with_runtime_executor(tx.clone(), state_transaction)?; + Self::validate_with_runtime_executor(tx.clone(), state_transaction, wasm_cache)?; if let (authority, Executable::Wasm(bytes)) = tx.into() { Self::validate_wasm(authority, state_transaction, bytes)? @@ -270,6 +274,7 @@ impl StateBlock<'_> { fn validate_with_runtime_executor( tx: AcceptedTransaction, state_transaction: &mut StateTransaction<'_, '_>, + wasm_cache: &mut WasmCache<'_, '_, '_>, ) -> Result<(), TransactionRejectionReason> { let tx: SignedTransaction = tx.into(); let authority = tx.authority().clone(); @@ -278,7 +283,7 @@ impl StateBlock<'_> { .world .executor .clone() // Cloning executor is a cheap operation - .execute_transaction(state_transaction, &authority, tx) + .execute_transaction(state_transaction, &authority, tx, wasm_cache) .map_err(|error| { if let ValidationFail::InternalError(msg) = &error { error!(