From 3be87fd94ca31456318ebd8aecda05acc0bcb29e 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 | 5 +- crates/iroha_core/src/block.rs | 12 +- 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, 268 insertions(+), 62 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 56999a2c20..7b06128602 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::{ @@ -132,10 +132,11 @@ 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 _ = criterion.bench_function("validate", move |b| { b.iter(|| { let mut state_block = state.block(); - match state_block.validate(transaction.clone()) { + match state_block.validate(transaction.clone(), &mut wasm_cache) { Ok(_) => success_count += 1, Err(_) => failure_count += 1, } diff --git a/crates/iroha_core/src/block.rs b/crates/iroha_core/src/block.rs index 70fe9e1f99..58e42be1ec 100644 --- a/crates/iroha_core/src/block.rs +++ b/crates/iroha_core/src/block.rs @@ -122,7 +122,7 @@ mod pending { use nonzero_ext::nonzero; use super::*; - use crate::state::StateBlock; + use crate::{smartcontracts::wasm::cache::WasmCache, state::StateBlock}; /// First stage in the life-cycle of a [`Block`]. /// In the beginning the block is assumed to be verified and to contain only accepted transactions. @@ -231,9 +231,10 @@ mod pending { transactions: Vec, state_block: &mut StateBlock<'_>, ) -> Vec { + let mut wasm_cache = WasmCache::new(); transactions .into_iter() - .map(|tx| match state_block.validate(tx) { + .map(|tx| match state_block.validate(tx, &mut wasm_cache) { Ok(tx) => CommittedTransaction { value: tx, error: None, @@ -299,7 +300,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)] @@ -612,6 +615,7 @@ mod valid { (params.sumeragi().max_clock_drift(), params.transaction) }; + let mut wasm_cache = WasmCache::new(); for CommittedTransaction { value, error } in block.transactions_mut() { let tx = if is_genesis { AcceptedTransaction::accept_genesis( @@ -629,7 +633,7 @@ mod valid { ) }?; - *error = match state_block.validate(tx) { + *error = match state_block.validate(tx, &mut wasm_cache) { Ok(_) => None, Err((_tx, error)) => Some(Box::new(error)), }; 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 e046928829..704f58ee99 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"; @@ -526,7 +529,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, >; @@ -560,7 +565,7 @@ pub mod state { } impl_blank_validate_operations!( - ExecuteTransaction<'_, '_, '_>, + ExecuteTransactionInner<'_, '_, '_>, ExecuteInstruction<'_, '_, '_>, Migrate<'_, '_, '_>, ); @@ -584,6 +589,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 payload = create_validate_payload(&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 payload = Self::get_validate_payload(&instance, &mut store); + let validation_res = + execute_executor_validate_part1(&mut store, &instance, payload, validate_fn_name)?; - // NOTE: This function takes ownership of the pointer - let offset = validate_fn - .call(&mut store, payload) - .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 payload = create_validate_payload(&state); + self.set_store_state(state); + + let validation_res = execute_executor_validate_part1( + &mut self.store, + &self.instance, + payload, + 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_payload( - instance: &Instance, - store: &mut Store>>, - ) -> WasmUsize { - let state = store.data(); - let payload = payloads::Validate { - context: payloads::ExecutorContext { - authority: state.authority.clone(), - block_height: state.state.state().height() as u64, - }, - target: state.specific_state.to_validate.clone(), - }; - Runtime::encode_payload(instance, store, payload) + 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, + payload: 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 payload = Runtime::encode_payload(&instance, &mut *store, payload); + + // NOTE: This function takes ownership of the pointer + let offset = validate_fn + .call(&mut *store, payload) + .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_payload( + state: &CommonState>, +) -> payloads::Validate { + payloads::Validate { + context: payloads::ExecutorContext { + authority: state.authority.clone(), + block_height: state.state.state().height() as u64, + }, + target: state.specific_state.to_validate.clone(), } } @@ -1098,6 +1178,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 @@ -1120,7 +1231,9 @@ where } } -impl<'wrld, 'block, 'state> Runtime> { +impl<'wrld, 'block, 'state> + RuntimeFull> +{ /// Execute `execute_transaction()` entrypoint of the given module of runtime executor /// /// # Errors @@ -1130,23 +1243,22 @@ impl<'wrld, 'block, 'state> Runtime, authority: &AccountId, - module: &wasmtime::Module, transaction: SignedTransaction, ) -> Result { let span = wasm_log_span!("Running `execute_transaction()`"); - 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), ); - self.execute_executor_execute_internal(module, state, import::EXECUTOR_EXECUTE_TRANSACTION) + self.execute_executor_execute_internal(state, import::EXECUTOR_EXECUTE_TRANSACTION) } } @@ -1390,7 +1502,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 fe51896b57..2bbada6958 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!(