xref: /wasmtime-44.0.1/crates/bench-api/src/lib.rs (revision 331b0dee)
1 //! A C API for benchmarking Wasmtime's WebAssembly compilation, instantiation,
2 //! and execution.
3 //!
4 //! The API expects calls that match the following state machine:
5 //!
6 //! ```text
7 //!               |
8 //!               |
9 //!               V
10 //! .---> wasm_bench_create
11 //! |        |        |
12 //! |        |        |
13 //! |        |        V
14 //! |        |   wasm_bench_compile
15 //! |        |     |            |
16 //! |        |     |            |     .----.
17 //! |        |     |            |     |    |
18 //! |        |     |            V     V    |
19 //! |        |     |     wasm_bench_instantiate <------.
20 //! |        |     |            |        |             |
21 //! |        |     |            |        |             |
22 //! |        |     |            |        |             |
23 //! |        |     |     .------'        '-----> wasm_bench_execute
24 //! |        |     |     |                             |
25 //! |        |     |     |                             |
26 //! |        V     V     V                             |
27 //! '------ wasm_bench_free <--------------------------'
28 //!               |
29 //!               |
30 //!               V
31 //! ```
32 //!
33 //! All API calls must happen on the same thread.
34 //!
35 //! Functions which return pointers use null as an error value. Function which
36 //! return `int` use `0` as OK and non-zero as an error value.
37 //!
38 //! # Example
39 //!
40 //! ```
41 //! use std::ptr;
42 //! use wasmtime_bench_api::*;
43 //!
44 //! let working_dir = std::env::current_dir().unwrap().display().to_string();
45 //! let stdout_path = "./stdout.log";
46 //! let stderr_path = "./stderr.log";
47 //!
48 //! // Functions to start/end timers for compilation.
49 //! //
50 //! // The `compilation_timer` pointer configured in the `WasmBenchConfig` is
51 //! // passed through.
52 //! extern "C" fn compilation_start(timer: *mut u8) {
53 //!     // Start your compilation timer here.
54 //! }
55 //! extern "C" fn compilation_end(timer: *mut u8) {
56 //!     // End your compilation timer here.
57 //! }
58 //!
59 //! // Similar for instantiation.
60 //! extern "C" fn instantiation_start(timer: *mut u8) {
61 //!     // Start your instantiation timer here.
62 //! }
63 //! extern "C" fn instantiation_end(timer: *mut u8) {
64 //!     // End your instantiation timer here.
65 //! }
66 //!
67 //! // Similar for execution.
68 //! extern "C" fn execution_start(timer: *mut u8) {
69 //!     // Start your execution timer here.
70 //! }
71 //! extern "C" fn execution_end(timer: *mut u8) {
72 //!     // End your execution timer here.
73 //! }
74 //!
75 //! let config = WasmBenchConfig {
76 //!     working_dir_ptr: working_dir.as_ptr(),
77 //!     working_dir_len: working_dir.len(),
78 //!     stdout_path_ptr: stdout_path.as_ptr(),
79 //!     stdout_path_len: stdout_path.len(),
80 //!     stderr_path_ptr: stderr_path.as_ptr(),
81 //!     stderr_path_len: stderr_path.len(),
82 //!     stdin_path_ptr: ptr::null(),
83 //!     stdin_path_len: 0,
84 //!     compilation_timer: ptr::null_mut(),
85 //!     compilation_start,
86 //!     compilation_end,
87 //!     instantiation_timer: ptr::null_mut(),
88 //!     instantiation_start,
89 //!     instantiation_end,
90 //!     execution_timer: ptr::null_mut(),
91 //!     execution_start,
92 //!     execution_end,
93 //! };
94 //!
95 //! let mut bench_api = ptr::null_mut();
96 //! unsafe {
97 //!     let code = wasm_bench_create(config, &mut bench_api);
98 //!     assert_eq!(code, OK);
99 //!     assert!(!bench_api.is_null());
100 //! };
101 //!
102 //! let wasm = wat::parse_bytes(br#"
103 //!     (module
104 //!         (func $bench_start (import "bench" "start"))
105 //!         (func $bench_end (import "bench" "end"))
106 //!         (func $start (export "_start")
107 //!             call $bench_start
108 //!             i32.const 1
109 //!             i32.const 2
110 //!             i32.add
111 //!             drop
112 //!             call $bench_end
113 //!         )
114 //!     )
115 //! "#).unwrap();
116 //!
117 //! // This will call the `compilation_{start,end}` timing functions on success.
118 //! let code = unsafe { wasm_bench_compile(bench_api, wasm.as_ptr(), wasm.len()) };
119 //! assert_eq!(code, OK);
120 //!
121 //! // This will call the `instantiation_{start,end}` timing functions on success.
122 //! let code = unsafe { wasm_bench_instantiate(bench_api) };
123 //! assert_eq!(code, OK);
124 //!
125 //! // This will call the `execution_{start,end}` timing functions on success.
126 //! let code = unsafe { wasm_bench_execute(bench_api) };
127 //! assert_eq!(code, OK);
128 //!
129 //! unsafe {
130 //!     wasm_bench_free(bench_api);
131 //! }
132 //! ```
133 
134 mod unsafe_send_sync;
135 
136 use crate::unsafe_send_sync::UnsafeSendSync;
137 use anyhow::{anyhow, Context, Result};
138 use std::os::raw::{c_int, c_void};
139 use std::slice;
140 use std::{env, path::PathBuf};
141 use wasmtime::{Config, Engine, Instance, Linker, Module, Store};
142 use wasmtime_wasi::{sync::WasiCtxBuilder, WasiCtx};
143 
144 pub type ExitCode = c_int;
145 pub const OK: ExitCode = 0;
146 pub const ERR: ExitCode = -1;
147 
148 // Randomize the location of heap objects to avoid accidental locality being an
149 // uncontrolled variable that obscures performance evaluation in our
150 // experiments.
151 #[cfg(feature = "shuffling-allocator")]
152 #[global_allocator]
153 static ALLOC: shuffling_allocator::ShufflingAllocator<std::alloc::System> =
154     shuffling_allocator::wrap!(&std::alloc::System);
155 
156 /// Configuration options for the benchmark.
157 #[repr(C)]
158 pub struct WasmBenchConfig {
159     /// The working directory where benchmarks should be executed.
160     pub working_dir_ptr: *const u8,
161     pub working_dir_len: usize,
162 
163     /// The file path that should be created and used as `stdout`.
164     pub stdout_path_ptr: *const u8,
165     pub stdout_path_len: usize,
166 
167     /// The file path that should be created and used as `stderr`.
168     pub stderr_path_ptr: *const u8,
169     pub stderr_path_len: usize,
170 
171     /// The (optional) file path that should be opened and used as `stdin`. If
172     /// not provided, then the WASI context will not have a `stdin` initialized.
173     pub stdin_path_ptr: *const u8,
174     pub stdin_path_len: usize,
175 
176     /// The functions to start and stop performance timers/counters during Wasm
177     /// compilation.
178     pub compilation_timer: *mut u8,
179     pub compilation_start: extern "C" fn(*mut u8),
180     pub compilation_end: extern "C" fn(*mut u8),
181 
182     /// The functions to start and stop performance timers/counters during Wasm
183     /// instantiation.
184     pub instantiation_timer: *mut u8,
185     pub instantiation_start: extern "C" fn(*mut u8),
186     pub instantiation_end: extern "C" fn(*mut u8),
187 
188     /// The functions to start and stop performance timers/counters during Wasm
189     /// execution.
190     pub execution_timer: *mut u8,
191     pub execution_start: extern "C" fn(*mut u8),
192     pub execution_end: extern "C" fn(*mut u8),
193 }
194 
195 impl WasmBenchConfig {
196     fn working_dir(&self) -> Result<PathBuf> {
197         let working_dir =
198             unsafe { std::slice::from_raw_parts(self.working_dir_ptr, self.working_dir_len) };
199         let working_dir = std::str::from_utf8(working_dir)
200             .context("given working directory is not valid UTF-8")?;
201         Ok(working_dir.into())
202     }
203 
204     fn stdout_path(&self) -> Result<PathBuf> {
205         let stdout_path =
206             unsafe { std::slice::from_raw_parts(self.stdout_path_ptr, self.stdout_path_len) };
207         let stdout_path =
208             std::str::from_utf8(stdout_path).context("given stdout path is not valid UTF-8")?;
209         Ok(stdout_path.into())
210     }
211 
212     fn stderr_path(&self) -> Result<PathBuf> {
213         let stderr_path =
214             unsafe { std::slice::from_raw_parts(self.stderr_path_ptr, self.stderr_path_len) };
215         let stderr_path =
216             std::str::from_utf8(stderr_path).context("given stderr path is not valid UTF-8")?;
217         Ok(stderr_path.into())
218     }
219 
220     fn stdin_path(&self) -> Result<Option<PathBuf>> {
221         if self.stdin_path_ptr.is_null() {
222             return Ok(None);
223         }
224 
225         let stdin_path =
226             unsafe { std::slice::from_raw_parts(self.stdin_path_ptr, self.stdin_path_len) };
227         let stdin_path =
228             std::str::from_utf8(stdin_path).context("given stdin path is not valid UTF-8")?;
229         Ok(Some(stdin_path.into()))
230     }
231 }
232 
233 /// Exposes a C-compatible way of creating the engine from the bytes of a single
234 /// Wasm module.
235 ///
236 /// On success, the `out_bench_ptr` is initialized to a pointer to a structure
237 /// that contains the engine's initialized state, and `0` is returned. On
238 /// failure, a non-zero status code is returned and `out_bench_ptr` is left
239 /// untouched.
240 #[no_mangle]
241 pub extern "C" fn wasm_bench_create(
242     config: WasmBenchConfig,
243     out_bench_ptr: *mut *mut c_void,
244 ) -> ExitCode {
245     let result = (|| -> Result<_> {
246         let working_dir = config.working_dir()?;
247         let working_dir =
248             cap_std::fs::Dir::open_ambient_dir(&working_dir, cap_std::ambient_authority())
249                 .with_context(|| {
250                     format!(
251                         "failed to preopen the working directory: {}",
252                         working_dir.display(),
253                     )
254                 })?;
255 
256         let stdout_path = config.stdout_path()?;
257         let stderr_path = config.stderr_path()?;
258         let stdin_path = config.stdin_path()?;
259 
260         let state = Box::new(BenchState::new(
261             config.compilation_timer,
262             config.compilation_start,
263             config.compilation_end,
264             config.instantiation_timer,
265             config.instantiation_start,
266             config.instantiation_end,
267             config.execution_timer,
268             config.execution_start,
269             config.execution_end,
270             move || {
271                 let mut cx = WasiCtxBuilder::new();
272 
273                 let stdout = std::fs::File::create(&stdout_path)
274                     .with_context(|| format!("failed to create {}", stdout_path.display()))?;
275                 let stdout = cap_std::fs::File::from_std(stdout, cap_std::ambient_authority());
276                 let stdout = wasi_cap_std_sync::file::File::from_cap_std(stdout);
277                 cx = cx.stdout(Box::new(stdout));
278 
279                 let stderr = std::fs::File::create(&stderr_path)
280                     .with_context(|| format!("failed to create {}", stderr_path.display()))?;
281                 let stderr = cap_std::fs::File::from_std(stderr, cap_std::ambient_authority());
282                 let stderr = wasi_cap_std_sync::file::File::from_cap_std(stderr);
283                 cx = cx.stderr(Box::new(stderr));
284 
285                 if let Some(stdin_path) = &stdin_path {
286                     let stdin = std::fs::File::open(stdin_path)
287                         .with_context(|| format!("failed to open {}", stdin_path.display()))?;
288                     let stdin = cap_std::fs::File::from_std(stdin, cap_std::ambient_authority());
289                     let stdin = wasi_cap_std_sync::file::File::from_cap_std(stdin);
290                     cx = cx.stdin(Box::new(stdin));
291                 }
292 
293                 // Allow access to the working directory so that the benchmark can read
294                 // its input workload(s).
295                 cx = cx.preopened_dir(working_dir.try_clone()?, ".")?;
296 
297                 // Pass this env var along so that the benchmark program can use smaller
298                 // input workload(s) if it has them and that has been requested.
299                 if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") {
300                     cx = cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val)?;
301                 }
302 
303                 Ok(cx.build())
304             },
305         )?);
306         Ok(Box::into_raw(state) as _)
307     })();
308 
309     if let Ok(bench_ptr) = result {
310         unsafe {
311             assert!(!out_bench_ptr.is_null());
312             *out_bench_ptr = bench_ptr;
313         }
314     }
315 
316     to_exit_code(result.map(|_| ()))
317 }
318 
319 /// Free the engine state allocated by this library.
320 #[no_mangle]
321 pub extern "C" fn wasm_bench_free(state: *mut c_void) {
322     assert!(!state.is_null());
323     unsafe {
324         Box::from_raw(state as *mut BenchState);
325     }
326 }
327 
328 /// Compile the Wasm benchmark module.
329 #[no_mangle]
330 pub extern "C" fn wasm_bench_compile(
331     state: *mut c_void,
332     wasm_bytes: *const u8,
333     wasm_bytes_length: usize,
334 ) -> ExitCode {
335     let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
336     let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) };
337     let result = state.compile(wasm_bytes).context("failed to compile");
338     to_exit_code(result)
339 }
340 
341 /// Instantiate the Wasm benchmark module.
342 #[no_mangle]
343 pub extern "C" fn wasm_bench_instantiate(state: *mut c_void) -> ExitCode {
344     let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
345     let result = state.instantiate().context("failed to instantiate");
346     to_exit_code(result)
347 }
348 
349 /// Execute the Wasm benchmark module.
350 #[no_mangle]
351 pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode {
352     let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
353     let result = state.execute().context("failed to execute");
354     to_exit_code(result)
355 }
356 
357 /// Helper function for converting a Rust result to a C error code.
358 ///
359 /// This will print an error indicating some information regarding the failure.
360 fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode {
361     match result.into() {
362         Ok(_) => OK,
363         Err(error) => {
364             eprintln!("{:?}", error);
365             ERR
366         }
367     }
368 }
369 
370 /// This structure contains the actual Rust implementation of the state required
371 /// to manage the Wasmtime engine between calls.
372 struct BenchState {
373     linker: Linker<HostState>,
374     compilation_timer: *mut u8,
375     compilation_start: extern "C" fn(*mut u8),
376     compilation_end: extern "C" fn(*mut u8),
377     instantiation_timer: *mut u8,
378     instantiation_start: extern "C" fn(*mut u8),
379     instantiation_end: extern "C" fn(*mut u8),
380     make_wasi_cx: Box<dyn FnMut() -> Result<WasiCtx>>,
381     module: Option<Module>,
382     store_and_instance: Option<(Store<HostState>, Instance)>,
383 }
384 
385 struct HostState {
386     wasi: WasiCtx,
387     #[cfg(feature = "wasi-nn")]
388     wasi_nn: wasmtime_wasi_nn::WasiNnCtx,
389 
390     #[cfg(feature = "wasi-crypto")]
391     wasi_crypto: wasmtime_wasi_crypto::WasiCryptoCtx,
392 }
393 
394 impl BenchState {
395     fn new(
396         compilation_timer: *mut u8,
397         compilation_start: extern "C" fn(*mut u8),
398         compilation_end: extern "C" fn(*mut u8),
399         instantiation_timer: *mut u8,
400         instantiation_start: extern "C" fn(*mut u8),
401         instantiation_end: extern "C" fn(*mut u8),
402         execution_timer: *mut u8,
403         execution_start: extern "C" fn(*mut u8),
404         execution_end: extern "C" fn(*mut u8),
405         make_wasi_cx: impl FnMut() -> Result<WasiCtx> + 'static,
406     ) -> Result<Self> {
407         // NB: do not configure a code cache.
408         let mut config = Config::new();
409         config.wasm_simd(true);
410         let engine = Engine::new(&config)?;
411         let mut linker = Linker::<HostState>::new(&engine);
412 
413         // Define the benchmarking start/end functions.
414         let execution_timer = unsafe {
415             // Safe because this bench API's contract requires that its methods
416             // are only ever called from a single thread.
417             UnsafeSendSync::new(execution_timer)
418         };
419         linker.func_wrap("bench", "start", move || {
420             execution_start(*execution_timer.get());
421             Ok(())
422         })?;
423         linker.func_wrap("bench", "end", move || {
424             execution_end(*execution_timer.get());
425             Ok(())
426         })?;
427 
428         wasmtime_wasi::add_to_linker(&mut linker, |cx| &mut cx.wasi)?;
429 
430         #[cfg(feature = "wasi-nn")]
431         wasmtime_wasi_nn::add_to_linker(&mut linker, |cx| &mut cx.wasi_nn)?;
432 
433         #[cfg(feature = "wasi-crypto")]
434         wasmtime_wasi_crypto::add_to_linker(&mut linker, |cx| &mut cx.wasi_crypto)?;
435 
436         Ok(Self {
437             linker,
438             compilation_timer,
439             compilation_start,
440             compilation_end,
441             instantiation_timer,
442             instantiation_start,
443             instantiation_end,
444             make_wasi_cx: Box::new(make_wasi_cx) as _,
445             module: None,
446             store_and_instance: None,
447         })
448     }
449 
450     fn compile(&mut self, bytes: &[u8]) -> Result<()> {
451         assert!(
452             self.module.is_none(),
453             "create a new engine to repeat compilation"
454         );
455 
456         (self.compilation_start)(self.compilation_timer);
457         let module = Module::from_binary(self.linker.engine(), bytes)?;
458         (self.compilation_end)(self.compilation_timer);
459 
460         self.module = Some(module);
461         Ok(())
462     }
463 
464     fn instantiate(&mut self) -> Result<()> {
465         let module = self
466             .module
467             .as_ref()
468             .expect("compile the module before instantiating it");
469 
470         let host = HostState {
471             wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?,
472             #[cfg(feature = "wasi-nn")]
473             wasi_nn: wasmtime_wasi_nn::WasiNnCtx::new()?,
474             #[cfg(feature = "wasi-crypto")]
475             wasi_crypto: wasmtime_wasi_nn::WasiCryptoCtx::new(),
476         };
477 
478         // NB: Start measuring instantiation time *after* we've created the WASI
479         // context, since that needs to do file I/O to setup
480         // stdin/stdout/stderr.
481         (self.instantiation_start)(self.instantiation_timer);
482         let mut store = Store::new(self.linker.engine(), host);
483         let instance = self.linker.instantiate(&mut store, &module)?;
484         (self.instantiation_end)(self.instantiation_timer);
485 
486         self.store_and_instance = Some((store, instance));
487         Ok(())
488     }
489 
490     fn execute(&mut self) -> Result<()> {
491         let (mut store, instance) = self
492             .store_and_instance
493             .take()
494             .expect("instantiate the module before executing it");
495 
496         let start_func = instance.get_typed_func::<(), (), _>(&mut store, "_start")?;
497         match start_func.call(&mut store, ()) {
498             Ok(_) => Ok(()),
499             Err(trap) => {
500                 // Since _start will likely return by using the system `exit` call, we must
501                 // check the trap code to see if it actually represents a successful exit.
502                 match trap.i32_exit_status() {
503                     Some(0) => Ok(()),
504                     Some(n) => Err(anyhow!("_start exited with a non-zero code: {}", n)),
505                     None => Err(anyhow!(
506                         "executing the benchmark resulted in a trap: {}",
507                         trap
508                     )),
509                 }
510             }
511         }
512     }
513 }
514