wasm-executor: Support growing the memory (#12520)

* As always, start with something :P

* Add support for max_heap_pages

* Add support for wasmtime

* Make it compile

* Fix compilation

* Copy wrongly merged code

* Fix compilation

* Some fixes

* Fix

* Get stuff working

* More work

* More fixes

* ...

* More

* FIXEs

* Switch wasmi to use `RuntimeBlob` like wasmtime

* Removed unused stuff

* Cleanup

* More cleanups

* Introduce `CallContext`

* Fixes

* More fixes

* Add builder for creating the `WasmExecutor`

* Adds some docs

* FMT

* First round of feedback.

* Review feedback round 2

* More fixes

* Fix try-runtime

* Update client/executor/wasmtime/src/instance_wrapper.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/executor/common/src/wasm_runtime.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/executor/common/src/runtime_blob/runtime_blob.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/executor/common/src/wasm_runtime.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/allocator/src/freeing_bump.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/allocator/src/freeing_bump.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Feedback round 3

* FMT

* Review comments

---------

Co-authored-by: Koute <koute@users.noreply.github.com>
This commit is contained in:
Bastian Köcher
2023-02-24 12:43:01 +01:00
committed by GitHub
parent c848d40775
commit 941288c6d0
37 changed files with 1092 additions and 667 deletions
+211 -39
View File
@@ -32,14 +32,15 @@ use std::{
use codec::Encode;
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{AllocationStats, WasmInstance, WasmModule},
wasm_runtime::{AllocationStats, HeapAllocStrategy, WasmInstance, WasmModule},
};
use sp_core::traits::{CodeExecutor, Externalities, RuntimeCode};
use sp_core::traits::{CallContext, CodeExecutor, Externalities, RuntimeCode};
use sp_version::{GetNativeVersion, NativeVersion, RuntimeVersion};
use sp_wasm_interface::{ExtendedHostFunctions, HostFunctions};
/// Default num of pages for the heap
const DEFAULT_HEAP_PAGES: u64 = 2048;
/// Default heap allocation strategy.
const DEFAULT_HEAP_ALLOC_STRATEGY: HeapAllocStrategy =
HeapAllocStrategy::Static { extra_pages: 2048 };
/// Set up the externalities and safe calling environment to execute runtime calls.
///
@@ -79,13 +80,136 @@ pub trait NativeExecutionDispatch: Send + Sync {
fn native_version() -> NativeVersion;
}
fn unwrap_heap_pages(pages: Option<HeapAllocStrategy>) -> HeapAllocStrategy {
pages.unwrap_or_else(|| DEFAULT_HEAP_ALLOC_STRATEGY)
}
/// Builder for creating a [`WasmExecutor`] instance.
pub struct WasmExecutorBuilder<H> {
_phantom: PhantomData<H>,
method: WasmExecutionMethod,
onchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
offchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
allow_missing_host_functions: bool,
runtime_cache_size: u8,
}
impl<H> WasmExecutorBuilder<H> {
/// Create a new instance of `Self`
///
/// - `method`: The wasm execution method that should be used by the executor.
pub fn new(method: WasmExecutionMethod) -> Self {
Self {
_phantom: PhantomData,
method,
onchain_heap_alloc_strategy: None,
offchain_heap_alloc_strategy: None,
max_runtime_instances: 2,
runtime_cache_size: 4,
allow_missing_host_functions: false,
cache_path: None,
}
}
/// Create the wasm executor with the given number of `heap_alloc_strategy` for onchain runtime
/// calls.
pub fn with_onchain_heap_alloc_strategy(
mut self,
heap_alloc_strategy: HeapAllocStrategy,
) -> Self {
self.onchain_heap_alloc_strategy = Some(heap_alloc_strategy);
self
}
/// Create the wasm executor with the given number of `heap_alloc_strategy` for offchain runtime
/// calls.
pub fn with_offchain_heap_alloc_strategy(
mut self,
heap_alloc_strategy: HeapAllocStrategy,
) -> Self {
self.offchain_heap_alloc_strategy = Some(heap_alloc_strategy);
self
}
/// Create the wasm executor with the given maximum number of `instances`.
///
/// The number of `instances` defines how many different instances of a runtime the cache is
/// storing.
///
/// By default the maximum number of `instances` is `2`.
pub fn with_max_runtime_instances(mut self, instances: usize) -> Self {
self.max_runtime_instances = instances;
self
}
/// Create the wasm executor with the given `cache_path`.
///
/// The `cache_path` is A path to a directory where the executor can place its files for
/// purposes of caching. This may be important in cases when there are many different modules
/// with the compiled execution method is used.
///
/// By default there is no `cache_path` given.
pub fn with_cache_path(mut self, cache_path: impl Into<PathBuf>) -> Self {
self.cache_path = Some(cache_path.into());
self
}
/// Create the wasm executor and allow/forbid missing host functions.
///
/// If missing host functions are forbidden, the instantiation of a wasm blob will fail
/// for imported host functions that the executor is not aware of. If they are allowed,
/// a stub is generated that will return an error when being called while executing the wasm.
///
/// By default missing host functions are forbidden.
pub fn with_allow_missing_host_functions(mut self, allow: bool) -> Self {
self.allow_missing_host_functions = allow;
self
}
/// Create the wasm executor with the given `runtime_cache_size`.
///
/// Defines the number of different runtimes/instantiated wasm blobs the cache stores.
/// Runtimes/wasm blobs are differentiated based on the hash and the number of heap pages.
///
/// By default this value is set to `4`.
pub fn with_runtime_cache_size(mut self, runtime_cache_size: u8) -> Self {
self.runtime_cache_size = runtime_cache_size;
self
}
/// Build the configured [`WasmExecutor`].
pub fn build(self) -> WasmExecutor<H> {
WasmExecutor {
method: self.method,
default_offchain_heap_alloc_strategy: unwrap_heap_pages(
self.offchain_heap_alloc_strategy,
),
default_onchain_heap_alloc_strategy: unwrap_heap_pages(
self.onchain_heap_alloc_strategy,
),
cache: Arc::new(RuntimeCache::new(
self.max_runtime_instances,
self.cache_path.clone(),
self.runtime_cache_size,
)),
cache_path: self.cache_path,
allow_missing_host_functions: self.allow_missing_host_functions,
phantom: PhantomData,
}
}
}
/// An abstraction over Wasm code executor. Supports selecting execution backend and
/// manages runtime cache.
pub struct WasmExecutor<H> {
/// Method used to execute fallback Wasm code.
method: WasmExecutionMethod,
/// The number of 64KB pages to allocate for Wasm execution.
default_heap_pages: u64,
/// The heap allocation strategy for onchain Wasm calls.
default_onchain_heap_alloc_strategy: HeapAllocStrategy,
/// The heap allocation strategy for offchain Wasm calls.
default_offchain_heap_alloc_strategy: HeapAllocStrategy,
/// WASM runtime cache.
cache: Arc<RuntimeCache>,
/// The path to a directory which the executor can leverage for a file cache, e.g. put there
@@ -100,7 +224,8 @@ impl<H> Clone for WasmExecutor<H> {
fn clone(&self) -> Self {
Self {
method: self.method,
default_heap_pages: self.default_heap_pages,
default_onchain_heap_alloc_strategy: self.default_onchain_heap_alloc_strategy,
default_offchain_heap_alloc_strategy: self.default_offchain_heap_alloc_strategy,
cache: self.cache.clone(),
cache_path: self.cache_path.clone(),
allow_missing_host_functions: self.allow_missing_host_functions,
@@ -119,8 +244,10 @@ where
///
/// `method` - Method used to execute Wasm code.
///
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution.
/// Defaults to `DEFAULT_HEAP_PAGES` if `None` is provided.
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution. Internally this
/// will be mapped as [`HeapAllocStrategy::Static`] where `default_heap_pages` represent the
/// static number of heap pages to allocate. Defaults to `DEFAULT_HEAP_ALLOC_STRATEGY` if `None`
/// is provided.
///
/// `max_runtime_instances` - The number of runtime instances to keep in memory ready for reuse.
///
@@ -138,7 +265,12 @@ where
) -> Self {
WasmExecutor {
method,
default_heap_pages: default_heap_pages.unwrap_or(DEFAULT_HEAP_PAGES),
default_onchain_heap_alloc_strategy: unwrap_heap_pages(
default_heap_pages.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ }),
),
default_offchain_heap_alloc_strategy: unwrap_heap_pages(
default_heap_pages.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ }),
),
cache: Arc::new(RuntimeCache::new(
max_runtime_instances,
cache_path.clone(),
@@ -150,6 +282,11 @@ where
}
}
/// Instantiate a builder for creating an instance of `Self`.
pub fn builder(method: WasmExecutionMethod) -> WasmExecutorBuilder<H> {
WasmExecutorBuilder::new(method)
}
/// Ignore missing function imports if set true.
pub fn allow_missing_host_functions(&mut self, allow_missing_host_functions: bool) {
self.allow_missing_host_functions = allow_missing_host_functions
@@ -172,6 +309,7 @@ where
&self,
runtime_code: &RuntimeCode,
ext: &mut dyn Externalities,
heap_alloc_strategy: HeapAllocStrategy,
f: F,
) -> Result<R>
where
@@ -186,7 +324,7 @@ where
runtime_code,
ext,
self.method,
self.default_heap_pages,
heap_alloc_strategy,
self.allow_missing_host_functions,
|module, instance, version, ext| {
let module = AssertUnwindSafe(module);
@@ -259,7 +397,7 @@ where
) -> std::result::Result<Vec<u8>, Error> {
let module = crate::wasm_runtime::create_wasm_runtime_with_code::<H>(
self.method,
self.default_heap_pages,
self.default_onchain_heap_alloc_strategy,
runtime_blob,
allow_missing_host_functions,
self.cache_path.as_deref(),
@@ -334,6 +472,7 @@ where
method: &str,
data: &[u8],
_use_native: bool,
context: CallContext,
) -> (Result<Vec<u8>>, bool) {
tracing::trace!(
target: "executor",
@@ -341,10 +480,25 @@ where
"Executing function",
);
let result =
self.with_instance(runtime_code, ext, |_, mut instance, _onchain_version, mut ext| {
let on_chain_heap_alloc_strategy = runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.default_onchain_heap_alloc_strategy);
let heap_alloc_strategy = match context {
CallContext::Offchain => self.default_offchain_heap_alloc_strategy,
CallContext::Onchain => on_chain_heap_alloc_strategy,
};
let result = self.with_instance(
runtime_code,
ext,
heap_alloc_strategy,
|_, mut instance, _onchain_version, mut ext| {
with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
});
},
);
(result, false)
}
}
@@ -358,20 +512,25 @@ where
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
) -> Result<RuntimeVersion> {
self.with_instance(runtime_code, ext, |_module, _instance, version, _ext| {
Ok(version.cloned().ok_or_else(|| Error::ApiError("Unknown version".into())))
})
let on_chain_heap_pages = runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.default_onchain_heap_alloc_strategy);
self.with_instance(
runtime_code,
ext,
on_chain_heap_pages,
|_module, _instance, version, _ext| {
Ok(version.cloned().ok_or_else(|| Error::ApiError("Unknown version".into())))
},
)
}
}
/// A generic `CodeExecutor` implementation that uses a delegate to determine wasm code equivalence
/// and dispatch to native code when possible, falling back on `WasmExecutor` when not.
pub struct NativeElseWasmExecutor<D>
where
D: NativeExecutionDispatch,
{
/// Dummy field to avoid the compiler complaining about us not using `D`.
_dummy: PhantomData<D>,
pub struct NativeElseWasmExecutor<D: NativeExecutionDispatch> {
/// Native runtime version info.
native_version: NativeVersion,
/// Fallback wasm executor.
@@ -386,8 +545,10 @@ impl<D: NativeExecutionDispatch> NativeElseWasmExecutor<D> {
///
/// `fallback_method` - Method used to execute fallback Wasm code.
///
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution.
/// Defaults to `DEFAULT_HEAP_PAGES` if `None` is provided.
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution. Internally this
/// will be mapped as [`HeapAllocStrategy::Static`] where `default_heap_pages` represent the
/// static number of heap pages to allocate. Defaults to `DEFAULT_HEAP_ALLOC_STRATEGY` if `None`
/// is provided.
///
/// `max_runtime_instances` - The number of runtime instances to keep in memory ready for reuse.
///
@@ -406,11 +567,16 @@ impl<D: NativeExecutionDispatch> NativeElseWasmExecutor<D> {
runtime_cache_size,
);
NativeElseWasmExecutor {
_dummy: Default::default(),
native_version: D::native_version(),
wasm,
}
NativeElseWasmExecutor { native_version: D::native_version(), wasm }
}
/// Create a new instance using the given [`WasmExecutor`].
pub fn new_with_wasm_executor(
executor: WasmExecutor<
ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>,
>,
) -> Self {
Self { native_version: D::native_version(), wasm: executor }
}
/// Ignore missing function imports if set true.
@@ -425,9 +591,7 @@ impl<D: NativeExecutionDispatch> RuntimeVersionOf for NativeElseWasmExecutor<D>
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
) -> Result<RuntimeVersion> {
self.wasm.with_instance(runtime_code, ext, |_module, _instance, version, _ext| {
Ok(version.cloned().ok_or_else(|| Error::ApiError("Unknown version".into())))
})
self.wasm.runtime_version(ext, runtime_code)
}
}
@@ -447,6 +611,7 @@ impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecut
method: &str,
data: &[u8],
use_native: bool,
context: CallContext,
) -> (Result<Vec<u8>>, bool) {
tracing::trace!(
target: "executor",
@@ -454,10 +619,21 @@ impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecut
"Executing function",
);
let on_chain_heap_alloc_strategy = runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.wasm.default_onchain_heap_alloc_strategy);
let heap_alloc_strategy = match context {
CallContext::Offchain => self.wasm.default_offchain_heap_alloc_strategy,
CallContext::Onchain => on_chain_heap_alloc_strategy,
};
let mut used_native = false;
let result = self.wasm.with_instance(
runtime_code,
ext,
heap_alloc_strategy,
|_, mut instance, onchain_version, mut ext| {
let onchain_version =
onchain_version.ok_or_else(|| Error::ApiError("Unknown version".into()))?;
@@ -496,11 +672,7 @@ impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecut
impl<D: NativeExecutionDispatch> Clone for NativeElseWasmExecutor<D> {
fn clone(&self) -> Self {
NativeElseWasmExecutor {
_dummy: Default::default(),
native_version: D::native_version(),
wasm: self.wasm.clone(),
}
NativeElseWasmExecutor { native_version: D::native_version(), wasm: self.wasm.clone() }
}
}