xref: /wasmtime-44.0.1/crates/bench-api/src/lib.rs (revision 3b3b126f)
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