diff --git a/Cargo.lock b/Cargo.lock index ff14da370db9c..03bc44a0a848a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4173,6 +4173,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memfd" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6627dc657574b49d6ad27105ed671822be56e0d2547d413bfbf3e8d8fa92e7a" +dependencies = [ + "libc", +] + [[package]] name = "memmap" version = "0.7.0" @@ -8197,6 +8206,7 @@ dependencies = [ "hex-literal", "lazy_static", "lru", + "num_cpus", "parity-scale-codec", "parking_lot 0.12.0", "paste 1.0.6", @@ -8221,6 +8231,7 @@ dependencies = [ "sp-version", "sp-wasm-interface", "substrate-test-runtime", + "tempfile", "tracing", "tracing-subscriber", "wasmi", @@ -8274,6 +8285,7 @@ dependencies = [ "sp-runtime-interface", "sp-sandbox", "sp-wasm-interface", + "tempfile", "wasmtime", "wat", ] @@ -11838,6 +11850,7 @@ dependencies = [ "libc", "log", "mach", + "memfd", "memoffset", "more-asserts", "rand 0.8.4", diff --git a/bin/node/cli/benches/block_production.rs b/bin/node/cli/benches/block_production.rs index 376241d8157bf..ad16ba8e4072b 100644 --- a/bin/node/cli/benches/block_production.rs +++ b/bin/node/cli/benches/block_production.rs @@ -29,7 +29,7 @@ use sc_consensus::{ use sc_service::{ config::{ DatabaseSource, KeepBlocks, KeystoreConfig, NetworkConfiguration, OffchainWorkerConfig, - PruningMode, WasmExecutionMethod, + PruningMode, WasmExecutionMethod, WasmtimeInstantiationStrategy, }, BasePath, Configuration, Role, }; @@ -77,7 +77,9 @@ fn new_node(tokio_handle: Handle) -> node_cli::service::NewFullBase { state_pruning: Some(PruningMode::ArchiveAll), keep_blocks: KeepBlocks::All, chain_spec: spec, - wasm_method: WasmExecutionMethod::Compiled, + wasm_method: WasmExecutionMethod::Compiled { + instantiation_strategy: WasmtimeInstantiationStrategy::PoolingCopyOnWrite, + }, execution_strategies: ExecutionStrategies { syncing: execution_strategy, importing: execution_strategy, diff --git a/bin/node/executor/benches/bench.rs b/bin/node/executor/benches/bench.rs index 3d7c264a89d1c..61e2d1b053012 100644 --- a/bin/node/executor/benches/bench.rs +++ b/bin/node/executor/benches/bench.rs @@ -25,6 +25,8 @@ use node_runtime::{ UncheckedExtrinsic, }; use node_testing::keyring::*; +#[cfg(feature = "wasmtime")] +use sc_executor::WasmtimeInstantiationStrategy; use sc_executor::{Externalities, NativeElseWasmExecutor, RuntimeVersionOf, WasmExecutionMethod}; use sp_core::{ storage::well_known_keys, @@ -183,7 +185,9 @@ fn bench_execute_block(c: &mut Criterion) { ExecutionMethod::Native, ExecutionMethod::Wasm(WasmExecutionMethod::Interpreted), #[cfg(feature = "wasmtime")] - ExecutionMethod::Wasm(WasmExecutionMethod::Compiled), + ExecutionMethod::Wasm(WasmExecutionMethod::Compiled { + instantiation_strategy: WasmtimeInstantiationStrategy::PoolingCopyOnWrite, + }), ]; for strategy in execution_methods { diff --git a/bin/node/testing/src/bench.rs b/bin/node/testing/src/bench.rs index e5287dc3c4af2..00ce7f64bc3f0 100644 --- a/bin/node/testing/src/bench.rs +++ b/bin/node/testing/src/bench.rs @@ -46,7 +46,7 @@ use sc_client_api::{ }; use sc_client_db::PruningMode; use sc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, ImportResult, ImportedAux}; -use sc_executor::{NativeElseWasmExecutor, WasmExecutionMethod}; +use sc_executor::{NativeElseWasmExecutor, WasmExecutionMethod, WasmtimeInstantiationStrategy}; use sp_api::ProvideRuntimeApi; use sp_block_builder::BlockBuilder; use sp_consensus::BlockOrigin; @@ -398,7 +398,14 @@ impl BenchDb { let backend = sc_service::new_db_backend(db_config).expect("Should not fail"); let client = sc_service::new_client( backend.clone(), - NativeElseWasmExecutor::new(WasmExecutionMethod::Compiled, None, 8, 2), + NativeElseWasmExecutor::new( + WasmExecutionMethod::Compiled { + instantiation_strategy: WasmtimeInstantiationStrategy::PoolingCopyOnWrite, + }, + None, + 8, + 2, + ), &keyring.generate_genesis(), None, None, diff --git a/client/cli/src/arg_enums.rs b/client/cli/src/arg_enums.rs index ac50413803278..bc0989cf34659 100644 --- a/client/cli/src/arg_enums.rs +++ b/client/cli/src/arg_enums.rs @@ -20,6 +20,36 @@ use clap::ArgEnum; +/// The instantiation strategy to use in compiled mode. +#[derive(Debug, Clone, Copy, ArgEnum)] +#[clap(rename_all = "kebab-case")] +pub enum WasmtimeInstantiationStrategy { + /// Pool the instances to avoid initializing everything from scratch + /// on each instantiation. Use copy-on-write memory when possible. + PoolingCopyOnWrite, + + /// Recreate the instance from scratch on every instantiation. + /// Use copy-on-write memory when possible. + RecreateInstanceCopyOnWrite, + + /// Pool the instances to avoid initializing everything from scratch + /// on each instantiation. + Pooling, + + /// Recreate the instance from scratch on every instantiation. Very slow. + RecreateInstance, + + /// Legacy instance reuse mechanism. DEPRECATED. Will be removed in the future. + /// + /// Should only be used in case of encountering any issues with the new default + /// instantiation strategy. + LegacyInstanceReuse, +} + +/// The default [`WasmtimeInstantiationStrategy`]. +pub const DEFAULT_WASMTIME_INSTANTIATION_STRATEGY: WasmtimeInstantiationStrategy = + WasmtimeInstantiationStrategy::PoolingCopyOnWrite; + /// How to execute Wasm runtime code. #[derive(Debug, Clone, Copy)] pub enum WasmExecutionMethod { @@ -71,18 +101,33 @@ impl WasmExecutionMethod { } } -impl Into for WasmExecutionMethod { - fn into(self) -> sc_service::config::WasmExecutionMethod { - match self { - WasmExecutionMethod::Interpreted => - sc_service::config::WasmExecutionMethod::Interpreted, - #[cfg(feature = "wasmtime")] - WasmExecutionMethod::Compiled => sc_service::config::WasmExecutionMethod::Compiled, - #[cfg(not(feature = "wasmtime"))] - WasmExecutionMethod::Compiled => panic!( - "Substrate must be compiled with \"wasmtime\" feature for compiled Wasm execution" - ), - } +/// Converts the execution method and instantiation strategy command line arguments +/// into an execution method which can be used internally. +pub fn execution_method_from_cli( + execution_method: WasmExecutionMethod, + _instantiation_strategy: WasmtimeInstantiationStrategy, +) -> sc_service::config::WasmExecutionMethod { + match execution_method { + WasmExecutionMethod::Interpreted => sc_service::config::WasmExecutionMethod::Interpreted, + #[cfg(feature = "wasmtime")] + WasmExecutionMethod::Compiled => sc_service::config::WasmExecutionMethod::Compiled { + instantiation_strategy: match _instantiation_strategy { + WasmtimeInstantiationStrategy::PoolingCopyOnWrite => + sc_service::config::WasmtimeInstantiationStrategy::PoolingCopyOnWrite, + WasmtimeInstantiationStrategy::RecreateInstanceCopyOnWrite => + sc_service::config::WasmtimeInstantiationStrategy::RecreateInstanceCopyOnWrite, + WasmtimeInstantiationStrategy::Pooling => + sc_service::config::WasmtimeInstantiationStrategy::Pooling, + WasmtimeInstantiationStrategy::RecreateInstance => + sc_service::config::WasmtimeInstantiationStrategy::RecreateInstance, + WasmtimeInstantiationStrategy::LegacyInstanceReuse => + sc_service::config::WasmtimeInstantiationStrategy::LegacyInstanceReuse, + }, + }, + #[cfg(not(feature = "wasmtime"))] + WasmExecutionMethod::Compiled => panic!( + "Substrate must be compiled with \"wasmtime\" feature for compiled Wasm execution" + ), } } diff --git a/client/cli/src/params/import_params.rs b/client/cli/src/params/import_params.rs index 4c9b334150557..aef7511ffc371 100644 --- a/client/cli/src/params/import_params.rs +++ b/client/cli/src/params/import_params.rs @@ -18,10 +18,11 @@ use crate::{ arg_enums::{ - ExecutionStrategy, WasmExecutionMethod, DEFAULT_EXECUTION_BLOCK_CONSTRUCTION, - DEFAULT_EXECUTION_IMPORT_BLOCK, DEFAULT_EXECUTION_IMPORT_BLOCK_VALIDATOR, - DEFAULT_EXECUTION_OFFCHAIN_WORKER, DEFAULT_EXECUTION_OTHER, DEFAULT_EXECUTION_SYNCING, - DEFAULT_WASM_EXECUTION_METHOD, + ExecutionStrategy, WasmExecutionMethod, WasmtimeInstantiationStrategy, + DEFAULT_EXECUTION_BLOCK_CONSTRUCTION, DEFAULT_EXECUTION_IMPORT_BLOCK, + DEFAULT_EXECUTION_IMPORT_BLOCK_VALIDATOR, DEFAULT_EXECUTION_OFFCHAIN_WORKER, + DEFAULT_EXECUTION_OTHER, DEFAULT_EXECUTION_SYNCING, + DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, DEFAULT_WASM_EXECUTION_METHOD, }, params::{DatabaseParams, PruningParams}, }; @@ -62,6 +63,27 @@ pub struct ImportParams { )] pub wasm_method: WasmExecutionMethod, + /// The WASM instantiation method to use. + /// + /// Only has an effect when `wasm-execution` is set to `compiled`. + /// + /// The copy-on-write strategies are only supported on Linux. + /// If the copy-on-write variant of a strategy is unsupported + /// the executor will fall back to the non-CoW equivalent. + /// + /// The fastest (and the default) strategy available is `pooling-copy-on-write`. + /// + /// The `legacy-instance-reuse` strategy is deprecated and will + /// be removed in the future. It should only be used in case of + /// issues with the default instantiation strategy. + #[clap( + long, + value_name = "STRATEGY", + default_value_t = DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, + arg_enum, + )] + pub wasmtime_instantiation_strategy: WasmtimeInstantiationStrategy, + /// Specify the path where local WASM runtimes are stored. /// /// These runtimes will override on-chain runtimes when the version matches. @@ -85,7 +107,7 @@ impl ImportParams { /// Get the WASM execution method from the parameters pub fn wasm_method(&self) -> sc_service::config::WasmExecutionMethod { - self.wasm_method.into() + crate::execution_method_from_cli(self.wasm_method, self.wasmtime_instantiation_strategy) } /// Enable overriding on-chain WASM with locally-stored WASM diff --git a/client/executor/Cargo.toml b/client/executor/Cargo.toml index ad2288b9272d3..566ed0a50fc0f 100644 --- a/client/executor/Cargo.toml +++ b/client/executor/Cargo.toml @@ -50,6 +50,8 @@ paste = "1.0" regex = "1.5.5" criterion = "0.3" env_logger = "0.9" +num_cpus = "1.13.1" +tempfile = "3.3.0" [[bench]] name = "bench" diff --git a/client/executor/benches/bench.rs b/client/executor/benches/bench.rs index 49ea8be50624e..fcefe408603d7 100644 --- a/client/executor/benches/bench.rs +++ b/client/executor/benches/bench.rs @@ -17,26 +17,43 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use sc_executor_common::{runtime_blob::RuntimeBlob, wasm_runtime::WasmModule}; +use codec::Encode; + +use sc_executor_common::{ + runtime_blob::RuntimeBlob, + wasm_runtime::{WasmInstance, WasmModule}, +}; +#[cfg(feature = "wasmtime")] +use sc_executor_wasmtime::InstantiationStrategy; use sc_runtime_test::wasm_binary_unwrap as test_runtime; use sp_wasm_interface::HostFunctions as _; -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, +}; +#[derive(Clone)] enum Method { Interpreted, #[cfg(feature = "wasmtime")] Compiled { - fast_instance_reuse: bool, + instantiation_strategy: InstantiationStrategy, + precompile: bool, }, } -// This is just a bog-standard Kusama runtime with the extra `test_empty_return` -// function copy-pasted from the test runtime. +// This is just a bog-standard Kusama runtime with an extra +// `test_empty_return` and `test_dirty_plenty_memory` functions +// copy-pasted from the test runtime. fn kusama_runtime() -> &'static [u8] { include_bytes!("kusama_runtime.wasm") } -fn initialize(runtime: &[u8], method: Method) -> Arc { +fn initialize( + _tmpdir: &mut Option, + runtime: &[u8], + method: Method, +) -> Arc { let blob = RuntimeBlob::uncompress_if_needed(runtime).unwrap(); let host_functions = sp_io::SubstrateHostFunctions::host_functions(); let heap_pages = 2048; @@ -51,80 +68,200 @@ fn initialize(runtime: &[u8], method: Method) -> Arc { ) .map(|runtime| -> Arc { Arc::new(runtime) }), #[cfg(feature = "wasmtime")] - Method::Compiled { fast_instance_reuse } => - sc_executor_wasmtime::create_runtime::( - blob, - sc_executor_wasmtime::Config { + Method::Compiled { instantiation_strategy, precompile } => { + let config = sc_executor_wasmtime::Config { + allow_missing_func_imports, + cache_path: None, + semantics: sc_executor_wasmtime::Semantics { + extra_heap_pages: heap_pages, + instantiation_strategy, + deterministic_stack_limit: None, + canonicalize_nans: false, + parallel_compilation: true, max_memory_size: None, - allow_missing_func_imports, - cache_path: None, - semantics: sc_executor_wasmtime::Semantics { - extra_heap_pages: heap_pages, - fast_instance_reuse, - deterministic_stack_limit: None, - canonicalize_nans: false, - parallel_compilation: true, - }, }, - ) - .map(|runtime| -> Arc { Arc::new(runtime) }), + }; + + if precompile { + let precompiled_blob = + sc_executor_wasmtime::prepare_runtime_artifact(blob, &config.semantics) + .unwrap(); + + // Create a fresh temporary directory to make absolutely sure + // we'll use the right module. + *_tmpdir = Some(tempfile::tempdir().unwrap()); + let tmpdir = _tmpdir.as_ref().unwrap(); + + let path = tmpdir.path().join("module.bin"); + std::fs::write(&path, &precompiled_blob).unwrap(); + unsafe { + sc_executor_wasmtime::create_runtime_from_artifact::< + sp_io::SubstrateHostFunctions, + >(&path, config) + } + } else { + sc_executor_wasmtime::create_runtime::(blob, config) + } + .map(|runtime| -> Arc { Arc::new(runtime) }) + }, } .unwrap() } +fn run_benchmark( + c: &mut Criterion, + benchmark_name: &str, + thread_count: usize, + runtime: &dyn WasmModule, + testcase: impl Fn(&mut Box) + Copy + Send + 'static, +) { + c.bench_function(benchmark_name, |b| { + // Here we deliberately start a bunch of extra threads which will just + // keep on independently instantiating the runtime over and over again. + // + // We don't really have to measure how much time those take since the + // work done is essentially the same on each thread, and what we're + // interested in here is only how those extra threads affect the execution + // on the current thread. + // + // In an ideal case assuming we have enough CPU cores those extra threads + // shouldn't affect the main thread's runtime at all, however in practice + // they're not completely independent. There might be per-process + // locks in the kernel which are briefly held during instantiation, etc., + // and how much those affect the execution here is what we want to measure. + let is_benchmark_running = Arc::new(AtomicBool::new(true)); + let threads_running = Arc::new(AtomicUsize::new(0)); + let aux_threads: Vec<_> = (0..thread_count - 1) + .map(|_| { + let mut instance = runtime.new_instance().unwrap(); + let is_benchmark_running = is_benchmark_running.clone(); + let threads_running = threads_running.clone(); + std::thread::spawn(move || { + threads_running.fetch_add(1, Ordering::SeqCst); + while is_benchmark_running.load(Ordering::Relaxed) { + testcase(&mut instance); + } + }) + }) + .collect(); + + while threads_running.load(Ordering::SeqCst) != (thread_count - 1) { + std::thread::yield_now(); + } + + let mut instance = runtime.new_instance().unwrap(); + b.iter(|| testcase(&mut instance)); + + is_benchmark_running.store(false, Ordering::SeqCst); + for thread in aux_threads { + thread.join().unwrap(); + } + }); +} + fn bench_call_instance(c: &mut Criterion) { let _ = env_logger::try_init(); - #[cfg(feature = "wasmtime")] - { - let runtime = initialize(test_runtime(), Method::Compiled { fast_instance_reuse: true }); - c.bench_function("call_instance_test_runtime_with_fast_instance_reuse", |b| { - let mut instance = runtime.new_instance().unwrap(); - b.iter(|| instance.call_export("test_empty_return", &[0]).unwrap()) - }); - } + let strategies = [ + #[cfg(feature = "wasmtime")] + ( + "legacy_instance_reuse", + Method::Compiled { + instantiation_strategy: InstantiationStrategy::LegacyInstanceReuse, + precompile: false, + }, + ), + #[cfg(feature = "wasmtime")] + ( + "recreate_instance_vanilla", + Method::Compiled { + instantiation_strategy: InstantiationStrategy::RecreateInstance, + precompile: false, + }, + ), + #[cfg(feature = "wasmtime")] + ( + "recreate_instance_cow_fresh", + Method::Compiled { + instantiation_strategy: InstantiationStrategy::RecreateInstanceCopyOnWrite, + precompile: false, + }, + ), + #[cfg(feature = "wasmtime")] + ( + "recreate_instance_cow_precompiled", + Method::Compiled { + instantiation_strategy: InstantiationStrategy::RecreateInstanceCopyOnWrite, + precompile: true, + }, + ), + #[cfg(feature = "wasmtime")] + ( + "pooling_vanilla", + Method::Compiled { + instantiation_strategy: InstantiationStrategy::Pooling, + precompile: false, + }, + ), + #[cfg(feature = "wasmtime")] + ( + "pooling_cow_fresh", + Method::Compiled { + instantiation_strategy: InstantiationStrategy::PoolingCopyOnWrite, + precompile: false, + }, + ), + #[cfg(feature = "wasmtime")] + ( + "pooling_cow_precompiled", + Method::Compiled { + instantiation_strategy: InstantiationStrategy::PoolingCopyOnWrite, + precompile: true, + }, + ), + ("interpreted", Method::Interpreted), + ]; - #[cfg(feature = "wasmtime")] - { - let runtime = initialize(test_runtime(), Method::Compiled { fast_instance_reuse: false }); - c.bench_function("call_instance_test_runtime_without_fast_instance_reuse", |b| { - let mut instance = runtime.new_instance().unwrap(); - b.iter(|| instance.call_export("test_empty_return", &[0]).unwrap()); - }); - } + let runtimes = [("kusama_runtime", kusama_runtime()), ("test_runtime", test_runtime())]; - #[cfg(feature = "wasmtime")] - { - let runtime = initialize(kusama_runtime(), Method::Compiled { fast_instance_reuse: true }); - c.bench_function("call_instance_kusama_runtime_with_fast_instance_reuse", |b| { - let mut instance = runtime.new_instance().unwrap(); - b.iter(|| instance.call_export("test_empty_return", &[0]).unwrap()) - }); - } + let thread_counts = [1, 2, 4, 8, 16]; - #[cfg(feature = "wasmtime")] - { - let runtime = initialize(kusama_runtime(), Method::Compiled { fast_instance_reuse: false }); - c.bench_function("call_instance_kusama_runtime_without_fast_instance_reuse", |b| { - let mut instance = runtime.new_instance().unwrap(); - b.iter(|| instance.call_export("test_empty_return", &[0]).unwrap()); - }); + fn test_call_empty_function(instance: &mut Box) { + instance.call_export("test_empty_return", &[0]).unwrap(); } - { - let runtime = initialize(test_runtime(), Method::Interpreted); - c.bench_function("call_instance_test_runtime_interpreted", |b| { - let mut instance = runtime.new_instance().unwrap(); - b.iter(|| instance.call_export("test_empty_return", &[0]).unwrap()) - }); + fn test_dirty_1mb_of_memory(instance: &mut Box) { + instance.call_export("test_dirty_plenty_memory", &(0, 16).encode()).unwrap(); } - { - let runtime = initialize(kusama_runtime(), Method::Interpreted); - c.bench_function("call_instance_kusama_runtime_interpreted", |b| { - let mut instance = runtime.new_instance().unwrap(); - b.iter(|| instance.call_export("test_empty_return", &[0]).unwrap()) - }); + let testcases = [ + ("call_empty_function", test_call_empty_function as fn(&mut Box)), + ("dirty_1mb_of_memory", test_dirty_1mb_of_memory), + ]; + + let num_cpus = num_cpus::get_physical(); + let mut tmpdir = None; + + for (strategy_name, strategy) in strategies { + for (runtime_name, runtime) in runtimes { + let runtime = initialize(&mut tmpdir, runtime, strategy.clone()); + + for (testcase_name, testcase) in testcases { + for thread_count in thread_counts { + if thread_count > num_cpus { + // If there are not enough cores available the benchmark is pointless. + continue + } + + let benchmark_name = format!( + "{}_from_{}_with_{}_on_{}_threads", + testcase_name, runtime_name, strategy_name, thread_count + ); + + run_benchmark(c, &benchmark_name, thread_count, &*runtime, testcase); + } + } + } } } diff --git a/client/executor/benches/kusama_runtime.wasm b/client/executor/benches/kusama_runtime.wasm index 3470237fb5aee..28adce9623400 100755 Binary files a/client/executor/benches/kusama_runtime.wasm and b/client/executor/benches/kusama_runtime.wasm differ diff --git a/client/executor/src/integration_tests/linux.rs b/client/executor/src/integration_tests/linux.rs index 8775a35cb83cc..60e537299186e 100644 --- a/client/executor/src/integration_tests/linux.rs +++ b/client/executor/src/integration_tests/linux.rs @@ -38,7 +38,13 @@ fn memory_consumption_compiled() { // For that we make a series of runtime calls, probing the RSS for the VMA matching the linear // memory. After the call we expect RSS to be equal to 0. - let runtime = mk_test_runtime(WasmExecutionMethod::Compiled, 1024); + let runtime = mk_test_runtime( + WasmExecutionMethod::Compiled { + instantiation_strategy: + sc_executor_wasmtime::InstantiationStrategy::LegacyInstanceReuse, + }, + 1024, + ); let mut instance = runtime.new_instance().unwrap(); let heap_base = instance diff --git a/client/executor/src/integration_tests/mod.rs b/client/executor/src/integration_tests/mod.rs index 75b458a399e3f..8ce0b56da2389 100644 --- a/client/executor/src/integration_tests/mod.rs +++ b/client/executor/src/integration_tests/mod.rs @@ -54,8 +54,42 @@ macro_rules! test_wasm_execution { #[test] #[cfg(feature = "wasmtime")] - fn [<$method_name _compiled>]() { - $method_name(WasmExecutionMethod::Compiled); + fn [<$method_name _compiled_recreate_instance_cow>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstanceCopyOnWrite + }); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_recreate_instance_vanilla>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstance + }); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_pooling_cow>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite + }); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_pooling_vanilla>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::Pooling + }); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_legacy_instance_reuse>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::LegacyInstanceReuse + }); } } }; @@ -88,14 +122,82 @@ macro_rules! test_wasm_execution_sandbox { #[test] #[cfg(feature = "wasmtime")] - fn [<$method_name _compiled_host_executor>]() { - $method_name(WasmExecutionMethod::Compiled, "_host"); + fn [<$method_name _compiled_pooling_cow_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite + }, "_host"); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_pooling_cow_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite + }, "_embedded"); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_pooling_vanilla_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::Pooling + }, "_host"); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_pooling_vanilla_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::Pooling + }, "_embedded"); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_recreate_instance_cow_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstanceCopyOnWrite + }, "_host"); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_recreate_instance_cow_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstanceCopyOnWrite + }, "_embedded"); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_recreate_instance_vanilla_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstance + }, "_host"); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_recreate_instance_vanilla_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstance + }, "_embedded"); + } + + #[test] + #[cfg(feature = "wasmtime")] + fn [<$method_name _compiled_legacy_instance_reuse_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::LegacyInstanceReuse + }, "_host"); } #[test] #[cfg(feature = "wasmtime")] - fn [<$method_name _compiled_embedded_executor>]() { - $method_name(WasmExecutionMethod::Compiled, "_embedded"); + fn [<$method_name _compiled_legacy_instance_reuse_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::LegacyInstanceReuse + }, "_embedded"); } } }; @@ -153,7 +255,7 @@ fn call_not_existing_function(wasm_method: WasmExecutionMethod) { let expected = match wasm_method { WasmExecutionMethod::Interpreted => "Trap: Host(Other(\"Function `missing_external` is only a stub. Calling a stub is not allowed.\"))", #[cfg(feature = "wasmtime")] - WasmExecutionMethod::Compiled => "call to a missing function env:missing_external" + WasmExecutionMethod::Compiled { .. } => "call to a missing function env:missing_external" }; assert_eq!(error.message, expected); }, @@ -173,7 +275,7 @@ fn call_yet_another_not_existing_function(wasm_method: WasmExecutionMethod) { let expected = match wasm_method { WasmExecutionMethod::Interpreted => "Trap: Host(Other(\"Function `yet_another_missing_external` is only a stub. Calling a stub is not allowed.\"))", #[cfg(feature = "wasmtime")] - WasmExecutionMethod::Compiled => "call to a missing function env:yet_another_missing_external" + WasmExecutionMethod::Compiled { .. } => "call to a missing function env:yet_another_missing_external" }; assert_eq!(error.message, expected); }, @@ -473,7 +575,9 @@ fn should_trap_when_heap_exhausted(wasm_method: WasmExecutionMethod) { match err { #[cfg(feature = "wasmtime")] - Error::AbortedDueToTrap(error) if wasm_method == WasmExecutionMethod::Compiled => { + Error::AbortedDueToTrap(error) + if matches!(wasm_method, WasmExecutionMethod::Compiled { .. }) => + { assert_eq!( error.message, r#"host code panicked while being called by the runtime: Failed to allocate memory: "Allocator ran out of space""# @@ -807,7 +911,7 @@ fn unreachable_intrinsic(wasm_method: WasmExecutionMethod) { let expected = match wasm_method { WasmExecutionMethod::Interpreted => "Trap: Unreachable", #[cfg(feature = "wasmtime")] - WasmExecutionMethod::Compiled => "wasm trap: wasm `unreachable` instruction executed", + WasmExecutionMethod::Compiled { .. } => "wasm trap: wasm `unreachable` instruction executed", }; assert_eq!(error.message, expected); }, diff --git a/client/executor/src/lib.rs b/client/executor/src/lib.rs index 5cd04b9e4ee6a..fefb84ede9105 100644 --- a/client/executor/src/lib.rs +++ b/client/executor/src/lib.rs @@ -51,6 +51,9 @@ pub use wasmi; pub use sc_executor_common::{error, sandbox}; +#[cfg(feature = "wasmtime")] +pub use sc_executor_wasmtime::InstantiationStrategy as WasmtimeInstantiationStrategy; + /// Extracts the runtime version of a given runtime code. pub trait RuntimeVersionOf { /// Extract [`RuntimeVersion`](sp_version::RuntimeVersion) of the given `runtime_code`. diff --git a/client/executor/src/wasm_runtime.rs b/client/executor/src/wasm_runtime.rs index 85c33ecacf8ff..1dee739c50f9e 100644 --- a/client/executor/src/wasm_runtime.rs +++ b/client/executor/src/wasm_runtime.rs @@ -46,7 +46,10 @@ pub enum WasmExecutionMethod { Interpreted, /// Uses the Wasmtime compiled runtime. #[cfg(feature = "wasmtime")] - Compiled, + Compiled { + /// The instantiation strategy to use. + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy, + }, } impl Default for WasmExecutionMethod { @@ -71,6 +74,9 @@ struct VersionedRuntime { module: Arc, /// Runtime version according to `Core_version` if any. version: Option, + + // TODO: Remove this once the legacy instance reuse instantiation strategy + // for `wasmtime` is gone, as this only makes sense with that particular strategy. /// Cached instance pool. instances: Arc>>>>, } @@ -310,22 +316,23 @@ where .map(|runtime| -> Arc { Arc::new(runtime) }) }, #[cfg(feature = "wasmtime")] - WasmExecutionMethod::Compiled => sc_executor_wasmtime::create_runtime::( - blob, - sc_executor_wasmtime::Config { - max_memory_size: None, - allow_missing_func_imports, - cache_path: cache_path.map(ToOwned::to_owned), - semantics: sc_executor_wasmtime::Semantics { - extra_heap_pages: heap_pages, - fast_instance_reuse: true, - deterministic_stack_limit: None, - canonicalize_nans: false, - parallel_compilation: true, + WasmExecutionMethod::Compiled { instantiation_strategy } => + sc_executor_wasmtime::create_runtime::( + blob, + sc_executor_wasmtime::Config { + allow_missing_func_imports, + cache_path: cache_path.map(ToOwned::to_owned), + semantics: sc_executor_wasmtime::Semantics { + extra_heap_pages: heap_pages, + instantiation_strategy, + deterministic_stack_limit: None, + canonicalize_nans: false, + parallel_compilation: true, + max_memory_size: None, + }, }, - }, - ) - .map(|runtime| -> Arc { Arc::new(runtime) }), + ) + .map(|runtime| -> Arc { Arc::new(runtime) }), } } diff --git a/client/executor/wasmtime/Cargo.toml b/client/executor/wasmtime/Cargo.toml index c514a93be1d88..cbe6e1a0bd101 100644 --- a/client/executor/wasmtime/Cargo.toml +++ b/client/executor/wasmtime/Cargo.toml @@ -23,6 +23,8 @@ wasmtime = { version = "0.35.3", default-features = false, features = [ "cranelift", "jitdump", "parallel-compilation", + "memory-init-cow", + "pooling-allocator", ] } sc-allocator = { version = "4.1.0-dev", path = "../../allocator" } sc-executor-common = { version = "0.10.0-dev", path = "../common" } @@ -34,3 +36,4 @@ sp-wasm-interface = { version = "6.0.0", features = ["wasmtime"], path = "../../ wat = "1.0" sc-runtime-test = { version = "2.0.0", path = "../runtime-test" } sp-io = { version = "6.0.0", path = "../../../primitives/io" } +tempfile = "3.3.0" diff --git a/client/executor/wasmtime/src/lib.rs b/client/executor/wasmtime/src/lib.rs index c54c8305f3e4b..4d0e4c37c947e 100644 --- a/client/executor/wasmtime/src/lib.rs +++ b/client/executor/wasmtime/src/lib.rs @@ -38,5 +38,5 @@ mod tests; pub use runtime::{ create_runtime, create_runtime_from_artifact, prepare_runtime_artifact, Config, - DeterministicStackLimit, Semantics, + DeterministicStackLimit, InstantiationStrategy, Semantics, }; diff --git a/client/executor/wasmtime/src/runtime.rs b/client/executor/wasmtime/src/runtime.rs index fa3b567cc0abc..dbe0b129757b7 100644 --- a/client/executor/wasmtime/src/runtime.rs +++ b/client/executor/wasmtime/src/runtime.rs @@ -80,7 +80,7 @@ impl StoreData { pub(crate) type Store = wasmtime::Store; enum Strategy { - FastInstanceReuse { + LegacyInstanceReuse { instance_wrapper: InstanceWrapper, globals_snapshot: GlobalsSnapshot, data_segments_snapshot: Arc, @@ -136,41 +136,42 @@ struct InstanceSnapshotData { pub struct WasmtimeRuntime { engine: wasmtime::Engine, instance_pre: Arc>, - snapshot_data: Option, + instantiation_strategy: InternalInstantiationStrategy, config: Config, } impl WasmModule for WasmtimeRuntime { fn new_instance(&self) -> Result> { - let strategy = if let Some(ref snapshot_data) = self.snapshot_data { - let mut instance_wrapper = InstanceWrapper::new( - &self.engine, - &self.instance_pre, - self.config.max_memory_size, - )?; - let heap_base = instance_wrapper.extract_heap_base()?; - - // This function panics if the instance was created from a runtime blob different from - // which the mutable globals were collected. Here, it is easy to see that there is only - // a single runtime blob and thus it's the same that was used for both creating the - // instance and collecting the mutable globals. - let globals_snapshot = GlobalsSnapshot::take( - &snapshot_data.mutable_globals, - &mut InstanceGlobals { instance: &mut instance_wrapper }, - ); + 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 heap_base = instance_wrapper.extract_heap_base()?; - Strategy::FastInstanceReuse { - instance_wrapper, - globals_snapshot, - data_segments_snapshot: snapshot_data.data_segments_snapshot.clone(), - heap_base, - } - } else { - Strategy::RecreateInstance(InstanceCreator { + // This function panics if the instance was created from a runtime blob different + // from which the mutable globals were collected. Here, it is easy to see that there + // is only a single runtime blob and thus it's the same that was used for both + // creating the instance and collecting the mutable globals. + let globals_snapshot = GlobalsSnapshot::take( + &snapshot_data.mutable_globals, + &mut InstanceGlobals { instance: &mut instance_wrapper }, + ); + + Strategy::LegacyInstanceReuse { + instance_wrapper, + globals_snapshot, + data_segments_snapshot: snapshot_data.data_segments_snapshot.clone(), + heap_base, + } + }, + InternalInstantiationStrategy::Builtin => Strategy::RecreateInstance(InstanceCreator { engine: self.engine.clone(), instance_pre: self.instance_pre.clone(), - max_memory_size: self.config.max_memory_size, - }) + max_memory_size: self.config.semantics.max_memory_size, + }), }; Ok(Box::new(WasmtimeInstance { strategy })) @@ -186,7 +187,7 @@ pub struct WasmtimeInstance { impl WasmInstance for WasmtimeInstance { fn call(&mut self, method: InvokeMethod, data: &[u8]) -> Result> { match &mut self.strategy { - Strategy::FastInstanceReuse { + Strategy::LegacyInstanceReuse { ref mut instance_wrapper, globals_snapshot, data_segments_snapshot, @@ -225,7 +226,7 @@ impl WasmInstance for WasmtimeInstance { fn get_global_const(&mut self, name: &str) -> Result> { match &mut self.strategy { - Strategy::FastInstanceReuse { instance_wrapper, .. } => + Strategy::LegacyInstanceReuse { instance_wrapper, .. } => instance_wrapper.get_global_val(name), Strategy::RecreateInstance(ref mut instance_creator) => instance_creator.instantiate()?.get_global_val(name), @@ -239,7 +240,7 @@ impl WasmInstance for WasmtimeInstance { // associated with it. None }, - Strategy::FastInstanceReuse { instance_wrapper, .. } => + Strategy::LegacyInstanceReuse { instance_wrapper, .. } => Some(instance_wrapper.base_ptr()), } } @@ -326,6 +327,48 @@ fn common_config(semantics: &Semantics) -> std::result::Result (true, true), + InstantiationStrategy::Pooling => (true, false), + InstantiationStrategy::RecreateInstanceCopyOnWrite => (false, true), + InstantiationStrategy::RecreateInstance => (false, false), + InstantiationStrategy::LegacyInstanceReuse => (false, false), + }; + + 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), + ); + + if use_pooling { + config.allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling { + strategy: wasmtime::PoolingAllocationStrategy::ReuseAffinity, + + // Pooling needs a bunch of hard limits to be set; if we go over + // any of these then the instantiation will fail. + instance_limits: wasmtime::InstanceLimits { + // Current minimum values for kusama (as of 2022-04-14): + // size: 32384 + // table_elements: 1249 + // memory_pages: 2070 + size: 64 * 1024, + table_elements: 2048, + memory_pages: 4096, + + // We can only have a single of those. + tables: 1, + memories: 1, + + // This determines how many instances of the module can be + // instantiated in parallel from the same `Module`. + // + // This includes nested instances spawned with `sp_tasks::spawn` + // from *within* the runtime. + count: 32, + }, + }); + } + Ok(config) } @@ -373,18 +416,47 @@ pub struct DeterministicStackLimit { pub native_stack_max: u32, } -pub struct Semantics { - /// Enabling this will lead to some optimization shenanigans that make calling [`WasmInstance`] - /// extremely fast. - /// - /// Primarily this is achieved by not recreating the instance for each call and performing a - /// bare minimum clean up: reapplying the data segments and restoring the values for global - /// variables. +/// The instantiation strategy to use for the WASM executor. +/// +/// All of the CoW strategies (with `CopyOnWrite` suffix) are only supported when either: +/// a) we're running on Linux, +/// b) we're running on an Unix-like system and we're precompiling +/// our module beforehand. +/// +/// If the CoW variant of a strategy is unsupported the executor will +/// fall back to the non-CoW equivalent. +#[non_exhaustive] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub enum InstantiationStrategy { + /// Pool the instances to avoid initializing everything from scratch + /// on each instantiation. Use copy-on-write memory when possible. /// - /// Since this feature depends on instrumentation, it can be set only if runtime is - /// instantiated using the runtime blob, e.g. using [`create_runtime`]. - // I.e. if [`CodeSupplyMode::Verbatim`] is used. - pub fast_instance_reuse: bool, + /// This is the fastest instantiation strategy. + PoolingCopyOnWrite, + + /// Recreate the instance from scratch on every instantiation. + /// Use copy-on-write memory when possible. + RecreateInstanceCopyOnWrite, + + /// Pool the instances to avoid initializing everything from scratch + /// on each instantiation. + Pooling, + + /// Recreate the instance from scratch on every instantiation. Very slow. + RecreateInstance, + + /// Legacy instance reuse mechanism. DEPRECATED. Will be removed. Do not use. + LegacyInstanceReuse, +} + +enum InternalInstantiationStrategy { + LegacyInstanceReuse(InstanceSnapshotData), + Builtin, +} + +pub struct Semantics { + /// The instantiation strategy to use. + pub instantiation_strategy: InstantiationStrategy, /// Specifying `Some` will enable deterministic stack height. That is, all executor /// invocations will reach stack overflow at the exactly same point across different wasmtime @@ -418,9 +490,7 @@ pub struct Semantics { /// 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, -} -pub struct Config { /// 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. @@ -436,7 +506,9 @@ pub struct Config { /// /// The default is `None`. pub max_memory_size: Option, +} +pub struct Config { /// The WebAssembly standard requires all imports of an instantiated module to be resolved, /// otherwise, the instantiation fails. If this option is set to `true`, then this behavior is /// overriden and imports that are requested by the module and not provided by the host @@ -452,24 +524,16 @@ pub struct Config { enum CodeSupplyMode<'a> { /// The runtime is instantiated using the given runtime blob. - Verbatim { - // Rationale to take the `RuntimeBlob` here is so that the client will be able to reuse - // the blob e.g. if they did a prevalidation. If they didn't they can pass a `RuntimeBlob` - // instance and it will be used anyway in most cases, because we are going to do at least - // some instrumentations for both anticipated paths: substrate execution and PVF execution. - // - // Should there raise a need in performing no instrumentation and the client doesn't need - // to do any checks, then we can provide a `Cow` like semantics here: if we need the blob - // and the user got `RuntimeBlob` then extract it, or otherwise create it from the given - // bytecode. - blob: RuntimeBlob, - }, + Fresh(RuntimeBlob), - /// The code is supplied in a form of a compiled artifact. + /// The runtime is instantiated using a precompiled module. /// /// This assumes that the code is already prepared for execution and the same `Config` was /// used. - Artifact { compiled_artifact: &'a [u8] }, + /// + /// We use a `Path` here instead of simply passing a byte slice to allow `wasmtime` to + /// map the runtime's linear memory on supported platforms in a copy-on-write fashion. + Precompiled(&'a Path), } /// Create a new `WasmtimeRuntime` given the code. This function performs translation from Wasm to @@ -484,29 +548,34 @@ pub fn create_runtime( where H: HostFunctions, { - // SAFETY: this is safe because it doesn't use `CodeSupplyMode::Artifact`. - unsafe { do_create_runtime::(CodeSupplyMode::Verbatim { blob }, config) } + // SAFETY: this is safe because it doesn't use `CodeSupplyMode::Precompiled`. + unsafe { do_create_runtime::(CodeSupplyMode::Fresh(blob), config) } } -/// The same as [`create_runtime`] but takes a precompiled artifact, which makes this function -/// considerably faster than [`create_runtime`]. +/// The same as [`create_runtime`] but takes a path to a precompiled artifact, +/// which makes this function considerably faster than [`create_runtime`]. /// /// # Safety /// -/// The caller must ensure that the compiled artifact passed here was produced by -/// [`prepare_runtime_artifact`]. Otherwise, there is a risk of arbitrary code execution with all -/// implications. +/// The caller must ensure that the compiled artifact passed here was: +/// 1) produced by [`prepare_runtime_artifact`], +/// 2) written to the disk as a file, +/// 3) was not modified, +/// 4) will not be modified while any runtime using this artifact is alive, or is being +/// instantiated. +/// +/// Failure to adhere to these requirements might lead to crashes and arbitrary code execution. /// -/// It is ok though if the `compiled_artifact` was created by code of another version or with +/// It is ok though if the compiled artifact was created by code of another version or with /// different configuration flags. In such case the caller will receive an `Err` deterministically. pub unsafe fn create_runtime_from_artifact( - compiled_artifact: &[u8], + compiled_artifact_path: &Path, config: Config, ) -> std::result::Result where H: HostFunctions, { - do_create_runtime::(CodeSupplyMode::Artifact { compiled_artifact }, config) + do_create_runtime::(CodeSupplyMode::Precompiled(compiled_artifact_path), config) } /// # Safety @@ -520,7 +589,6 @@ unsafe fn do_create_runtime( where H: HostFunctions, { - // Create the engine, store and finally the module from the given code. let mut wasmtime_config = common_config(&config.semantics)?; if let Some(ref cache_path) = config.cache_path { if let Err(reason) = setup_wasmtime_caching(cache_path, &mut wasmtime_config) { @@ -534,45 +602,71 @@ where let engine = Engine::new(&wasmtime_config) .map_err(|e| WasmError::Other(format!("cannot create the wasmtime engine: {}", e)))?; - let (module, snapshot_data) = match code_supply_mode { - CodeSupplyMode::Verbatim { blob } => { + let (module, instantiation_strategy) = match code_supply_mode { + CodeSupplyMode::Fresh(blob) => { let blob = prepare_blob_for_compilation(blob, &config.semantics)?; let serialized_blob = blob.clone().serialize(); let module = wasmtime::Module::new(&engine, &serialized_blob) .map_err(|e| WasmError::Other(format!("cannot create module: {}", e)))?; - if config.semantics.fast_instance_reuse { - let data_segments_snapshot = DataSegmentsSnapshot::take(&blob).map_err(|e| { - WasmError::Other(format!("cannot take data segments snapshot: {}", e)) - })?; - let data_segments_snapshot = Arc::new(data_segments_snapshot); - let mutable_globals = ExposedMutableGlobalsSet::collect(&blob); - - (module, Some(InstanceSnapshotData { data_segments_snapshot, mutable_globals })) - } else { - (module, None) + match config.semantics.instantiation_strategy { + InstantiationStrategy::LegacyInstanceReuse => { + let data_segments_snapshot = + DataSegmentsSnapshot::take(&blob).map_err(|e| { + WasmError::Other(format!("cannot take data segments snapshot: {}", e)) + })?; + let data_segments_snapshot = Arc::new(data_segments_snapshot); + let mutable_globals = ExposedMutableGlobalsSet::collect(&blob); + + ( + module, + InternalInstantiationStrategy::LegacyInstanceReuse(InstanceSnapshotData { + data_segments_snapshot, + mutable_globals, + }), + ) + }, + InstantiationStrategy::Pooling | + InstantiationStrategy::PoolingCopyOnWrite | + InstantiationStrategy::RecreateInstance | + InstantiationStrategy::RecreateInstanceCopyOnWrite => + (module, InternalInstantiationStrategy::Builtin), } }, - CodeSupplyMode::Artifact { compiled_artifact } => { - // SAFETY: The unsafity of `deserialize` is covered by this function. The + CodeSupplyMode::Precompiled(compiled_artifact_path) => { + if let InstantiationStrategy::LegacyInstanceReuse = + config.semantics.instantiation_strategy + { + return Err(WasmError::Other("the legacy instance reuse instantiation strategy is incompatible with precompiled modules".into())); + } + + // SAFETY: The unsafety of `deserialize_file` is covered by this function. The // responsibilities to maintain the invariants are passed to the caller. - let module = wasmtime::Module::deserialize(&engine, compiled_artifact) + // + // See [`create_runtime_from_artifact`] for more details. + let module = wasmtime::Module::deserialize_file(&engine, compiled_artifact_path) .map_err(|e| WasmError::Other(format!("cannot deserialize module: {}", e)))?; - (module, None) + (module, InternalInstantiationStrategy::Builtin) }, }; let mut linker = wasmtime::Linker::new(&engine); crate::imports::prepare_imports::(&mut linker, &module, config.allow_missing_func_imports)?; - let mut store = crate::instance_wrapper::create_store(module.engine(), config.max_memory_size); + let mut store = + crate::instance_wrapper::create_store(module.engine(), config.semantics.max_memory_size); let instance_pre = linker .instantiate_pre(&mut store, &module) .map_err(|e| WasmError::Other(format!("cannot preinstantiate module: {}", e)))?; - Ok(WasmtimeRuntime { engine, instance_pre: Arc::new(instance_pre), snapshot_data, config }) + Ok(WasmtimeRuntime { + engine, + instance_pre: Arc::new(instance_pre), + instantiation_strategy, + config, + }) } fn prepare_blob_for_compilation( @@ -583,16 +677,17 @@ fn prepare_blob_for_compilation( blob = blob.inject_stack_depth_metering(logical_max)?; } - // If enabled, this should happen after all other passes that may introduce global variables. - if semantics.fast_instance_reuse { + if let InstantiationStrategy::LegacyInstanceReuse = semantics.instantiation_strategy { + // When this strategy is used this must be called after all other passes which may introduce + // new global variables, otherwise they will not be reset when we call into the runtime + // again. blob.expose_mutable_globals(); } // We don't actually need the memory to be imported so we can just convert any memory // import into an export with impunity. This simplifies our code since `wasmtime` will - // now automatically take care of creating the memory for us, and it also allows us - // to potentially enable `wasmtime`'s instance pooling at a later date. (Imported - // memories are ineligible for pooling.) + // 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 diff --git a/client/executor/wasmtime/src/tests.rs b/client/executor/wasmtime/src/tests.rs index d5b92f2f24a76..51875d1eb6b50 100644 --- a/client/executor/wasmtime/src/tests.rs +++ b/client/executor/wasmtime/src/tests.rs @@ -19,18 +19,20 @@ use codec::{Decode as _, Encode as _}; use sc_executor_common::{error::Error, runtime_blob::RuntimeBlob, wasm_runtime::WasmModule}; use sc_runtime_test::wasm_binary_unwrap; -use std::sync::Arc; + +use crate::InstantiationStrategy; type HostFunctions = sp_io::SubstrateHostFunctions; struct RuntimeBuilder { code: Option, - fast_instance_reuse: bool, + instantiation_strategy: InstantiationStrategy, canonicalize_nans: bool, deterministic_stack: bool, extra_heap_pages: u64, max_memory_size: Option, precompile_runtime: bool, + tmpdir: Option, } impl RuntimeBuilder { @@ -39,41 +41,42 @@ impl RuntimeBuilder { fn new_on_demand() -> Self { Self { code: None, - fast_instance_reuse: false, + instantiation_strategy: InstantiationStrategy::RecreateInstance, canonicalize_nans: false, deterministic_stack: false, extra_heap_pages: 1024, max_memory_size: None, precompile_runtime: false, + tmpdir: None, } } - fn use_wat(&mut self, code: String) -> &mut Self { + fn use_wat(mut self, code: String) -> Self { self.code = Some(code); self } - fn canonicalize_nans(&mut self, canonicalize_nans: bool) -> &mut Self { + fn canonicalize_nans(mut self, canonicalize_nans: bool) -> Self { self.canonicalize_nans = canonicalize_nans; self } - fn deterministic_stack(&mut self, deterministic_stack: bool) -> &mut Self { + fn deterministic_stack(mut self, deterministic_stack: bool) -> Self { self.deterministic_stack = deterministic_stack; self } - fn precompile_runtime(&mut self, precompile_runtime: bool) -> &mut Self { + fn precompile_runtime(mut self, precompile_runtime: bool) -> Self { self.precompile_runtime = precompile_runtime; self } - fn max_memory_size(&mut self, max_memory_size: Option) -> &mut Self { + fn max_memory_size(mut self, max_memory_size: Option) -> Self { self.max_memory_size = max_memory_size; self } - fn build(&mut self) -> Arc { + fn build<'a>(&'a mut self) -> impl WasmModule + 'a { let blob = { let wasm: Vec; @@ -90,11 +93,10 @@ impl RuntimeBuilder { }; let config = crate::Config { - max_memory_size: self.max_memory_size, allow_missing_func_imports: true, cache_path: None, semantics: crate::Semantics { - fast_instance_reuse: self.fast_instance_reuse, + instantiation_strategy: self.instantiation_strategy, deterministic_stack_limit: match self.deterministic_stack { true => Some(crate::DeterministicStackLimit { logical_max: 65536, @@ -105,24 +107,31 @@ impl RuntimeBuilder { canonicalize_nans: self.canonicalize_nans, parallel_compilation: true, extra_heap_pages: self.extra_heap_pages, + max_memory_size: self.max_memory_size, }, }; - let rt = if self.precompile_runtime { + if self.precompile_runtime { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("runtime.bin"); + + // Delay the removal of the temporary directory until we're dropped. + self.tmpdir = Some(dir); + let artifact = crate::prepare_runtime_artifact(blob, &config.semantics).unwrap(); - unsafe { crate::create_runtime_from_artifact::(&artifact, config) } + std::fs::write(&path, artifact).unwrap(); + unsafe { crate::create_runtime_from_artifact::(&path, config) } } else { crate::create_runtime::(blob, config) } - .expect("cannot create runtime"); - - Arc::new(rt) as Arc + .expect("cannot create runtime") } } #[test] fn test_nan_canonicalization() { - let runtime = RuntimeBuilder::new_on_demand().canonicalize_nans(true).build(); + let mut builder = RuntimeBuilder::new_on_demand().canonicalize_nans(true); + let runtime = builder.build(); let mut instance = runtime.new_instance().expect("failed to instantiate a runtime"); @@ -161,10 +170,11 @@ fn test_nan_canonicalization() { fn test_stack_depth_reaching() { const TEST_GUARD_PAGE_SKIP: &str = include_str!("test-guard-page-skip.wat"); - let runtime = RuntimeBuilder::new_on_demand() + let mut builder = RuntimeBuilder::new_on_demand() .use_wat(TEST_GUARD_PAGE_SKIP.to_string()) - .deterministic_stack(true) - .build(); + .deterministic_stack(true); + + let runtime = builder.build(); let mut instance = runtime.new_instance().expect("failed to instantiate a runtime"); match instance.call_export("test-many-locals", &[]).unwrap_err() { @@ -202,11 +212,12 @@ fn test_max_memory_pages(import_memory: bool, precompile_runtime: bool) { wat: String, precompile_runtime: bool, ) -> Result<(), Box> { - let runtime = RuntimeBuilder::new_on_demand() + let mut builder = RuntimeBuilder::new_on_demand() .use_wat(wat) .max_memory_size(max_memory_size) - .precompile_runtime(precompile_runtime) - .build(); + .precompile_runtime(precompile_runtime); + + let runtime = builder.build(); let mut instance = runtime.new_instance()?; let _ = instance.call_export("main", &[])?; Ok(()) @@ -377,15 +388,15 @@ fn test_instances_without_reuse_are_not_leaked() { let runtime = crate::create_runtime::( RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(), crate::Config { - max_memory_size: None, allow_missing_func_imports: true, cache_path: None, semantics: crate::Semantics { - fast_instance_reuse: false, + instantiation_strategy: InstantiationStrategy::RecreateInstance, deterministic_stack_limit: None, canonicalize_nans: false, parallel_compilation: true, extra_heap_pages: 2048, + max_memory_size: None, }, }, ) diff --git a/client/service/src/config.rs b/client/service/src/config.rs index 35380da11fc71..c895300fea4d1 100644 --- a/client/service/src/config.rs +++ b/client/service/src/config.rs @@ -21,6 +21,8 @@ pub use sc_client_api::execution_extensions::{ExecutionStrategies, ExecutionStrategy}; pub use sc_client_db::{Database, DatabaseSource, KeepBlocks, PruningMode}; pub use sc_executor::WasmExecutionMethod; +#[cfg(feature = "wasmtime")] +pub use sc_executor::WasmtimeInstantiationStrategy; pub use sc_network::{ config::{ MultiaddrWithPeerId, NetworkConfiguration, NodeKeyConfig, NonDefaultSetConfig, Role, diff --git a/utils/frame/benchmarking-cli/src/pallet/command.rs b/utils/frame/benchmarking-cli/src/pallet/command.rs index 660f31b8f1529..aa95223619b45 100644 --- a/utils/frame/benchmarking-cli/src/pallet/command.rs +++ b/utils/frame/benchmarking-cli/src/pallet/command.rs @@ -23,7 +23,9 @@ use frame_benchmarking::{ }; use frame_support::traits::StorageInfo; use linked_hash_map::LinkedHashMap; -use sc_cli::{CliConfiguration, ExecutionStrategy, Result, SharedParams}; +use sc_cli::{ + execution_method_from_cli, CliConfiguration, ExecutionStrategy, Result, SharedParams, +}; use sc_client_db::BenchmarkingState; use sc_executor::NativeElseWasmExecutor; use sc_service::{Configuration, NativeExecutionDispatch}; @@ -121,7 +123,6 @@ impl PalletCmd { } let spec = config.chain_spec; - let wasm_method = self.wasm_method.into(); let strategy = self.execution.unwrap_or(ExecutionStrategy::Native); let pallet = self.pallet.clone().unwrap_or_default(); let pallet = pallet.as_bytes(); @@ -141,7 +142,7 @@ impl PalletCmd { let state_without_tracking = BenchmarkingState::::new(genesis_storage, cache_size, self.record_proof, false)?; let executor = NativeElseWasmExecutor::::new( - wasm_method, + execution_method_from_cli(self.wasm_method, self.wasmtime_instantiation_strategy), self.heap_pages, 2, // The runtime instances cache size. 2, // The runtime cache size diff --git a/utils/frame/benchmarking-cli/src/pallet/mod.rs b/utils/frame/benchmarking-cli/src/pallet/mod.rs index 48ddcc7ce8eec..227c9b2f8a7b6 100644 --- a/utils/frame/benchmarking-cli/src/pallet/mod.rs +++ b/utils/frame/benchmarking-cli/src/pallet/mod.rs @@ -18,7 +18,10 @@ mod command; mod writer; -use sc_cli::{ExecutionStrategy, WasmExecutionMethod, DEFAULT_WASM_EXECUTION_METHOD}; +use sc_cli::{ + ExecutionStrategy, WasmExecutionMethod, WasmtimeInstantiationStrategy, + DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, DEFAULT_WASM_EXECUTION_METHOD, +}; use std::{fmt::Debug, path::PathBuf}; // Add a more relaxed parsing for pallet names by allowing pallet directory names with `-` to be @@ -131,6 +134,17 @@ pub struct PalletCmd { )] pub wasm_method: WasmExecutionMethod, + /// The WASM instantiation method to use. + /// + /// Only has an effect when `wasm-execution` is set to `compiled`. + #[clap( + long = "wasm-instantiation-strategy", + value_name = "STRATEGY", + default_value_t = DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, + arg_enum, + )] + pub wasmtime_instantiation_strategy: WasmtimeInstantiationStrategy, + /// Limit the memory the database cache can use. #[clap(long = "db-cache", value_name = "MiB", default_value = "1024")] pub database_cache_size: u32, diff --git a/utils/frame/try-runtime/cli/src/lib.rs b/utils/frame/try-runtime/cli/src/lib.rs index c13bbb3626176..71d258a68982e 100644 --- a/utils/frame/try-runtime/cli/src/lib.rs +++ b/utils/frame/try-runtime/cli/src/lib.rs @@ -271,7 +271,9 @@ use remote_externalities::{ }; use sc_chain_spec::ChainSpec; use sc_cli::{ - CliConfiguration, ExecutionStrategy, WasmExecutionMethod, DEFAULT_WASM_EXECUTION_METHOD, + execution_method_from_cli, CliConfiguration, ExecutionStrategy, WasmExecutionMethod, + WasmtimeInstantiationStrategy, DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, + DEFAULT_WASM_EXECUTION_METHOD, }; use sc_executor::NativeElseWasmExecutor; use sc_service::{Configuration, NativeExecutionDispatch}; @@ -400,6 +402,17 @@ pub struct SharedParams { )] pub wasm_method: WasmExecutionMethod, + /// The WASM instantiation method to use. + /// + /// Only has an effect when `wasm-execution` is set to `compiled`. + #[clap( + long = "wasm-instantiation-strategy", + value_name = "STRATEGY", + default_value_t = DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, + arg_enum, + )] + pub wasmtime_instantiation_strategy: WasmtimeInstantiationStrategy, + /// The number of 64KB pages to allocate for Wasm execution. Defaults to /// [`sc_service::Configuration.default_heap_pages`]. #[clap(long)] @@ -675,13 +688,12 @@ pub(crate) fn build_executor( shared: &SharedParams, config: &sc_service::Configuration, ) -> NativeElseWasmExecutor { - let wasm_method = shared.wasm_method; let heap_pages = shared.heap_pages.or(config.default_heap_pages); let max_runtime_instances = config.max_runtime_instances; let runtime_cache_size = config.runtime_cache_size; NativeElseWasmExecutor::::new( - wasm_method.into(), + execution_method_from_cli(shared.wasm_method, shared.wasmtime_instantiation_strategy), heap_pages, max_runtime_instances, runtime_cache_size,