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