|
|
|
use criterion::{criterion_group, criterion_main, Criterion};
|
|
|
|
use std::thread;
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
use wasmtime::*;
|
|
|
|
|
|
|
|
fn measure_execution_time(c: &mut Criterion) {
|
|
|
|
// Baseline performance: a single measurment covers both initializing
|
|
|
|
// thread local resources and executing the first call.
|
|
|
|
//
|
|
|
|
// The other two bench functions should sum to this duration.
|
|
|
|
c.bench_function("lazy initialization at call", move |b| {
|
|
|
|
let (engine, module) = test_setup();
|
|
|
|
b.iter_custom(move |iters| {
|
|
|
|
(0..iters)
|
|
|
|
.into_iter()
|
|
|
|
.map(|_| lazy_thread_instantiate(engine.clone(), module.clone()))
|
|
|
|
.sum()
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
// Using Engine::tls_eager_initialize: measure how long eager
|
|
|
|
// initialization takes on a new thread.
|
|
|
|
c.bench_function("eager initialization", move |b| {
|
|
|
|
let (engine, module) = test_setup();
|
|
|
|
b.iter_custom(move |iters| {
|
|
|
|
(0..iters)
|
|
|
|
.into_iter()
|
|
|
|
.map(|_| {
|
|
|
|
let (init, _call) = eager_thread_instantiate(engine.clone(), module.clone());
|
|
|
|
init
|
|
|
|
})
|
|
|
|
.sum()
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
// Measure how long the first call takes on a thread after it has been
|
|
|
|
// eagerly initialized.
|
|
|
|
c.bench_function("call after eager initialization", move |b| {
|
|
|
|
let (engine, module) = test_setup();
|
|
|
|
b.iter_custom(move |iters| {
|
|
|
|
(0..iters)
|
|
|
|
.into_iter()
|
|
|
|
.map(|_| {
|
|
|
|
let (_init, call) = eager_thread_instantiate(engine.clone(), module.clone());
|
|
|
|
call
|
|
|
|
})
|
|
|
|
.sum()
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Creating a store and measuring the time to perform a call is the same behavior
|
|
|
|
/// in both setups.
|
|
|
|
fn duration_of_call(engine: &Engine, module: &Module) -> Duration {
|
|
|
|
let mut store = Store::new(engine, ());
|
|
|
|
let inst = Instance::new(&mut store, module, &[]).expect("instantiate");
|
|
|
|
let f = inst.get_func(&mut store, "f").expect("get f");
|
|
|
|
let f = f.typed::<(), (), _>(&store).expect("type f");
|
|
|
|
|
|
|
|
let call = Instant::now();
|
|
|
|
f.call(&mut store, ()).expect("call f");
|
|
|
|
call.elapsed()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// When wasmtime first runs a function on a thread, it needs to initialize
|
|
|
|
/// some thread-local resources and install signal handlers. This benchmark
|
|
|
|
/// spawns a new thread, and returns the duration it took to execute the first
|
|
|
|
/// function call made on that thread.
|
|
|
|
fn lazy_thread_instantiate(engine: Engine, module: Module) -> Duration {
|
|
|
|
thread::spawn(move || duration_of_call(&engine, &module))
|
|
|
|
.join()
|
|
|
|
.expect("thread joins")
|
|
|
|
}
|
|
|
|
/// This benchmark spawns a new thread, and records the duration to eagerly
|
|
|
|
/// initializes the thread local resources. It then creates a store and
|
|
|
|
/// instance, and records the duration it took to execute the first function
|
|
|
|
/// call.
|
|
|
|
fn eager_thread_instantiate(engine: Engine, module: Module) -> (Duration, Duration) {
|
|
|
|
thread::spawn(move || {
|
|
|
|
let init_start = Instant::now();
|
|
|
|
Engine::tls_eager_initialize().expect("eager init");
|
|
|
|
let init_duration = init_start.elapsed();
|
|
|
|
|
|
|
|
(init_duration, duration_of_call(&engine, &module))
|
|
|
|
})
|
|
|
|
.join()
|
|
|
|
.expect("thread joins")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn test_setup() -> (Engine, Module) {
|
|
|
|
// We only expect to create one Instance at a time, with a single memory.
|
|
|
|
let pool_count = 10;
|
|
|
|
|
|
|
|
let mut config = Config::new();
|
|
|
|
config.allocation_strategy(InstanceAllocationStrategy::Pooling {
|
|
|
|
strategy: PoolingAllocationStrategy::NextAvailable,
|
|
|
|
module_limits: ModuleLimits {
|
|
|
|
memory_pages: 1,
|
|
|
|
..Default::default()
|
|
|
|
},
|
Add guard pages to the front of linear memories (#2977)
* Add guard pages to the front of linear memories
This commit implements a safety feature for Wasmtime to place guard
pages before the allocation of all linear memories. Guard pages placed
after linear memories are typically present for performance (at least)
because it can help elide bounds checks. Guard pages before a linear
memory, however, are never strictly needed for performance or features.
The intention of a preceding guard page is to help insulate against bugs
in Cranelift or other code generators, such as CVE-2021-32629.
This commit adds a `Config::guard_before_linear_memory` configuration
option, defaulting to `true`, which indicates whether guard pages should
be present both before linear memories as well as afterwards. Guard
regions continue to be controlled by
`{static,dynamic}_memory_guard_size` methods.
The implementation here affects both on-demand allocated memories as
well as the pooling allocator for memories. For on-demand memories this
adjusts the size of the allocation as well as adjusts the calculations
for the base pointer of the wasm memory. For the pooling allocator this
will place a singular extra guard region at the very start of the
allocation for memories. Since linear memories in the pooling allocator
are contiguous every memory already had a preceding guard region in
memory, it was just the previous memory's guard region afterwards. Only
the first memory needed this extra guard.
I've attempted to write some tests to help test all this, but this is
all somewhat tricky to test because the settings are pretty far away
from the actual behavior. I think, though, that the tests added here
should help cover various use cases and help us have confidence in
tweaking the various `Config` settings beyond their defaults.
Note that this also contains a semantic change where
`InstanceLimits::memory_reservation_size` has been removed. Instead this
field is now inferred from the `static_memory_maximum_size` and guard
size settings. This should hopefully remove some duplication in these
settings, canonicalizing on the guard-size/static-size settings as the
way to control memory sizes and virtual reservations.
* Update config docs
* Fix a typo
* Fix benchmark
* Fix wasmtime-runtime tests
* Fix some more tests
* Try to fix uffd failing test
* Review items
* Tweak 32-bit defaults
Makes the pooling allocator a bit more reasonable by default on 32-bit
with these settings.
3 years ago
|
|
|
instance_limits: InstanceLimits { count: pool_count },
|
|
|
|
});
|
|
|
|
let engine = Engine::new(&config).unwrap();
|
|
|
|
|
|
|
|
// The module has a memory (shouldn't matter) and a single function which is a no-op.
|
|
|
|
let module = Module::new(&engine, r#"(module (memory 1) (func (export "f")))"#).unwrap();
|
|
|
|
(engine, module)
|
|
|
|
}
|
|
|
|
|
|
|
|
criterion_group!(benches, measure_execution_time);
|
|
|
|
criterion_main!(benches);
|