mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 22:11:06 +00:00
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:
@@ -13,9 +13,9 @@ readme = "README.md"
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.17"
|
||||
cfg-if = "1.0"
|
||||
libc = "0.2.121"
|
||||
log = "0.4.17"
|
||||
|
||||
# When bumping wasmtime do not forget to also bump rustix
|
||||
# to exactly the same version as used by wasmtime!
|
||||
|
||||
@@ -24,20 +24,25 @@ use wasmtime::Caller;
|
||||
use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
|
||||
use sp_wasm_interface::{Pointer, WordSize};
|
||||
|
||||
use crate::{runtime::StoreData, util};
|
||||
use crate::{instance_wrapper::MemoryWrapper, runtime::StoreData, util};
|
||||
|
||||
/// The state required to construct a HostContext context. The context only lasts for one host
|
||||
/// call, whereas the state is maintained for the duration of a Wasm runtime call, which may make
|
||||
/// many different host calls that must share state.
|
||||
pub struct HostState {
|
||||
allocator: FreeingBumpHeapAllocator,
|
||||
/// The allocator instance to keep track of allocated memory.
|
||||
///
|
||||
/// This is stored as an `Option` as we need to temporarly set this to `None` when we are
|
||||
/// allocating/deallocating memory. The problem being that we can only mutable access `caller`
|
||||
/// once.
|
||||
allocator: Option<FreeingBumpHeapAllocator>,
|
||||
panic_message: Option<String>,
|
||||
}
|
||||
|
||||
impl HostState {
|
||||
/// Constructs a new `HostState`.
|
||||
pub fn new(allocator: FreeingBumpHeapAllocator) -> Self {
|
||||
HostState { allocator, panic_message: None }
|
||||
HostState { allocator: Some(allocator), panic_message: None }
|
||||
}
|
||||
|
||||
/// Takes the error message out of the host state, leaving a `None` in its place.
|
||||
@@ -46,7 +51,9 @@ impl HostState {
|
||||
}
|
||||
|
||||
pub(crate) fn allocation_stats(&self) -> AllocationStats {
|
||||
self.allocator.stats()
|
||||
self.allocator.as_ref()
|
||||
.expect("Allocator is always set and only unavailable when doing an allocation/deallocation; qed")
|
||||
.stats()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,22 +88,38 @@ impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> {
|
||||
|
||||
fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result<Pointer<u8>> {
|
||||
let memory = self.caller.data().memory();
|
||||
let (memory, data) = memory.data_and_store_mut(&mut self.caller);
|
||||
data.host_state_mut()
|
||||
.expect("host state is not empty when calling a function in wasm; qed")
|
||||
let mut allocator = self
|
||||
.host_state_mut()
|
||||
.allocator
|
||||
.allocate(memory, size)
|
||||
.map_err(|e| e.to_string())
|
||||
.take()
|
||||
.expect("allocator is not empty when calling a function in wasm; qed");
|
||||
|
||||
// We can not return on error early, as we need to store back allocator.
|
||||
let res = allocator
|
||||
.allocate(&mut MemoryWrapper(&memory, &mut self.caller), size)
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
self.host_state_mut().allocator = Some(allocator);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn deallocate_memory(&mut self, ptr: Pointer<u8>) -> sp_wasm_interface::Result<()> {
|
||||
let memory = self.caller.data().memory();
|
||||
let (memory, data) = memory.data_and_store_mut(&mut self.caller);
|
||||
data.host_state_mut()
|
||||
.expect("host state is not empty when calling a function in wasm; qed")
|
||||
let mut allocator = self
|
||||
.host_state_mut()
|
||||
.allocator
|
||||
.deallocate(memory, ptr)
|
||||
.map_err(|e| e.to_string())
|
||||
.take()
|
||||
.expect("allocator is not empty when calling a function in wasm; qed");
|
||||
|
||||
// We can not return on error early, as we need to store back allocator.
|
||||
let res = allocator
|
||||
.deallocate(&mut MemoryWrapper(&memory, &mut self.caller), ptr)
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
self.host_state_mut().allocator = Some(allocator);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn register_panic_error_message(&mut self, message: &str) {
|
||||
|
||||
@@ -26,8 +26,7 @@ use sc_executor_common::{
|
||||
};
|
||||
use sp_wasm_interface::{Pointer, Value, WordSize};
|
||||
use wasmtime::{
|
||||
AsContext, AsContextMut, Engine, Extern, Func, Global, Instance, InstancePre, Memory, Table,
|
||||
Val,
|
||||
AsContext, AsContextMut, Engine, Extern, Instance, InstancePre, Memory, Table, Val,
|
||||
};
|
||||
|
||||
/// Invoked entrypoint format.
|
||||
@@ -113,66 +112,58 @@ impl EntryPoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around [`Memory`] that implements [`sc_allocator::Memory`].
|
||||
pub(crate) struct MemoryWrapper<'a, C>(pub &'a wasmtime::Memory, pub &'a mut C);
|
||||
|
||||
impl<C: AsContextMut> sc_allocator::Memory for MemoryWrapper<'_, C> {
|
||||
fn with_access<R>(&self, run: impl FnOnce(&[u8]) -> R) -> R {
|
||||
run(self.0.data(&self.1))
|
||||
}
|
||||
|
||||
fn with_access_mut<R>(&mut self, run: impl FnOnce(&mut [u8]) -> R) -> R {
|
||||
run(self.0.data_mut(&mut self.1))
|
||||
}
|
||||
|
||||
fn grow(&mut self, additional: u32) -> std::result::Result<(), ()> {
|
||||
self.0
|
||||
.grow(&mut self.1, additional as u64)
|
||||
.map_err(|e| {
|
||||
log::error!(
|
||||
target: "wasm-executor",
|
||||
"Failed to grow memory by {} pages: {}",
|
||||
additional,
|
||||
e,
|
||||
)
|
||||
})
|
||||
.map(drop)
|
||||
}
|
||||
|
||||
fn pages(&self) -> u32 {
|
||||
self.0.size(&self.1) as u32
|
||||
}
|
||||
|
||||
fn max_pages(&self) -> Option<u32> {
|
||||
self.0.ty(&self.1).maximum().map(|p| p as _)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap the given WebAssembly Instance of a wasm module with Substrate-runtime.
|
||||
///
|
||||
/// This struct is a handy wrapper around a wasmtime `Instance` that provides substrate specific
|
||||
/// routines.
|
||||
pub struct InstanceWrapper {
|
||||
instance: Instance,
|
||||
/// The memory instance of the `instance`.
|
||||
///
|
||||
/// It is important to make sure that we don't make any copies of this to make it easier to
|
||||
/// proof
|
||||
memory: Memory,
|
||||
store: Store,
|
||||
}
|
||||
|
||||
fn extern_memory(extern_: &Extern) -> Option<&Memory> {
|
||||
match extern_ {
|
||||
Extern::Memory(mem) => Some(mem),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extern_global(extern_: &Extern) -> Option<&Global> {
|
||||
match extern_ {
|
||||
Extern::Global(glob) => Some(glob),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extern_table(extern_: &Extern) -> Option<&Table> {
|
||||
match extern_ {
|
||||
Extern::Table(table) => Some(table),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extern_func(extern_: &Extern) -> Option<&Func> {
|
||||
match extern_ {
|
||||
Extern::Func(func) => Some(func),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_store(engine: &wasmtime::Engine, max_memory_size: Option<usize>) -> Store {
|
||||
let limits = if let Some(max_memory_size) = max_memory_size {
|
||||
wasmtime::StoreLimitsBuilder::new().memory_size(max_memory_size).build()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let mut store =
|
||||
Store::new(engine, StoreData { limits, host_state: None, memory: None, table: None });
|
||||
if max_memory_size.is_some() {
|
||||
store.limiter(|s| &mut s.limits);
|
||||
}
|
||||
store
|
||||
}
|
||||
|
||||
impl InstanceWrapper {
|
||||
pub(crate) fn new(
|
||||
engine: &Engine,
|
||||
instance_pre: &InstancePre<StoreData>,
|
||||
max_memory_size: Option<usize>,
|
||||
) -> Result<Self> {
|
||||
let mut store = create_store(engine, max_memory_size);
|
||||
pub(crate) fn new(engine: &Engine, instance_pre: &InstancePre<StoreData>) -> Result<Self> {
|
||||
let mut store = Store::new(engine, Default::default());
|
||||
let instance = instance_pre.instantiate(&mut store).map_err(|error| {
|
||||
WasmError::Other(format!(
|
||||
"failed to instantiate a new WASM module instance: {:#}",
|
||||
@@ -201,9 +192,10 @@ impl InstanceWrapper {
|
||||
self.instance.get_export(&mut self.store, method).ok_or_else(|| {
|
||||
Error::from(format!("Exported method {} is not found", method))
|
||||
})?;
|
||||
let func = extern_func(&export)
|
||||
let func = export
|
||||
.into_func()
|
||||
.ok_or_else(|| Error::from(format!("Export {} is not a function", method)))?;
|
||||
EntryPoint::direct(*func, &self.store).map_err(|_| {
|
||||
EntryPoint::direct(func, &self.store).map_err(|_| {
|
||||
Error::from(format!("Exported function '{}' has invalid signature.", method))
|
||||
})?
|
||||
},
|
||||
@@ -259,7 +251,8 @@ impl InstanceWrapper {
|
||||
.get_export(&mut self.store, "__heap_base")
|
||||
.ok_or_else(|| Error::from("__heap_base is not found"))?;
|
||||
|
||||
let heap_base_global = extern_global(&heap_base_export)
|
||||
let heap_base_global = heap_base_export
|
||||
.into_global()
|
||||
.ok_or_else(|| Error::from("__heap_base is not a global"))?;
|
||||
|
||||
let heap_base = heap_base_global
|
||||
@@ -277,7 +270,7 @@ impl InstanceWrapper {
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let global = extern_global(&global).ok_or_else(|| format!("`{}` is not a global", name))?;
|
||||
let global = global.into_global().ok_or_else(|| format!("`{}` is not a global", name))?;
|
||||
|
||||
match global.get(&mut self.store) {
|
||||
Val::I32(val) => Ok(Some(Value::I32(val))),
|
||||
@@ -300,7 +293,8 @@ fn get_linear_memory(instance: &Instance, ctx: impl AsContextMut) -> Result<Memo
|
||||
.get_export(ctx, "memory")
|
||||
.ok_or_else(|| Error::from("memory is not exported under `memory` name"))?;
|
||||
|
||||
let memory = *extern_memory(&memory_export)
|
||||
let memory = memory_export
|
||||
.into_memory()
|
||||
.ok_or_else(|| Error::from("the `memory` export should have memory type"))?;
|
||||
|
||||
Ok(memory)
|
||||
@@ -311,8 +305,8 @@ fn get_table(instance: &Instance, ctx: &mut Store) -> Option<Table> {
|
||||
instance
|
||||
.get_export(ctx, "__indirect_function_table")
|
||||
.as_ref()
|
||||
.and_then(extern_table)
|
||||
.cloned()
|
||||
.and_then(Extern::into_table)
|
||||
}
|
||||
|
||||
/// Functions related to memory.
|
||||
@@ -403,7 +397,7 @@ fn decommit_works() {
|
||||
let module = wasmtime::Module::new(&engine, code).unwrap();
|
||||
let linker = wasmtime::Linker::new(&engine);
|
||||
let instance_pre = linker.instantiate_pre(&module).unwrap();
|
||||
let mut wrapper = InstanceWrapper::new(&engine, &instance_pre, None).unwrap();
|
||||
let mut wrapper = InstanceWrapper::new(&engine, &instance_pre).unwrap();
|
||||
unsafe { *wrapper.memory.data_ptr(&wrapper.store) = 42 };
|
||||
assert_eq!(unsafe { *wrapper.memory.data_ptr(&wrapper.store) }, 42);
|
||||
wrapper.decommit();
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
use crate::{
|
||||
host::HostState,
|
||||
instance_wrapper::{EntryPoint, InstanceWrapper},
|
||||
instance_wrapper::{EntryPoint, InstanceWrapper, MemoryWrapper},
|
||||
util::{self, replace_strategy_if_broken},
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ use sc_executor_common::{
|
||||
self, DataSegmentsSnapshot, ExposedMutableGlobalsSet, GlobalsSnapshot, RuntimeBlob,
|
||||
},
|
||||
util::checked_range,
|
||||
wasm_runtime::{InvokeMethod, WasmInstance, WasmModule},
|
||||
wasm_runtime::{HeapAllocStrategy, InvokeMethod, WasmInstance, WasmModule},
|
||||
};
|
||||
use sp_runtime_interface::unpack_ptr_and_len;
|
||||
use sp_wasm_interface::{HostFunctions, Pointer, Value, WordSize};
|
||||
@@ -42,12 +42,10 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use wasmtime::{AsContext, Engine, Memory, StoreLimits, Table};
|
||||
use wasmtime::{AsContext, Engine, Memory, Table};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct StoreData {
|
||||
/// The limits we apply to the store. We need to store it here to return a reference to this
|
||||
/// object when we have the limits enabled.
|
||||
pub(crate) limits: StoreLimits,
|
||||
/// This will only be set when we call into the runtime.
|
||||
pub(crate) host_state: Option<HostState>,
|
||||
/// This will be always set once the store is initialized.
|
||||
@@ -83,12 +81,11 @@ enum Strategy {
|
||||
struct InstanceCreator {
|
||||
engine: wasmtime::Engine,
|
||||
instance_pre: Arc<wasmtime::InstancePre<StoreData>>,
|
||||
max_memory_size: Option<usize>,
|
||||
}
|
||||
|
||||
impl InstanceCreator {
|
||||
fn instantiate(&mut self) -> Result<InstanceWrapper> {
|
||||
InstanceWrapper::new(&self.engine, &self.instance_pre, self.max_memory_size)
|
||||
InstanceWrapper::new(&self.engine, &self.instance_pre)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,18 +125,13 @@ pub struct WasmtimeRuntime {
|
||||
engine: wasmtime::Engine,
|
||||
instance_pre: Arc<wasmtime::InstancePre<StoreData>>,
|
||||
instantiation_strategy: InternalInstantiationStrategy,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl WasmModule for WasmtimeRuntime {
|
||||
fn new_instance(&self) -> Result<Box<dyn WasmInstance>> {
|
||||
let strategy = match self.instantiation_strategy {
|
||||
InternalInstantiationStrategy::LegacyInstanceReuse(ref snapshot_data) => {
|
||||
let mut instance_wrapper = InstanceWrapper::new(
|
||||
&self.engine,
|
||||
&self.instance_pre,
|
||||
self.config.semantics.max_memory_size,
|
||||
)?;
|
||||
let mut instance_wrapper = InstanceWrapper::new(&self.engine, &self.instance_pre)?;
|
||||
let heap_base = instance_wrapper.extract_heap_base()?;
|
||||
|
||||
// This function panics if the instance was created from a runtime blob different
|
||||
@@ -161,7 +153,6 @@ impl WasmModule for WasmtimeRuntime {
|
||||
InternalInstantiationStrategy::Builtin => Strategy::RecreateInstance(InstanceCreator {
|
||||
engine: self.engine.clone(),
|
||||
instance_pre: self.instance_pre.clone(),
|
||||
max_memory_size: self.config.semantics.max_memory_size,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -350,25 +341,22 @@ fn common_config(semantics: &Semantics) -> std::result::Result<wasmtime::Config,
|
||||
InstantiationStrategy::LegacyInstanceReuse => (false, false),
|
||||
};
|
||||
|
||||
const WASM_PAGE_SIZE: u64 = 65536;
|
||||
|
||||
config.memory_init_cow(use_cow);
|
||||
config.memory_guaranteed_dense_image_size(
|
||||
semantics.max_memory_size.map(|max| max as u64).unwrap_or(u64::MAX),
|
||||
);
|
||||
config.memory_guaranteed_dense_image_size(match semantics.heap_alloc_strategy {
|
||||
HeapAllocStrategy::Dynamic { maximum_pages } =>
|
||||
maximum_pages.map(|p| p as u64 * WASM_PAGE_SIZE).unwrap_or(u64::MAX),
|
||||
HeapAllocStrategy::Static { .. } => u64::MAX,
|
||||
});
|
||||
|
||||
if use_pooling {
|
||||
const WASM_PAGE_SIZE: u64 = 65536;
|
||||
const MAX_WASM_PAGES: u64 = 0x10000;
|
||||
|
||||
let memory_pages = if let Some(max_memory_size) = semantics.max_memory_size {
|
||||
let max_memory_size = max_memory_size as u64;
|
||||
let mut pages = max_memory_size / WASM_PAGE_SIZE;
|
||||
if max_memory_size % WASM_PAGE_SIZE != 0 {
|
||||
pages += 1;
|
||||
}
|
||||
|
||||
std::cmp::min(MAX_WASM_PAGES, pages)
|
||||
} else {
|
||||
MAX_WASM_PAGES
|
||||
let memory_pages = match semantics.heap_alloc_strategy {
|
||||
HeapAllocStrategy::Dynamic { maximum_pages } =>
|
||||
maximum_pages.map(|p| p as u64).unwrap_or(MAX_WASM_PAGES),
|
||||
HeapAllocStrategy::Static { .. } => MAX_WASM_PAGES,
|
||||
};
|
||||
|
||||
let mut pooling_config = wasmtime::PoolingAllocationConfig::default();
|
||||
@@ -514,25 +502,8 @@ pub struct Semantics {
|
||||
/// Configures wasmtime to use multiple threads for compiling.
|
||||
pub parallel_compilation: bool,
|
||||
|
||||
/// The number of extra WASM pages which will be allocated
|
||||
/// on top of what is requested by the WASM blob itself.
|
||||
pub extra_heap_pages: u64,
|
||||
|
||||
/// The total amount of memory in bytes an instance can request.
|
||||
///
|
||||
/// If specified, the runtime will be able to allocate only that much of wasm memory.
|
||||
/// This is the total number and therefore the [`Semantics::extra_heap_pages`] is accounted
|
||||
/// for.
|
||||
///
|
||||
/// That means that the initial number of pages of a linear memory plus the
|
||||
/// [`Semantics::extra_heap_pages`] multiplied by the wasm page size (64KiB) should be less
|
||||
/// than or equal to `max_memory_size`, otherwise the instance won't be created.
|
||||
///
|
||||
/// Moreover, `memory.grow` will fail (return -1) if the sum of sizes of currently mounted
|
||||
/// and additional pages exceeds `max_memory_size`.
|
||||
///
|
||||
/// The default is `None`.
|
||||
pub max_memory_size: Option<usize>,
|
||||
/// The heap allocation strategy to use.
|
||||
pub heap_alloc_strategy: HeapAllocStrategy,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -689,12 +660,7 @@ where
|
||||
.instantiate_pre(&module)
|
||||
.map_err(|e| WasmError::Other(format!("cannot preinstantiate module: {:#}", e)))?;
|
||||
|
||||
Ok(WasmtimeRuntime {
|
||||
engine,
|
||||
instance_pre: Arc::new(instance_pre),
|
||||
instantiation_strategy,
|
||||
config,
|
||||
})
|
||||
Ok(WasmtimeRuntime { engine, instance_pre: Arc::new(instance_pre), instantiation_strategy })
|
||||
}
|
||||
|
||||
fn prepare_blob_for_compilation(
|
||||
@@ -717,12 +683,7 @@ fn prepare_blob_for_compilation(
|
||||
// now automatically take care of creating the memory for us, and it is also necessary
|
||||
// to enable `wasmtime`'s instance pooling. (Imported memories are ineligible for pooling.)
|
||||
blob.convert_memory_import_into_export()?;
|
||||
blob.add_extra_heap_pages_to_memory_section(
|
||||
semantics
|
||||
.extra_heap_pages
|
||||
.try_into()
|
||||
.map_err(|e| WasmError::Other(format!("invalid `extra_heap_pages`: {}", e)))?,
|
||||
)?;
|
||||
blob.setup_memory_according_to_heap_alloc_strategy(semantics.heap_alloc_strategy)?;
|
||||
|
||||
Ok(blob)
|
||||
}
|
||||
@@ -783,9 +744,8 @@ fn inject_input_data(
|
||||
) -> Result<(Pointer<u8>, WordSize)> {
|
||||
let mut ctx = instance.store_mut();
|
||||
let memory = ctx.data().memory();
|
||||
let memory = memory.data_mut(&mut ctx);
|
||||
let data_len = data.len() as WordSize;
|
||||
let data_ptr = allocator.allocate(memory, data_len)?;
|
||||
let data_ptr = allocator.allocate(&mut MemoryWrapper(&memory, &mut ctx), data_len)?;
|
||||
util::write_memory_from(instance.store_mut(), data_ptr, data)?;
|
||||
Ok((data_ptr, data_len))
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::{Decode as _, Encode as _};
|
||||
use sc_executor_common::{error::Error, runtime_blob::RuntimeBlob, wasm_runtime::WasmModule};
|
||||
use sc_executor_common::{
|
||||
error::Error,
|
||||
runtime_blob::RuntimeBlob,
|
||||
wasm_runtime::{HeapAllocStrategy, WasmModule},
|
||||
};
|
||||
use sc_runtime_test::wasm_binary_unwrap;
|
||||
|
||||
use crate::InstantiationStrategy;
|
||||
@@ -77,8 +81,7 @@ struct RuntimeBuilder {
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
canonicalize_nans: bool,
|
||||
deterministic_stack: bool,
|
||||
extra_heap_pages: u64,
|
||||
max_memory_size: Option<usize>,
|
||||
heap_pages: HeapAllocStrategy,
|
||||
precompile_runtime: bool,
|
||||
tmpdir: Option<tempfile::TempDir>,
|
||||
}
|
||||
@@ -90,8 +93,7 @@ impl RuntimeBuilder {
|
||||
instantiation_strategy,
|
||||
canonicalize_nans: false,
|
||||
deterministic_stack: false,
|
||||
extra_heap_pages: 1024,
|
||||
max_memory_size: None,
|
||||
heap_pages: HeapAllocStrategy::Static { extra_pages: 1024 },
|
||||
precompile_runtime: false,
|
||||
tmpdir: None,
|
||||
}
|
||||
@@ -117,8 +119,8 @@ impl RuntimeBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
fn max_memory_size(mut self, max_memory_size: Option<usize>) -> Self {
|
||||
self.max_memory_size = max_memory_size;
|
||||
fn heap_alloc_strategy(mut self, heap_pages: HeapAllocStrategy) -> Self {
|
||||
self.heap_pages = heap_pages;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -152,8 +154,7 @@ impl RuntimeBuilder {
|
||||
},
|
||||
canonicalize_nans: self.canonicalize_nans,
|
||||
parallel_compilation: true,
|
||||
extra_heap_pages: self.extra_heap_pages,
|
||||
max_memory_size: self.max_memory_size,
|
||||
heap_alloc_strategy: self.heap_pages,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -227,7 +228,7 @@ fn deep_call_stack_wat(depth: usize) -> String {
|
||||
// We need two limits here since depending on whether the code is compiled in debug
|
||||
// or in release mode the maximum call depth is slightly different.
|
||||
const CALL_DEPTH_LOWER_LIMIT: usize = 65455;
|
||||
const CALL_DEPTH_UPPER_LIMIT: usize = 65503;
|
||||
const CALL_DEPTH_UPPER_LIMIT: usize = 65509;
|
||||
|
||||
test_wasm_execution!(test_consume_under_1mb_of_stack_does_not_trap);
|
||||
fn test_consume_under_1mb_of_stack_does_not_trap(instantiation_strategy: InstantiationStrategy) {
|
||||
@@ -344,29 +345,25 @@ fn test_max_memory_pages(
|
||||
import_memory: bool,
|
||||
precompile_runtime: bool,
|
||||
) {
|
||||
fn try_instantiate(
|
||||
max_memory_size: Option<usize>,
|
||||
fn call(
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
wat: String,
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
precompile_runtime: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy)
|
||||
.use_wat(wat)
|
||||
.max_memory_size(max_memory_size)
|
||||
.heap_alloc_strategy(heap_alloc_strategy)
|
||||
.precompile_runtime(precompile_runtime);
|
||||
|
||||
let runtime = builder.build();
|
||||
let mut instance = runtime.new_instance()?;
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
let _ = instance.call_export("main", &[])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn memory(initial: u32, maximum: Option<u32>, import: bool) -> String {
|
||||
let memory = if let Some(maximum) = maximum {
|
||||
format!("(memory $0 {} {})", initial, maximum)
|
||||
} else {
|
||||
format!("(memory $0 {})", initial)
|
||||
};
|
||||
fn memory(initial: u32, maximum: u32, import: bool) -> String {
|
||||
let memory = format!("(memory $0 {} {})", initial, maximum);
|
||||
|
||||
if import {
|
||||
format!("(import \"env\" \"memory\" {})", memory)
|
||||
@@ -375,152 +372,90 @@ fn test_max_memory_pages(
|
||||
}
|
||||
}
|
||||
|
||||
const WASM_PAGE_SIZE: usize = 65536;
|
||||
let assert_grow_ok = |alloc_strategy: HeapAllocStrategy, initial_pages: u32, max_pages: u32| {
|
||||
eprintln!("assert_grow_ok({alloc_strategy:?}, {initial_pages}, {max_pages})");
|
||||
|
||||
// check the old behavior if preserved. That is, if no limit is set we allow 4 GiB of memory.
|
||||
try_instantiate(
|
||||
None,
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#,
|
||||
/*
|
||||
We want to allocate the maximum number of pages supported in wasm for this test.
|
||||
However, due to a bug in wasmtime (I think wasmi is also affected) it is only possible
|
||||
to allocate 65536 - 1 pages.
|
||||
call(
|
||||
alloc_strategy,
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
|
||||
Then, during creation of the Substrate Runtime instance, 1024 (heap_pages) pages are
|
||||
mounted.
|
||||
|
||||
Thus 65535 = 64511 + 1024
|
||||
*/
|
||||
memory(64511, None, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// max is not specified, therefore it's implied to be 65536 pages (4 GiB).
|
||||
//
|
||||
// max_memory_size = (1 (initial) + 1024 (heap_pages)) * WASM_PAGE_SIZE
|
||||
try_instantiate(
|
||||
Some((1 + 1024) * WASM_PAGE_SIZE),
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#,
|
||||
// 1 initial, max is not specified.
|
||||
memory(1, None, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// max is specified explicitly to 2048 pages.
|
||||
try_instantiate(
|
||||
Some((1 + 1024) * WASM_PAGE_SIZE),
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#,
|
||||
// Max is 2048.
|
||||
memory(1, Some(2048), import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// memory grow should work as long as it doesn't exceed 1025 pages in total.
|
||||
try_instantiate(
|
||||
Some((0 + 1024 + 25) * WASM_PAGE_SIZE),
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
|
||||
;; assert(memory.grow returns != -1)
|
||||
(if
|
||||
(i32.eq
|
||||
(memory.grow
|
||||
(i32.const 25)
|
||||
;; assert(memory.grow returns != -1)
|
||||
(if
|
||||
(i32.eq
|
||||
(memory.grow
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.const -1)
|
||||
)
|
||||
(unreachable)
|
||||
)
|
||||
(i32.const -1)
|
||||
|
||||
(i64.const 0)
|
||||
)
|
||||
(unreachable)
|
||||
)
|
||||
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#,
|
||||
// Zero starting pages.
|
||||
memory(0, None, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap();
|
||||
memory(initial_pages, max_pages, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
// We start with 1025 pages and try to grow at least one.
|
||||
try_instantiate(
|
||||
Some((1 + 1024) * WASM_PAGE_SIZE),
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
let assert_grow_fail =
|
||||
|alloc_strategy: HeapAllocStrategy, initial_pages: u32, max_pages: u32| {
|
||||
eprintln!("assert_grow_fail({alloc_strategy:?}, {initial_pages}, {max_pages})");
|
||||
|
||||
;; assert(memory.grow returns == -1)
|
||||
(if
|
||||
(i32.ne
|
||||
(memory.grow
|
||||
(i32.const 1)
|
||||
call(
|
||||
alloc_strategy,
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
|
||||
;; assert(memory.grow returns == -1)
|
||||
(if
|
||||
(i32.ne
|
||||
(memory.grow
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.const -1)
|
||||
)
|
||||
(unreachable)
|
||||
)
|
||||
|
||||
(i64.const 0)
|
||||
)
|
||||
(i32.const -1)
|
||||
)
|
||||
(unreachable)
|
||||
)
|
||||
|
||||
(i64.const 0)
|
||||
)
|
||||
"#,
|
||||
memory(initial_pages, max_pages, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
"#,
|
||||
// Initial=1, meaning after heap pages mount the total will be already 1025.
|
||||
memory(1, None, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: Some(10) }, 1, 10);
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: Some(10) }, 9, 10);
|
||||
assert_grow_fail(HeapAllocStrategy::Dynamic { maximum_pages: Some(10) }, 10, 10);
|
||||
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: None }, 1, 10);
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: None }, 9, 10);
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: None }, 10, 10);
|
||||
|
||||
assert_grow_fail(HeapAllocStrategy::Static { extra_pages: 10 }, 1, 10);
|
||||
assert_grow_fail(HeapAllocStrategy::Static { extra_pages: 10 }, 9, 10);
|
||||
assert_grow_fail(HeapAllocStrategy::Static { extra_pages: 10 }, 10, 10);
|
||||
}
|
||||
|
||||
// This test takes quite a while to execute in a debug build (over 6 minutes on a TR 3970x)
|
||||
@@ -538,8 +473,7 @@ fn test_instances_without_reuse_are_not_leaked() {
|
||||
deterministic_stack_limit: None,
|
||||
canonicalize_nans: false,
|
||||
parallel_compilation: true,
|
||||
extra_heap_pages: 2048,
|
||||
max_memory_size: None,
|
||||
heap_alloc_strategy: HeapAllocStrategy::Static { extra_pages: 2048 },
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -583,6 +517,10 @@ fn test_rustix_version_matches_with_wasmtime() {
|
||||
.unwrap();
|
||||
|
||||
if wasmtime_rustix.req != our_rustix.req {
|
||||
panic!("our version of rustix ({0}) doesn't match wasmtime's ({1}); bump the version in `sc-executor-wasmtime`'s `Cargo.toml' to '{1}' and try again", our_rustix.req, wasmtime_rustix.req);
|
||||
panic!(
|
||||
"our version of rustix ({0}) doesn't match wasmtime's ({1}); \
|
||||
bump the version in `sc-executor-wasmtime`'s `Cargo.toml' to '{1}' and try again",
|
||||
our_rustix.req, wasmtime_rustix.req,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user