1 //! A C API for benchmarking Wasmtime's WebAssembly compilation, instantiation, 2 //! and execution. 3 //! 4 //! The API expects sequential calls to: 5 //! 6 //! - `wasm_bench_create` 7 //! - `wasm_bench_compile` 8 //! - `wasm_bench_instantiate` 9 //! - `wasm_bench_execute` 10 //! - `wasm_bench_free` 11 //! 12 //! You may repeat this sequence of calls multiple times to take multiple 13 //! measurements of compilation, instantiation, and execution time within a 14 //! single process. 15 //! 16 //! All API calls must happen on the same thread. 17 //! 18 //! Functions which return pointers use null as an error value. Function which 19 //! return `int` use `0` as OK and non-zero as an error value. 20 //! 21 //! # Example 22 //! 23 //! ``` 24 //! use std::ptr; 25 //! use wasmtime_bench_api::*; 26 //! 27 //! let working_dir = std::env::current_dir().unwrap().display().to_string(); 28 //! let mut bench_api = ptr::null_mut(); 29 //! unsafe { 30 //! let code = wasm_bench_create(working_dir.as_ptr(), working_dir.len(), &mut bench_api); 31 //! assert_eq!(code, OK); 32 //! assert!(!bench_api.is_null()); 33 //! }; 34 //! 35 //! let wasm = wat::parse_bytes(br#" 36 //! (module 37 //! (func $bench_start (import "bench" "start")) 38 //! (func $bench_end (import "bench" "end")) 39 //! (func $start (export "_start") 40 //! call $bench_start 41 //! i32.const 1 42 //! i32.const 2 43 //! i32.add 44 //! drop 45 //! call $bench_end 46 //! ) 47 //! ) 48 //! "#).unwrap(); 49 //! 50 //! // Start your compilation timer here. 51 //! let code = unsafe { wasm_bench_compile(bench_api, wasm.as_ptr(), wasm.len()) }; 52 //! // End your compilation timer here. 53 //! assert_eq!(code, OK); 54 //! 55 //! // The Wasm benchmark will expect us to provide functions to start ("bench" 56 //! // "start") and stop ("bench" "stop") the measurement counters/timers during 57 //! // execution. 58 //! extern "C" fn bench_start() { 59 //! // Start your execution timer here. 60 //! } 61 //! extern "C" fn bench_stop() { 62 //! // End your execution timer here. 63 //! } 64 //! 65 //! // Start your instantiation timer here. 66 //! let code = unsafe { wasm_bench_instantiate(bench_api, bench_start, bench_stop) }; 67 //! // End your instantiation timer here. 68 //! assert_eq!(code, OK); 69 //! 70 //! // No need to start timers for the execution since, by convention, the timer 71 //! // functions we passed during instantiation will be called by the benchmark 72 //! // at the appropriate time (before and after the benchmarked section). 73 //! let code = unsafe { wasm_bench_execute(bench_api) }; 74 //! assert_eq!(code, OK); 75 //! 76 //! unsafe { 77 //! wasm_bench_free(bench_api); 78 //! } 79 //! ``` 80 81 use anyhow::{anyhow, Context, Result}; 82 use std::env; 83 use std::os::raw::{c_int, c_void}; 84 use std::path::Path; 85 use std::slice; 86 use wasmtime::{Config, Engine, Instance, Linker, Module, Store}; 87 use wasmtime_wasi::sync::{Wasi, WasiCtxBuilder}; 88 89 pub type ExitCode = c_int; 90 pub const OK: ExitCode = 0; 91 pub const ERR: ExitCode = -1; 92 93 // Randomize the location of heap objects to avoid accidental locality being an 94 // uncontrolled variable that obscures performance evaluation in our 95 // experiments. 96 #[cfg(feature = "shuffling-allocator")] 97 #[global_allocator] 98 static ALLOC: shuffling_allocator::ShufflingAllocator<std::alloc::System> = 99 shuffling_allocator::wrap!(&std::alloc::System); 100 101 /// Exposes a C-compatible way of creating the engine from the bytes of a single 102 /// Wasm module. 103 /// 104 /// On success, the `out_bench_ptr` is initialized to a pointer to a structure 105 /// that contains the engine's initialized state, and `0` is returned. On 106 /// failure, a non-zero status code is returned and `out_bench_ptr` is left 107 /// untouched. 108 #[no_mangle] 109 pub extern "C" fn wasm_bench_create( 110 working_dir_ptr: *const u8, 111 working_dir_len: usize, 112 out_bench_ptr: *mut *mut c_void, 113 ) -> ExitCode { 114 let result = (|| -> Result<_> { 115 let working_dir = unsafe { std::slice::from_raw_parts(working_dir_ptr, working_dir_len) }; 116 let working_dir = std::str::from_utf8(working_dir) 117 .context("given working directory is not valid UTF-8")?; 118 let state = Box::new(BenchState::new(working_dir)?); 119 Ok(Box::into_raw(state) as _) 120 })(); 121 122 if let Ok(bench_ptr) = result { 123 unsafe { 124 assert!(!out_bench_ptr.is_null()); 125 *out_bench_ptr = bench_ptr; 126 } 127 } 128 129 to_exit_code(result.map(|_| ())) 130 } 131 132 /// Free the engine state allocated by this library. 133 #[no_mangle] 134 pub extern "C" fn wasm_bench_free(state: *mut c_void) { 135 assert!(!state.is_null()); 136 unsafe { 137 Box::from_raw(state as *mut BenchState); 138 } 139 } 140 141 /// Compile the Wasm benchmark module. 142 #[no_mangle] 143 pub extern "C" fn wasm_bench_compile( 144 state: *mut c_void, 145 wasm_bytes: *const u8, 146 wasm_bytes_length: usize, 147 ) -> ExitCode { 148 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; 149 let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) }; 150 let result = state.compile(wasm_bytes).context("failed to compile"); 151 to_exit_code(result) 152 } 153 154 /// Instantiate the Wasm benchmark module. 155 #[no_mangle] 156 pub extern "C" fn wasm_bench_instantiate( 157 state: *mut c_void, 158 bench_start: extern "C" fn(), 159 bench_end: extern "C" fn(), 160 ) -> ExitCode { 161 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; 162 let result = state 163 .instantiate(bench_start, bench_end) 164 .context("failed to instantiate"); 165 to_exit_code(result) 166 } 167 168 /// Execute the Wasm benchmark module. 169 #[no_mangle] 170 pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode { 171 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; 172 let result = state.execute().context("failed to execute"); 173 to_exit_code(result) 174 } 175 176 /// Helper function for converting a Rust result to a C error code. 177 /// 178 /// This will print an error indicating some information regarding the failure. 179 fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode { 180 match result.into() { 181 Ok(_) => OK, 182 Err(error) => { 183 eprintln!("{:?}", error); 184 ERR 185 } 186 } 187 } 188 189 /// This structure contains the actual Rust implementation of the state required 190 /// to manage the Wasmtime engine between calls. 191 struct BenchState { 192 engine: Engine, 193 linker: Linker, 194 module: Option<Module>, 195 instance: Option<Instance>, 196 did_execute: bool, 197 } 198 199 impl BenchState { 200 fn new(working_dir: impl AsRef<Path>) -> Result<Self> { 201 let mut config = Config::new(); 202 config.wasm_simd(true); 203 // NB: do not configure a code cache. 204 205 let engine = Engine::new(&config)?; 206 let store = Store::new(&engine); 207 208 let mut linker = Linker::new(&store); 209 210 // Create a WASI environment. 211 212 let mut cx = WasiCtxBuilder::new(); 213 cx = cx.inherit_stdio(); 214 // Allow access to the working directory so that the benchmark can read 215 // its input workload(s). 216 let working_dir = unsafe { cap_std::fs::Dir::open_ambient_dir(working_dir) } 217 .context("failed to preopen the working directory")?; 218 cx = cx.preopened_dir(working_dir, ".")?; 219 // Pass this env var along so that the benchmark program can use smaller 220 // input workload(s) if it has them and that has been requested. 221 if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") { 222 cx = cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val)?; 223 } 224 225 Wasi::new(linker.store(), cx.build()?).add_to_linker(&mut linker)?; 226 227 #[cfg(feature = "wasi-nn")] 228 { 229 use std::cell::RefCell; 230 use std::rc::Rc; 231 use wasmtime_wasi_nn::{WasiNn, WasiNnCtx}; 232 233 let wasi_nn = WasiNn::new(linker.store(), Rc::new(RefCell::new(WasiNnCtx::new()?))); 234 wasi_nn.add_to_linker(&mut linker)?; 235 } 236 237 #[cfg(feature = "wasi-crypto")] 238 { 239 use std::cell::RefCell; 240 use std::rc::Rc; 241 use wasmtime_wasi_crypto::{ 242 WasiCryptoAsymmetricCommon, WasiCryptoCommon, WasiCryptoCtx, WasiCryptoSignatures, 243 WasiCryptoSymmetric, 244 }; 245 246 let cx_crypto = Rc::new(RefCell::new(WasiCryptoCtx::new())); 247 WasiCryptoCommon::new(linker.store(), cx_crypto.clone()).add_to_linker(linker)?; 248 WasiCryptoAsymmetricCommon::new(linker.store(), cx_crypto.clone()) 249 .add_to_linker(linker)?; 250 WasiCryptoSignatures::new(linker.store(), cx_crypto.clone()).add_to_linker(linker)?; 251 WasiCryptoSymmetric::new(linker.store(), cx_crypto).add_to_linker(linker)?; 252 } 253 254 Ok(Self { 255 engine, 256 linker, 257 module: None, 258 instance: None, 259 did_execute: false, 260 }) 261 } 262 263 fn compile(&mut self, bytes: &[u8]) -> Result<()> { 264 assert!( 265 self.module.is_none(), 266 "create a new engine to repeat compilation" 267 ); 268 self.module = Some(Module::from_binary(&self.engine, bytes)?); 269 Ok(()) 270 } 271 272 fn instantiate( 273 &mut self, 274 bench_start: extern "C" fn(), 275 bench_end: extern "C" fn(), 276 ) -> Result<()> { 277 assert!( 278 self.instance.is_none(), 279 "create a new engine to repeat instantiation" 280 ); 281 let module = self 282 .module 283 .as_mut() 284 .expect("compile the module before instantiating it"); 285 286 // Import the specialized benchmarking functions. 287 self.linker.func("bench", "start", move || bench_start())?; 288 self.linker.func("bench", "end", move || bench_end())?; 289 290 self.instance = Some(self.linker.instantiate(&module)?); 291 Ok(()) 292 } 293 294 fn execute(&mut self) -> Result<()> { 295 assert!(!self.did_execute, "create a new engine to repeat execution"); 296 self.did_execute = true; 297 298 let instance = self 299 .instance 300 .as_ref() 301 .expect("instantiate the module before executing it"); 302 303 let start_func = instance.get_typed_func::<(), ()>("_start")?; 304 match start_func.call(()) { 305 Ok(_) => Ok(()), 306 Err(trap) => { 307 // Since _start will likely return by using the system `exit` call, we must 308 // check the trap code to see if it actually represents a successful exit. 309 match trap.i32_exit_status() { 310 Some(0) => Ok(()), 311 Some(n) => Err(anyhow!("_start exited with a non-zero code: {}", n)), 312 None => Err(anyhow!( 313 "executing the benchmark resulted in a trap: {}", 314 trap 315 )), 316 } 317 } 318 } 319 } 320 } 321