From cc81570a05a766a0c2fff6557ef36220268aab4e Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Tue, 15 Dec 2020 11:19:30 -0800 Subject: [PATCH] bench-api: Clean up the benchmarking API Mostly just tweaks to docs/naming/readability/tidying up. The biggest thing is that the wasm bytes are passed in during compilation now, rather than on initialization, which lets us remove the lifetime from our state struct and makes wrangling unsafe conversions that much easier. --- crates/bench-api/src/lib.rs | 290 +++++++++++++++++++++--------------- 1 file changed, 170 insertions(+), 120 deletions(-) diff --git a/crates/bench-api/src/lib.rs b/crates/bench-api/src/lib.rs index a12268e6de..a2958f09a8 100644 --- a/crates/bench-api/src/lib.rs +++ b/crates/bench-api/src/lib.rs @@ -1,145 +1,189 @@ -//! Expose a C-compatible API for controlling the Wasmtime engine during benchmarking. The API expects very sequential -//! use: -//! - `engine_create` -//! - `engine_compile_module` -//! - `engine_instantiate_module` -//! - `engine_execute_module` -//! - `engine_free` +//! A C API for benchmarking Wasmtime's WebAssembly compilation, instantiation, +//! and execution. //! -//! An example of this C-style usage, without error checking, is shown below: +//! The API expects sequential calls to: +//! +//! - `wasm_bench_create` +//! - `wasm_bench_compile` +//! - `wasm_bench_instantiate` +//! - `wasm_bench_execute` +//! - `wasm_bench_free` +//! +//! You may repeat this sequence of calls multiple times to take multiple +//! measurements of compilation, instantiation, and execution time within a +//! single process. +//! +//! All API calls must happen on the same thread. +//! +//! Functions which return pointers use null as an error value. Function which +//! return `int` use `0` as OK and non-zero as an error value. +//! +//! # Example //! //! ``` //! use wasmtime_bench_api::*; -//! let module = wat::parse_bytes(br#"(module -//! (func $bench_start (import "bench" "start")) -//! (func $bench_end (import "bench" "end")) -//! (func $start (export "_start") -//! (call $bench_start) (i32.const 2) (i32.const 2) (i32.add) (drop) (call $bench_end)) -//! )"#).unwrap(); -//! let engine = unsafe { engine_create(module.as_ptr(), module.len()) }; //! -//! // Start compilation timer. -//! unsafe { engine_compile_module(engine) }; -//! // End compilation timer. +//! let engine = unsafe { wasm_bench_create() }; +//! assert!(!engine.is_null()); //! -//! // The Wasm benchmark will expect us to provide functions to start ("bench" "start") and stop ("bench" "stop") the -//! // measurement counters/timers during execution; here we provide a no-op implementation. -//! extern "C" fn noop() {} +//! let wasm = wat::parse_bytes(br#" +//! (module +//! (func $bench_start (import "bench" "start")) +//! (func $bench_end (import "bench" "end")) +//! (func $start (export "_start") +//! call $bench_start +//! i32.const 1 +//! i32.const 2 +//! i32.add +//! drop +//! call $bench_end +//! ) +//! ) +//! "#).unwrap(); //! -//! // Start instantiation timer. -//! unsafe { engine_instantiate_module(engine, noop, noop) }; -//! // End instantiation timer. +//! // Start your compilation timer here. +//! let code = unsafe { wasm_bench_compile(engine, wasm.as_ptr(), wasm.len()) }; +//! // End your compilation timer here. +//! assert_eq!(code, OK); //! -//! // No need to start timers for the execution since, by convention, the timer functions we passed during -//! // instantiation will be called by the benchmark at the appropriate time (before and after the benchmarked section). -//! unsafe { engine_execute_module(engine) }; +//! // The Wasm benchmark will expect us to provide functions to start ("bench" +//! // "start") and stop ("bench" "stop") the measurement counters/timers during +//! // execution. +//! extern "C" fn bench_start() { +//! // Start your execution timer here. +//! } +//! extern "C" fn bench_stop() { +//! // End your execution timer here. +//! } //! -//! unsafe { engine_free(engine) } +//! // Start your instantiation timer here. +//! let code = unsafe { wasm_bench_instantiate(engine, bench_start, bench_stop) }; +//! // End your instantiation timer here. +//! assert_eq!(code, OK); +//! +//! // No need to start timers for the execution since, by convention, the timer +//! // functions we passed during instantiation will be called by the benchmark +//! // at the appropriate time (before and after the benchmarked section). +//! let code = unsafe { wasm_bench_execute(engine) }; +//! assert_eq!(code, OK); +//! +//! unsafe { +//! wasm_bench_free(engine); +//! } //! ``` -use anyhow::{anyhow, Result}; -use core::slice; -use std::os::raw::c_int; + +use anyhow::{anyhow, Context, Result}; +use std::os::raw::{c_int, c_void}; +use std::slice; use wasi_common::WasiCtxBuilder; use wasmtime::{Config, Engine, Instance, Linker, Module, Store}; use wasmtime_wasi::Wasi; -/// Exposes a C-compatible way of creating the engine from the bytes of a single Wasm module. This function returns a -/// pointer to an opaque structure that contains the engine's initialized state. +pub type ExitCode = c_int; +pub const OK: ExitCode = 0; +pub const ERR: ExitCode = -1; + +/// Exposes a C-compatible way of creating the engine from the bytes of a single +/// Wasm module. +/// +/// This function returns a pointer to a structure that contains the engine's +/// initialized state. #[no_mangle] -pub extern "C" fn engine_create( - wasm_bytes: *const u8, - wasm_bytes_length: usize, -) -> *mut OpaqueEngineState { - let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) }; - let state = Box::new(EngineState::new(wasm_bytes)); - Box::into_raw(state) as *mut _ +pub extern "C" fn wasm_bench_create() -> *mut c_void { + let state = Box::new(BenchState::new()); + Box::into_raw(state) as _ } /// Free the engine state allocated by this library. #[no_mangle] -pub extern "C" fn engine_free(state: *mut OpaqueEngineState) { +pub extern "C" fn wasm_bench_free(state: *mut c_void) { + assert!(!state.is_null()); unsafe { - Box::from_raw(state); + Box::from_raw(state as *mut BenchState); } } /// Compile the Wasm benchmark module. #[no_mangle] -pub extern "C" fn engine_compile_module(state: *mut OpaqueEngineState) -> c_int { - let result = unsafe { OpaqueEngineState::convert(state) }.compile(); - to_c_error(result, "failed to compile") +pub extern "C" fn wasm_bench_compile( + state: *mut c_void, + wasm_bytes: *const u8, + wasm_bytes_length: usize, +) -> ExitCode { + let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; + let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) }; + let result = state.compile(wasm_bytes).context("failed to compile"); + to_exit_code(result) } /// Instantiate the Wasm benchmark module. #[no_mangle] -pub extern "C" fn engine_instantiate_module( - state: *mut OpaqueEngineState, +pub extern "C" fn wasm_bench_instantiate( + state: *mut c_void, bench_start: extern "C" fn(), bench_end: extern "C" fn(), -) -> c_int { - let result = unsafe { OpaqueEngineState::convert(state) }.instantiate(bench_start, bench_end); - to_c_error(result, "failed to instantiate") +) -> ExitCode { + let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; + let result = state + .instantiate(bench_start, bench_end) + .context("failed to instantiate"); + to_exit_code(result) } /// Execute the Wasm benchmark module. #[no_mangle] -pub extern "C" fn engine_execute_module(state: *mut OpaqueEngineState) -> c_int { - let result = unsafe { OpaqueEngineState::convert(state) }.execute(); - to_c_error(result, "failed to execute") +pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode { + let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; + let result = state.execute().context("failed to execute"); + to_exit_code(result) } -/// Helper function for converting a Rust result to a C error code (0 == success). Additionally, this will print an -/// error indicating some information regarding the failure. -fn to_c_error(result: Result, message: &str) -> c_int { - match result { - Ok(_) => 0, +/// Helper function for converting a Rust result to a C error code. +/// +/// This will print an error indicating some information regarding the failure. +fn to_exit_code(result: impl Into>) -> ExitCode { + match result.into() { + Ok(_) => OK, Err(error) => { - println!("{}: {:?}", message, error); - 1 + eprintln!("{:?}", error); + ERR } } } -/// Opaque pointer type for hiding the engine state details. -#[repr(C)] -pub struct OpaqueEngineState { - _private: [u8; 0], -} -impl OpaqueEngineState { - unsafe fn convert(ptr: *mut OpaqueEngineState) -> &'static mut EngineState<'static> { - assert!(!ptr.is_null()); - &mut *(ptr as *mut EngineState) - } -} - -/// This structure contains the actual Rust implementation of the state required to manage the Wasmtime engine between -/// calls. -struct EngineState<'a> { - bytes: &'a [u8], +/// This structure contains the actual Rust implementation of the state required +/// to manage the Wasmtime engine between calls. +struct BenchState { engine: Engine, store: Store, module: Option, instance: Option, + did_execute: bool, } -impl<'a> EngineState<'a> { - fn new(bytes: &'a [u8]) -> Self { - // TODO turn off caching? +impl BenchState { + fn new() -> Self { let mut config = Config::new(); config.wasm_simd(true); + // NB: do not configure a code cache. + let engine = Engine::new(&config); let store = Store::new(&engine); Self { - bytes, engine, store, module: None, instance: None, + did_execute: false, } } - fn compile(&mut self) -> Result<()> { - self.module = Some(Module::from_binary(&self.engine, self.bytes)?); + fn compile(&mut self, bytes: &[u8]) -> Result<()> { + assert!( + self.module.is_none(), + "create a new engine to repeat compilation" + ); + self.module = Some(Module::from_binary(&self.engine, bytes)?); Ok(()) } @@ -148,51 +192,57 @@ impl<'a> EngineState<'a> { bench_start: extern "C" fn(), bench_end: extern "C" fn(), ) -> Result<()> { - // TODO instantiate WASI modules? - match &self.module { - Some(module) => { - let mut linker = Linker::new(&self.store); + assert!( + self.instance.is_none(), + "create a new engine to repeat instantiation" + ); + let module = self + .module + .as_mut() + .expect("compile the module before instantiating it"); - // Import a very restricted WASI environment. - let mut cx = WasiCtxBuilder::new(); - cx.inherit_stdio(); - let cx = cx.build()?; - let wasi = Wasi::new(linker.store(), cx); - wasi.add_to_linker(&mut linker)?; + let mut linker = Linker::new(&self.store); - // Import the specialized benchmarking functions. - linker.func("bench", "start", move || bench_start())?; - linker.func("bench", "end", move || bench_end())?; + // Import a very restricted WASI environment. + let mut cx = WasiCtxBuilder::new(); + cx.inherit_stdio(); + let cx = cx.build()?; + let wasi = Wasi::new(linker.store(), cx); + wasi.add_to_linker(&mut linker)?; - self.instance = Some(linker.instantiate(module)?); - } - None => panic!("compile the module before instantiating it"), - } + // Import the specialized benchmarking functions. + linker.func("bench", "start", move || bench_start())?; + linker.func("bench", "end", move || bench_end())?; + + self.instance = Some(linker.instantiate(&module)?); Ok(()) } - fn execute(&self) -> Result<()> { - match &self.instance { - Some(instance) => { - let start_func = instance.get_func("_start").expect("a _start function"); - let runnable_func = start_func.get0::<()>()?; - match runnable_func() { - Ok(_) => {} - Err(trap) => { - // Since _start will likely return by using the system `exit` call, we must - // check the trap code to see if it actually represents a successful exit. - let status = trap.i32_exit_status(); - if status != Some(0) { - return Err(anyhow!( - "_start exited with a non-zero code: {}", - status.unwrap() - )); - } - } - }; + fn execute(&mut self) -> Result<()> { + assert!(!self.did_execute, "create a new engine to repeat execution"); + self.did_execute = true; + + let instance = self + .instance + .as_ref() + .expect("instantiate the module before executing it"); + + let start_func = instance.get_func("_start").expect("a _start function"); + let runnable_func = start_func.get0::<()>()?; + match runnable_func() { + Ok(_) => Ok(()), + Err(trap) => { + // Since _start will likely return by using the system `exit` call, we must + // check the trap code to see if it actually represents a successful exit. + match trap.i32_exit_status() { + Some(0) => Ok(()), + Some(n) => Err(anyhow!("_start exited with a non-zero code: {}", n)), + None => Err(anyhow!( + "executing the benchmark resulted in a trap: {}", + trap + )), + } } - None => panic!("instantiate the module before executing it"), } - Ok(()) } }