1 use criterion::{Criterion, criterion_group, criterion_main};
2 use std::thread;
3 use std::time::{Duration, Instant};
4 use wasmtime::*;
5
measure_execution_time(c: &mut Criterion)6 fn measure_execution_time(c: &mut Criterion) {
7 // Baseline performance: a single measurement covers both initializing
8 // thread local resources and executing the first call.
9 //
10 // The other two bench functions should sum to this duration.
11 c.bench_function("lazy initialization at call", move |b| {
12 let (engine, module) = test_setup();
13 b.iter_custom(move |iters| {
14 (0..iters)
15 .map(|_| lazy_thread_instantiate(engine.clone(), module.clone()))
16 .sum()
17 })
18 });
19
20 // Using Engine::tls_eager_initialize: measure how long eager
21 // initialization takes on a new thread.
22 c.bench_function("eager initialization", move |b| {
23 let (engine, module) = test_setup();
24 b.iter_custom(move |iters| {
25 (0..iters)
26 .map(|_| {
27 let (init, _call) = eager_thread_instantiate(engine.clone(), module.clone());
28 init
29 })
30 .sum()
31 })
32 });
33
34 // Measure how long the first call takes on a thread after it has been
35 // eagerly initialized.
36 c.bench_function("call after eager initialization", move |b| {
37 let (engine, module) = test_setup();
38 b.iter_custom(move |iters| {
39 (0..iters)
40 .map(|_| {
41 let (_init, call) = eager_thread_instantiate(engine.clone(), module.clone());
42 call
43 })
44 .sum()
45 })
46 });
47 }
48
49 /// Creating a store and measuring the time to perform a call is the same behavior
50 /// in both setups.
duration_of_call(engine: &Engine, module: &Module) -> Duration51 fn duration_of_call(engine: &Engine, module: &Module) -> Duration {
52 let mut store = Store::new(engine, ());
53 let inst = Instance::new(&mut store, module, &[]).expect("instantiate");
54 let f = inst.get_func(&mut store, "f").expect("get f");
55 let f = f.typed::<(), ()>(&store).expect("type f");
56
57 let call = Instant::now();
58 f.call(&mut store, ()).expect("call f");
59 call.elapsed()
60 }
61
62 /// When wasmtime first runs a function on a thread, it needs to initialize
63 /// some thread-local resources and install signal handlers. This benchmark
64 /// spawns a new thread, and returns the duration it took to execute the first
65 /// function call made on that thread.
lazy_thread_instantiate(engine: Engine, module: Module) -> Duration66 fn lazy_thread_instantiate(engine: Engine, module: Module) -> Duration {
67 thread::spawn(move || duration_of_call(&engine, &module))
68 .join()
69 .expect("thread joins")
70 }
71 /// This benchmark spawns a new thread, and records the duration to eagerly
72 /// initializes the thread local resources. It then creates a store and
73 /// instance, and records the duration it took to execute the first function
74 /// call.
eager_thread_instantiate(engine: Engine, module: Module) -> (Duration, Duration)75 fn eager_thread_instantiate(engine: Engine, module: Module) -> (Duration, Duration) {
76 thread::spawn(move || {
77 let init_start = Instant::now();
78 Engine::tls_eager_initialize();
79 let init_duration = init_start.elapsed();
80
81 (init_duration, duration_of_call(&engine, &module))
82 })
83 .join()
84 .expect("thread joins")
85 }
86
test_setup() -> (Engine, Module)87 fn test_setup() -> (Engine, Module) {
88 // We only expect to create one Instance at a time, with a single memory.
89 let pool_count = 10;
90
91 let mut pool = PoolingAllocationConfig::default();
92 pool.total_memories(pool_count)
93 .total_stacks(pool_count)
94 .total_tables(pool_count);
95 let mut config = Config::new();
96 config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool));
97 let engine = Engine::new(&config).unwrap();
98
99 // The module has a memory (shouldn't matter) and a single function which is a no-op.
100 let module = Module::new(&engine, r#"(module (memory 1) (func (export "f")))"#).unwrap();
101 (engine, module)
102 }
103
104 criterion_group!(benches, measure_execution_time);
105 criterion_main!(benches);
106