Merge pull request #2512 from fitzgen/bench-api-tweaks
bench-api: Clean up the benchmarking API
This commit is contained in:
@@ -1,145 +1,189 @@
|
|||||||
//! Expose a C-compatible API for controlling the Wasmtime engine during benchmarking. The API expects very sequential
|
//! A C API for benchmarking Wasmtime's WebAssembly compilation, instantiation,
|
||||||
//! use:
|
//! and execution.
|
||||||
//! - `engine_create`
|
|
||||||
//! - `engine_compile_module`
|
|
||||||
//! - `engine_instantiate_module`
|
|
||||||
//! - `engine_execute_module`
|
|
||||||
//! - `engine_free`
|
|
||||||
//!
|
//!
|
||||||
//! 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::*;
|
//! 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.
|
//! let engine = unsafe { wasm_bench_create() };
|
||||||
//! unsafe { engine_compile_module(engine) };
|
//! assert!(!engine.is_null());
|
||||||
//! // End compilation timer.
|
|
||||||
//!
|
//!
|
||||||
//! // The Wasm benchmark will expect us to provide functions to start ("bench" "start") and stop ("bench" "stop") the
|
//! let wasm = wat::parse_bytes(br#"
|
||||||
//! // measurement counters/timers during execution; here we provide a no-op implementation.
|
//! (module
|
||||||
//! extern "C" fn noop() {}
|
//! (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.
|
//! // Start your compilation timer here.
|
||||||
//! unsafe { engine_instantiate_module(engine, noop, noop) };
|
//! let code = unsafe { wasm_bench_compile(engine, wasm.as_ptr(), wasm.len()) };
|
||||||
//! // End instantiation timer.
|
//! // 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
|
//! // The Wasm benchmark will expect us to provide functions to start ("bench"
|
||||||
//! // instantiation will be called by the benchmark at the appropriate time (before and after the benchmarked section).
|
//! // "start") and stop ("bench" "stop") the measurement counters/timers during
|
||||||
//! unsafe { engine_execute_module(engine) };
|
//! // 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 anyhow::{anyhow, Context, Result};
|
||||||
use std::os::raw::c_int;
|
use std::os::raw::{c_int, c_void};
|
||||||
|
use std::slice;
|
||||||
use wasi_common::WasiCtxBuilder;
|
use wasi_common::WasiCtxBuilder;
|
||||||
use wasmtime::{Config, Engine, Instance, Linker, Module, Store};
|
use wasmtime::{Config, Engine, Instance, Linker, Module, Store};
|
||||||
use wasmtime_wasi::Wasi;
|
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
|
pub type ExitCode = c_int;
|
||||||
/// pointer to an opaque structure that contains the engine's initialized state.
|
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]
|
#[no_mangle]
|
||||||
pub extern "C" fn engine_create(
|
pub extern "C" fn wasm_bench_create() -> *mut c_void {
|
||||||
wasm_bytes: *const u8,
|
let state = Box::new(BenchState::new());
|
||||||
wasm_bytes_length: usize,
|
Box::into_raw(state) as _
|
||||||
) -> *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 _
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Free the engine state allocated by this library.
|
/// Free the engine state allocated by this library.
|
||||||
#[no_mangle]
|
#[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 {
|
unsafe {
|
||||||
Box::from_raw(state);
|
Box::from_raw(state as *mut BenchState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compile the Wasm benchmark module.
|
/// Compile the Wasm benchmark module.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn engine_compile_module(state: *mut OpaqueEngineState) -> c_int {
|
pub extern "C" fn wasm_bench_compile(
|
||||||
let result = unsafe { OpaqueEngineState::convert(state) }.compile();
|
state: *mut c_void,
|
||||||
to_c_error(result, "failed to compile")
|
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.
|
/// Instantiate the Wasm benchmark module.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn engine_instantiate_module(
|
pub extern "C" fn wasm_bench_instantiate(
|
||||||
state: *mut OpaqueEngineState,
|
state: *mut c_void,
|
||||||
bench_start: extern "C" fn(),
|
bench_start: extern "C" fn(),
|
||||||
bench_end: extern "C" fn(),
|
bench_end: extern "C" fn(),
|
||||||
) -> c_int {
|
) -> ExitCode {
|
||||||
let result = unsafe { OpaqueEngineState::convert(state) }.instantiate(bench_start, bench_end);
|
let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
|
||||||
to_c_error(result, "failed to instantiate")
|
let result = state
|
||||||
|
.instantiate(bench_start, bench_end)
|
||||||
|
.context("failed to instantiate");
|
||||||
|
to_exit_code(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the Wasm benchmark module.
|
/// Execute the Wasm benchmark module.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn engine_execute_module(state: *mut OpaqueEngineState) -> c_int {
|
pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode {
|
||||||
let result = unsafe { OpaqueEngineState::convert(state) }.execute();
|
let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
|
||||||
to_c_error(result, "failed to execute")
|
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
|
/// Helper function for converting a Rust result to a C error code.
|
||||||
/// error indicating some information regarding the failure.
|
///
|
||||||
fn to_c_error<T>(result: Result<T>, message: &str) -> c_int {
|
/// This will print an error indicating some information regarding the failure.
|
||||||
match result {
|
fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode {
|
||||||
Ok(_) => 0,
|
match result.into() {
|
||||||
|
Ok(_) => OK,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
println!("{}: {:?}", message, error);
|
eprintln!("{:?}", error);
|
||||||
1
|
ERR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opaque pointer type for hiding the engine state details.
|
/// This structure contains the actual Rust implementation of the state required
|
||||||
#[repr(C)]
|
/// to manage the Wasmtime engine between calls.
|
||||||
pub struct OpaqueEngineState {
|
struct BenchState {
|
||||||
_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],
|
|
||||||
engine: Engine,
|
engine: Engine,
|
||||||
store: Store,
|
store: Store,
|
||||||
module: Option<Module>,
|
module: Option<Module>,
|
||||||
instance: Option<Instance>,
|
instance: Option<Instance>,
|
||||||
|
did_execute: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> EngineState<'a> {
|
impl BenchState {
|
||||||
fn new(bytes: &'a [u8]) -> Self {
|
fn new() -> Self {
|
||||||
// TODO turn off caching?
|
|
||||||
let mut config = Config::new();
|
let mut config = Config::new();
|
||||||
config.wasm_simd(true);
|
config.wasm_simd(true);
|
||||||
|
// NB: do not configure a code cache.
|
||||||
|
|
||||||
let engine = Engine::new(&config);
|
let engine = Engine::new(&config);
|
||||||
let store = Store::new(&engine);
|
let store = Store::new(&engine);
|
||||||
Self {
|
Self {
|
||||||
bytes,
|
|
||||||
engine,
|
engine,
|
||||||
store,
|
store,
|
||||||
module: None,
|
module: None,
|
||||||
instance: None,
|
instance: None,
|
||||||
|
did_execute: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile(&mut self) -> Result<()> {
|
fn compile(&mut self, bytes: &[u8]) -> Result<()> {
|
||||||
self.module = Some(Module::from_binary(&self.engine, self.bytes)?);
|
assert!(
|
||||||
|
self.module.is_none(),
|
||||||
|
"create a new engine to repeat compilation"
|
||||||
|
);
|
||||||
|
self.module = Some(Module::from_binary(&self.engine, bytes)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,51 +192,57 @@ impl<'a> EngineState<'a> {
|
|||||||
bench_start: extern "C" fn(),
|
bench_start: extern "C" fn(),
|
||||||
bench_end: extern "C" fn(),
|
bench_end: extern "C" fn(),
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// TODO instantiate WASI modules?
|
assert!(
|
||||||
match &self.module {
|
self.instance.is_none(),
|
||||||
Some(module) => {
|
"create a new engine to repeat instantiation"
|
||||||
let mut linker = Linker::new(&self.store);
|
);
|
||||||
|
let module = self
|
||||||
|
.module
|
||||||
|
.as_mut()
|
||||||
|
.expect("compile the module before instantiating it");
|
||||||
|
|
||||||
// Import a very restricted WASI environment.
|
let mut linker = Linker::new(&self.store);
|
||||||
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)?;
|
|
||||||
|
|
||||||
// Import the specialized benchmarking functions.
|
// Import a very restricted WASI environment.
|
||||||
linker.func("bench", "start", move || bench_start())?;
|
let mut cx = WasiCtxBuilder::new();
|
||||||
linker.func("bench", "end", move || bench_end())?;
|
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)?);
|
// Import the specialized benchmarking functions.
|
||||||
}
|
linker.func("bench", "start", move || bench_start())?;
|
||||||
None => panic!("compile the module before instantiating it"),
|
linker.func("bench", "end", move || bench_end())?;
|
||||||
}
|
|
||||||
|
self.instance = Some(linker.instantiate(&module)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(&self) -> Result<()> {
|
fn execute(&mut self) -> Result<()> {
|
||||||
match &self.instance {
|
assert!(!self.did_execute, "create a new engine to repeat execution");
|
||||||
Some(instance) => {
|
self.did_execute = true;
|
||||||
let start_func = instance.get_func("_start").expect("a _start function");
|
|
||||||
let runnable_func = start_func.get0::<()>()?;
|
let instance = self
|
||||||
match runnable_func() {
|
.instance
|
||||||
Ok(_) => {}
|
.as_ref()
|
||||||
Err(trap) => {
|
.expect("instantiate the module before executing it");
|
||||||
// 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 start_func = instance.get_func("_start").expect("a _start function");
|
||||||
let status = trap.i32_exit_status();
|
let runnable_func = start_func.get0::<()>()?;
|
||||||
if status != Some(0) {
|
match runnable_func() {
|
||||||
return Err(anyhow!(
|
Ok(_) => Ok(()),
|
||||||
"_start exited with a non-zero code: {}",
|
Err(trap) => {
|
||||||
status.unwrap()
|
// 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(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user