|
|
@ -4,8 +4,12 @@ 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_engine(); |
|
|
|
let (engine, module) = test_setup(); |
|
|
|
b.iter_custom(move |iters| { |
|
|
|
(0..iters) |
|
|
|
.into_iter() |
|
|
@ -14,8 +18,10 @@ fn measure_execution_time(c: &mut Criterion) { |
|
|
|
}) |
|
|
|
}); |
|
|
|
|
|
|
|
// 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_engine(); |
|
|
|
let (engine, module) = test_setup(); |
|
|
|
b.iter_custom(move |iters| { |
|
|
|
(0..iters) |
|
|
|
.into_iter() |
|
|
@ -26,8 +32,11 @@ fn measure_execution_time(c: &mut Criterion) { |
|
|
|
.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_engine(); |
|
|
|
let (engine, module) = test_setup(); |
|
|
|
b.iter_custom(move |iters| { |
|
|
|
(0..iters) |
|
|
|
.into_iter() |
|
|
@ -40,60 +49,66 @@ fn measure_execution_time(c: &mut Criterion) { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
fn test_engine() -> (Engine, Module) { |
|
|
|
let pool_count = 1000; |
|
|
|
|
|
|
|
let mut config = Config::new(); |
|
|
|
config.allocation_strategy(InstanceAllocationStrategy::Pooling { |
|
|
|
strategy: PoolingAllocationStrategy::NextAvailable, |
|
|
|
module_limits: ModuleLimits { |
|
|
|
memory_pages: 1, |
|
|
|
..Default::default() |
|
|
|
}, |
|
|
|
instance_limits: InstanceLimits { |
|
|
|
count: pool_count, |
|
|
|
memory_reservation_size: 1, |
|
|
|
}, |
|
|
|
}); |
|
|
|
|
|
|
|
let engine = Engine::new(&config).unwrap(); |
|
|
|
let module = Module::new(&engine, r#"(module (memory 1) (func (export "f")))"#).unwrap(); |
|
|
|
(engine, module) |
|
|
|
} |
|
|
|
|
|
|
|
fn lazy_thread_instantiate(engine: Engine, module: Module) -> Duration { |
|
|
|
thread::spawn(move || { |
|
|
|
let mut store = Store::new(&engine, ()); |
|
|
|
let inst = Instance::new(&mut store, &module, &[]).expect("instantiate"); |
|
|
|
/// 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(); |
|
|
|
|
|
|
|
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"); |
|
|
|
(init_duration, call.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() |
|
|
|
}, |
|
|
|
instance_limits: InstanceLimits { |
|
|
|
count: pool_count, |
|
|
|
memory_reservation_size: 1, |
|
|
|
}, |
|
|
|
}); |
|
|
|
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); |
|
|
|