From 3264ff24288ca7b868f1a817ea8eba43544a6278 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 | 14 +- crates/iroha_core/src/executor.rs | 19 +- crates/iroha_core/src/smartcontracts/wasm.rs | 169 +++++++++++++++--- .../src/smartcontracts/wasm/cache.rs | 79 ++++++++ crates/iroha_core/src/tx.rs | 13 +- 6 files changed, 249 insertions(+), 50 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 98ff226687..0cd36df0bb 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. @@ -217,9 +217,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, @@ -286,7 +287,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)] @@ -595,6 +598,7 @@ mod valid { (params.sumeragi().max_clock_drift(), params.transaction) }; + let mut wasm_cache = WasmCache::new(); block .transactions() // TODO: Unnecessary clone? @@ -617,13 +621,13 @@ mod valid { }?; if error.is_some() { - match state_block.validate(tx) { + match state_block.validate(tx, &mut wasm_cache) { Err(rejected_transaction) => Ok(rejected_transaction), Ok(_) => Err(TransactionValidationError::RejectedIsValid), }?; } else { state_block - .validate(tx) + .validate(tx, &mut wasm_cache) .map_err(|(_tx, error)| TransactionValidationError::NotValid(error))?; } diff --git a/crates/iroha_core/src/executor.rs b/crates/iroha_core/src/executor.rs index bbdc58f159..aca3ad504d 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 validation"); @@ -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_validate_transaction( + let wasm_cache = WasmCache::change_lifetime(wasm_cache); + let mut runtime = + wasm_cache.create_runtime_cached(state_transaction, &loaded_executor.module)?; + let result = runtime.execute_executor_validate_transaction( state_transaction, authority, - &loaded_executor.module, transaction, - )? + )?; + wasm_cache.save_cached_runtime(runtime); + result } } } diff --git a/crates/iroha_core/src/smartcontracts/wasm.rs b/crates/iroha_core/src/smartcontracts/wasm.rs index b121ae0f96..611ed89609 100644 --- a/crates/iroha_core/src/smartcontracts/wasm.rs +++ b/crates/iroha_core/src/smartcontracts/wasm.rs @@ -21,16 +21,19 @@ use iroha_logger::debug; use iroha_logger::{error_span as wasm_log_span, prelude::tracing::Span}; use iroha_wasm_codec::{self as codec, WasmUsize}; use wasmtime::{ - Caller, Config as WasmtimeConfig, Engine, Linker, Module, Store, StoreLimits, + Caller, Config as WasmtimeConfig, Engine, Instance, Linker, Module, Store, StoreLimits, StoreLimitsBuilder, TypedFunc, }; use crate::{ query::store::LiveQueryStoreHandle, - smartcontracts::{query::ValidQueryRequest, Execute}, + smartcontracts::{query::ValidQueryRequest, wasm::state::CommonState, Execute}, 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"; @@ -538,7 +541,9 @@ pub mod state { use super::*; /// State for executing `validate_transaction()` entrypoint - pub type ValidateTransaction<'wrld, 'block, 'state> = CommonState< + pub type ValidateTransaction<'wrld, 'block, 'state> = + Option>; + type ValidateTransactionInner<'wrld, 'block, 'state> = CommonState< chain_state::WithMut<'wrld, 'block, 'state>, specific::executor::ValidateTransaction, >; @@ -572,7 +577,7 @@ pub mod state { } impl_blank_validate_operations!( - ValidateTransaction<'_, '_, '_>, + ValidateTransactionInner<'_, '_, '_>, ValidateInstruction<'_, '_, '_>, Migrate<'_, '_, '_>, ); @@ -596,6 +601,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 @@ -749,6 +762,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> { fn execute_executor_validate_internal( &self, @@ -759,31 +783,77 @@ impl Runtime> { 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 validation_res = + execute_executor_validate_part1(&mut store, &instance, validate_fn_name)?; - // NOTE: This function takes ownership of the pointer - let offset = validate_fn - .call(&mut store, ()) - .map_err(ExportFnCallError::from)?; + let state = store.into_data(); + execute_executor_validate_part2(state); + + Ok(validation_res) + } +} + +impl RuntimeFull>> { + fn execute_executor_validate_internal( + &mut self, + state: state::CommonState, + validate_fn_name: &'static str, + ) -> Result { + self.set_store_state(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)?; + execute_executor_validate_part1(&mut self.store, &self.instance, validate_fn_name)?; - 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, - ); + 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 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, + validate_fn_name: &'static str, +) -> Result { + let validate_fn = Runtime::get_typed_func(instance, &mut *store, validate_fn_name)?; + + // NOTE: This function takes ownership of the pointer + let offset = validate_fn + .call(&mut *store, ()) + .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: state::CommonState, +) { + let executed_queries = state.take_executed_queries(); + forget_all_executed_queries( + state.state.state().borrow().query_handle(), + executed_queries, + ); } impl Runtime> @@ -1074,6 +1144,46 @@ where } } +impl<'wrld, 'block, 'state, R, S> + import::traits::ExecuteOperations< + Option, S>>, + > for R +where + R: ExecuteOperationsAsExecutorMut< + Option, S>>, + >, + state::CommonState, S>: + state::ValidateQueryOperation, +{ + #[codec::wrap] + fn execute_query( + query_request: QueryRequest, + state: &mut Option< + state::CommonState, 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< + state::CommonState, 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 @@ -1096,7 +1206,9 @@ where } } -impl<'wrld, 'block, 'state> Runtime> { +impl<'wrld, 'block, 'state> + RuntimeFull> +{ /// Execute `validate_transaction()` entrypoint of the given module of runtime executor /// /// # Errors @@ -1106,19 +1218,17 @@ impl<'wrld, 'block, 'state> Runtime, authority: &AccountId, - module: &wasmtime::Module, transaction: SignedTransaction, ) -> Result { let span = wasm_log_span!("Running `validate_transaction()`"); self.execute_executor_validate_internal( - module, - state::executor::ValidateTransaction::new( + CommonState::new( authority.clone(), - self.config, + self.runtime.config, span, state::chain_state::WithMut(state_transaction), state::specific::executor::ValidateTransaction::new(transaction), @@ -1148,6 +1258,7 @@ impl<'wrld, 'block, 'state> fn get_validate_transaction_payload( state: &state::executor::ValidateTransaction<'wrld, 'block, 'state>, ) -> Validate { + let state = state.as_ref().unwrap(); Validate { authority: state.authority.clone(), block_height: state.state.0.height() as u64, @@ -1513,7 +1624,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..f2d12f3a14 --- /dev/null +++ b/crates/iroha_core/src/smartcontracts/wasm/cache.rs @@ -0,0 +1,79 @@ +use iroha_data_model::parameter::SmartContractParameters; +use wasmtime::{Engine, Module, Store}; + +use crate::{ + prelude::WorldReadOnly, + smartcontracts::{ + wasm, + wasm::{state::executor::ValidateTransaction, 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. +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()); + } + unsafe { std::mem::transmute::<&mut WasmCache, &mut WasmCache>(wasm_cache) } + } + + /// Returns cached saved runtime, or creates a new one. + pub fn create_runtime_cached( + &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 save_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 44fe92aed4..ee8bd61cd0 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 - .validate_transaction(state_transaction, &authority, tx) + .validate_transaction(state_transaction, &authority, tx, wasm_cache) .map_err(|error| { if let ValidationFail::InternalError(msg) = &error { error!(