xref: /wasmtime-44.0.1/crates/bench-api/src/lib.rs (revision 25bf8e0e)
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 //!     execution_flags_ptr: ptr::null(),
94 //!     execution_flags_len: 0,
95 //! };
96 //!
97 //! let mut bench_api = ptr::null_mut();
98 //! unsafe {
99 //!     let code = wasm_bench_create(config, &mut bench_api);
100 //!     assert_eq!(code, OK);
101 //!     assert!(!bench_api.is_null());
102 //! };
103 //!
104 //! let wasm = wat::parse_bytes(br#"
105 //!     (module
106 //!         (func $bench_start (import "bench" "start"))
107 //!         (func $bench_end (import "bench" "end"))
108 //!         (func $start (export "_start")
109 //!             call $bench_start
110 //!             i32.const 1
111 //!             i32.const 2
112 //!             i32.add
113 //!             drop
114 //!             call $bench_end
115 //!         )
116 //!     )
117 //! "#).unwrap();
118 //!
119 //! // This will call the `compilation_{start,end}` timing functions on success.
120 //! let code = unsafe { wasm_bench_compile(bench_api, wasm.as_ptr(), wasm.len()) };
121 //! assert_eq!(code, OK);
122 //!
123 //! // This will call the `instantiation_{start,end}` timing functions on success.
124 //! let code = unsafe { wasm_bench_instantiate(bench_api) };
125 //! assert_eq!(code, OK);
126 //!
127 //! // This will call the `execution_{start,end}` timing functions on success.
128 //! let code = unsafe { wasm_bench_execute(bench_api) };
129 //! assert_eq!(code, OK);
130 //!
131 //! unsafe {
132 //!     wasm_bench_free(bench_api);
133 //! }
134 //! ```
135 
136 mod unsafe_send_sync;
137 
138 use crate::unsafe_send_sync::UnsafeSendSync;
139 use anyhow::{Context, Result};
140 use std::os::raw::{c_int, c_void};
141 use std::slice;
142 use std::{env, path::PathBuf};
143 use target_lexicon::Triple;
144 use wasmtime::{Config, Engine, Instance, Linker, Module, Store};
145 use wasmtime_cli_flags::{CommonOptions, WasiModules};
146 use wasmtime_wasi::{sync::WasiCtxBuilder, I32Exit, WasiCtx};
147 
148 pub type ExitCode = c_int;
149 pub const OK: ExitCode = 0;
150 pub const ERR: ExitCode = -1;
151 
152 // Randomize the location of heap objects to avoid accidental locality being an
153 // uncontrolled variable that obscures performance evaluation in our
154 // experiments.
155 #[cfg(feature = "shuffling-allocator")]
156 #[global_allocator]
157 static ALLOC: shuffling_allocator::ShufflingAllocator<std::alloc::System> =
158     shuffling_allocator::wrap!(&std::alloc::System);
159 
160 /// Configuration options for the benchmark.
161 #[repr(C)]
162 pub struct WasmBenchConfig {
163     /// The working directory where benchmarks should be executed.
164     pub working_dir_ptr: *const u8,
165     pub working_dir_len: usize,
166 
167     /// The file path that should be created and used as `stdout`.
168     pub stdout_path_ptr: *const u8,
169     pub stdout_path_len: usize,
170 
171     /// The file path that should be created and used as `stderr`.
172     pub stderr_path_ptr: *const u8,
173     pub stderr_path_len: usize,
174 
175     /// The (optional) file path that should be opened and used as `stdin`. If
176     /// not provided, then the WASI context will not have a `stdin` initialized.
177     pub stdin_path_ptr: *const u8,
178     pub stdin_path_len: usize,
179 
180     /// The functions to start and stop performance timers/counters during Wasm
181     /// compilation.
182     pub compilation_timer: *mut u8,
183     pub compilation_start: extern "C" fn(*mut u8),
184     pub compilation_end: extern "C" fn(*mut u8),
185 
186     /// The functions to start and stop performance timers/counters during Wasm
187     /// instantiation.
188     pub instantiation_timer: *mut u8,
189     pub instantiation_start: extern "C" fn(*mut u8),
190     pub instantiation_end: extern "C" fn(*mut u8),
191 
192     /// The functions to start and stop performance timers/counters during Wasm
193     /// execution.
194     pub execution_timer: *mut u8,
195     pub execution_start: extern "C" fn(*mut u8),
196     pub execution_end: extern "C" fn(*mut u8),
197 
198     /// The (optional) flags to use when running Wasmtime. These correspond to
199     /// the flags used when running Wasmtime from the command line.
200     pub execution_flags_ptr: *const u8,
201     pub execution_flags_len: usize,
202 }
203 
204 impl WasmBenchConfig {
205     fn working_dir(&self) -> Result<PathBuf> {
206         let working_dir =
207             unsafe { std::slice::from_raw_parts(self.working_dir_ptr, self.working_dir_len) };
208         let working_dir = std::str::from_utf8(working_dir)
209             .context("given working directory is not valid UTF-8")?;
210         Ok(working_dir.into())
211     }
212 
213     fn stdout_path(&self) -> Result<PathBuf> {
214         let stdout_path =
215             unsafe { std::slice::from_raw_parts(self.stdout_path_ptr, self.stdout_path_len) };
216         let stdout_path =
217             std::str::from_utf8(stdout_path).context("given stdout path is not valid UTF-8")?;
218         Ok(stdout_path.into())
219     }
220 
221     fn stderr_path(&self) -> Result<PathBuf> {
222         let stderr_path =
223             unsafe { std::slice::from_raw_parts(self.stderr_path_ptr, self.stderr_path_len) };
224         let stderr_path =
225             std::str::from_utf8(stderr_path).context("given stderr path is not valid UTF-8")?;
226         Ok(stderr_path.into())
227     }
228 
229     fn stdin_path(&self) -> Result<Option<PathBuf>> {
230         if self.stdin_path_ptr.is_null() {
231             return Ok(None);
232         }
233 
234         let stdin_path =
235             unsafe { std::slice::from_raw_parts(self.stdin_path_ptr, self.stdin_path_len) };
236         let stdin_path =
237             std::str::from_utf8(stdin_path).context("given stdin path is not valid UTF-8")?;
238         Ok(Some(stdin_path.into()))
239     }
240 
241     fn execution_flags(&self) -> Result<Option<CommonOptions>> {
242         if self.execution_flags_ptr.is_null() {
243             return Ok(None);
244         }
245 
246         let execution_flags = unsafe {
247             std::slice::from_raw_parts(self.execution_flags_ptr, self.execution_flags_len)
248         };
249         let execution_flags = std::str::from_utf8(execution_flags)
250             .context("given execution flags string is not valid UTF-8")?;
251 
252         let options = CommonOptions::parse_from_str(execution_flags)?;
253         Ok(Some(options))
254     }
255 }
256 
257 /// Exposes a C-compatible way of creating the engine from the bytes of a single
258 /// Wasm module.
259 ///
260 /// On success, the `out_bench_ptr` is initialized to a pointer to a structure
261 /// that contains the engine's initialized state, and `0` is returned. On
262 /// failure, a non-zero status code is returned and `out_bench_ptr` is left
263 /// untouched.
264 #[no_mangle]
265 pub extern "C" fn wasm_bench_create(
266     config: WasmBenchConfig,
267     out_bench_ptr: *mut *mut c_void,
268 ) -> ExitCode {
269     let result = (|| -> Result<_> {
270         let working_dir = config.working_dir()?;
271         let working_dir =
272             cap_std::fs::Dir::open_ambient_dir(&working_dir, cap_std::ambient_authority())
273                 .with_context(|| {
274                     format!(
275                         "failed to preopen the working directory: {}",
276                         working_dir.display(),
277                     )
278                 })?;
279 
280         let stdout_path = config.stdout_path()?;
281         let stderr_path = config.stderr_path()?;
282         let stdin_path = config.stdin_path()?;
283         let options = config.execution_flags()?;
284 
285         let state = Box::new(BenchState::new(
286             options,
287             config.compilation_timer,
288             config.compilation_start,
289             config.compilation_end,
290             config.instantiation_timer,
291             config.instantiation_start,
292             config.instantiation_end,
293             config.execution_timer,
294             config.execution_start,
295             config.execution_end,
296             move || {
297                 let mut cx = WasiCtxBuilder::new();
298 
299                 let stdout = std::fs::File::create(&stdout_path)
300                     .with_context(|| format!("failed to create {}", stdout_path.display()))?;
301                 let stdout = cap_std::fs::File::from_std(stdout);
302                 let stdout = wasi_cap_std_sync::file::File::from_cap_std(stdout);
303                 cx = cx.stdout(Box::new(stdout));
304 
305                 let stderr = std::fs::File::create(&stderr_path)
306                     .with_context(|| format!("failed to create {}", stderr_path.display()))?;
307                 let stderr = cap_std::fs::File::from_std(stderr);
308                 let stderr = wasi_cap_std_sync::file::File::from_cap_std(stderr);
309                 cx = cx.stderr(Box::new(stderr));
310 
311                 if let Some(stdin_path) = &stdin_path {
312                     let stdin = std::fs::File::open(stdin_path)
313                         .with_context(|| format!("failed to open {}", stdin_path.display()))?;
314                     let stdin = cap_std::fs::File::from_std(stdin);
315                     let stdin = wasi_cap_std_sync::file::File::from_cap_std(stdin);
316                     cx = cx.stdin(Box::new(stdin));
317                 }
318 
319                 // Allow access to the working directory so that the benchmark can read
320                 // its input workload(s).
321                 cx = cx.preopened_dir(working_dir.try_clone()?, ".")?;
322 
323                 // Pass this env var along so that the benchmark program can use smaller
324                 // input workload(s) if it has them and that has been requested.
325                 if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") {
326                     cx = cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val)?;
327                 }
328 
329                 Ok(cx.build())
330             },
331         )?);
332         Ok(Box::into_raw(state) as _)
333     })();
334 
335     if let Ok(bench_ptr) = result {
336         unsafe {
337             assert!(!out_bench_ptr.is_null());
338             *out_bench_ptr = bench_ptr;
339         }
340     }
341 
342     to_exit_code(result.map(|_| ()))
343 }
344 
345 /// Free the engine state allocated by this library.
346 #[no_mangle]
347 pub extern "C" fn wasm_bench_free(state: *mut c_void) {
348     assert!(!state.is_null());
349     unsafe {
350         drop(Box::from_raw(state as *mut BenchState));
351     }
352 }
353 
354 /// Compile the Wasm benchmark module.
355 #[no_mangle]
356 pub extern "C" fn wasm_bench_compile(
357     state: *mut c_void,
358     wasm_bytes: *const u8,
359     wasm_bytes_length: usize,
360 ) -> ExitCode {
361     let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
362     let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) };
363     let result = state.compile(wasm_bytes).context("failed to compile");
364     to_exit_code(result)
365 }
366 
367 /// Instantiate the Wasm benchmark module.
368 #[no_mangle]
369 pub extern "C" fn wasm_bench_instantiate(state: *mut c_void) -> ExitCode {
370     let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
371     let result = state.instantiate().context("failed to instantiate");
372     to_exit_code(result)
373 }
374 
375 /// Execute the Wasm benchmark module.
376 #[no_mangle]
377 pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode {
378     let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
379     let result = state.execute().context("failed to execute");
380     to_exit_code(result)
381 }
382 
383 /// Helper function for converting a Rust result to a C error code.
384 ///
385 /// This will print an error indicating some information regarding the failure.
386 fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode {
387     match result.into() {
388         Ok(_) => OK,
389         Err(error) => {
390             eprintln!("{:?}", error);
391             ERR
392         }
393     }
394 }
395 
396 /// This structure contains the actual Rust implementation of the state required
397 /// to manage the Wasmtime engine between calls.
398 struct BenchState {
399     linker: Linker<HostState>,
400     compilation_timer: *mut u8,
401     compilation_start: extern "C" fn(*mut u8),
402     compilation_end: extern "C" fn(*mut u8),
403     instantiation_timer: *mut u8,
404     instantiation_start: extern "C" fn(*mut u8),
405     instantiation_end: extern "C" fn(*mut u8),
406     make_wasi_cx: Box<dyn FnMut() -> Result<WasiCtx>>,
407     module: Option<Module>,
408     store_and_instance: Option<(Store<HostState>, Instance)>,
409     epoch_interruption: bool,
410     fuel: Option<u64>,
411 }
412 
413 struct HostState {
414     wasi: WasiCtx,
415     #[cfg(feature = "wasi-nn")]
416     wasi_nn: wasmtime_wasi_nn::WasiNnCtx,
417     #[cfg(feature = "wasi-crypto")]
418     wasi_crypto: wasmtime_wasi_crypto::WasiCryptoCtx,
419 }
420 
421 impl BenchState {
422     fn new(
423         options: Option<CommonOptions>,
424         compilation_timer: *mut u8,
425         compilation_start: extern "C" fn(*mut u8),
426         compilation_end: extern "C" fn(*mut u8),
427         instantiation_timer: *mut u8,
428         instantiation_start: extern "C" fn(*mut u8),
429         instantiation_end: extern "C" fn(*mut u8),
430         execution_timer: *mut u8,
431         execution_start: extern "C" fn(*mut u8),
432         execution_end: extern "C" fn(*mut u8),
433         make_wasi_cx: impl FnMut() -> Result<WasiCtx> + 'static,
434     ) -> Result<Self> {
435         let config = if let Some(o) = &options {
436             o.config(Some(&Triple::host().to_string()))?
437         } else {
438             Config::new()
439         };
440         // NB: do not configure a code cache.
441         let engine = Engine::new(&config)?;
442         let mut linker = Linker::<HostState>::new(&engine);
443 
444         // Define the benchmarking start/end functions.
445         let execution_timer = unsafe {
446             // Safe because this bench API's contract requires that its methods
447             // are only ever called from a single thread.
448             UnsafeSendSync::new(execution_timer)
449         };
450         linker.func_wrap("bench", "start", move || {
451             execution_start(*execution_timer.get());
452             Ok(())
453         })?;
454         linker.func_wrap("bench", "end", move || {
455             execution_end(*execution_timer.get());
456             Ok(())
457         })?;
458 
459         let mut epoch_interruption = false;
460         let mut fuel = None;
461         if let Some(opts) = &options {
462             epoch_interruption = opts.epoch_interruption;
463             fuel = opts.fuel;
464         }
465 
466         let wasi_modules = options
467             .map(|o| o.wasi_modules)
468             .flatten()
469             .unwrap_or(WasiModules::default());
470 
471         if wasi_modules.wasi_common {
472             wasmtime_wasi::add_to_linker(&mut linker, |cx| &mut cx.wasi)?;
473         }
474 
475         #[cfg(feature = "wasi-nn")]
476         if wasi_modules.wasi_nn {
477             wasmtime_wasi_nn::add_to_linker(&mut linker, |cx| &mut cx.wasi_nn)?;
478         }
479 
480         #[cfg(feature = "wasi-crypto")]
481         if wasi_modules.wasi_crypto {
482             wasmtime_wasi_crypto::add_to_linker(&mut linker, |cx| &mut cx.wasi_crypto)?;
483         }
484 
485         Ok(Self {
486             linker,
487             compilation_timer,
488             compilation_start,
489             compilation_end,
490             instantiation_timer,
491             instantiation_start,
492             instantiation_end,
493             make_wasi_cx: Box::new(make_wasi_cx) as _,
494             module: None,
495             store_and_instance: None,
496             epoch_interruption,
497             fuel,
498         })
499     }
500 
501     fn compile(&mut self, bytes: &[u8]) -> Result<()> {
502         assert!(
503             self.module.is_none(),
504             "create a new engine to repeat compilation"
505         );
506 
507         (self.compilation_start)(self.compilation_timer);
508         let module = Module::from_binary(self.linker.engine(), bytes)?;
509         (self.compilation_end)(self.compilation_timer);
510 
511         self.module = Some(module);
512         Ok(())
513     }
514 
515     fn instantiate(&mut self) -> Result<()> {
516         let module = self
517             .module
518             .as_ref()
519             .expect("compile the module before instantiating it");
520 
521         let host = HostState {
522             wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?,
523             #[cfg(feature = "wasi-nn")]
524             wasi_nn: wasmtime_wasi_nn::WasiNnCtx::new()?,
525             #[cfg(feature = "wasi-crypto")]
526             wasi_crypto: wasmtime_wasi_nn::WasiCryptoCtx::new(),
527         };
528 
529         // NB: Start measuring instantiation time *after* we've created the WASI
530         // context, since that needs to do file I/O to setup
531         // stdin/stdout/stderr.
532         (self.instantiation_start)(self.instantiation_timer);
533         let mut store = Store::new(self.linker.engine(), host);
534         if self.epoch_interruption {
535             store.set_epoch_deadline(1);
536         }
537         if let Some(fuel) = self.fuel {
538             store.add_fuel(fuel).unwrap();
539         }
540 
541         let instance = self.linker.instantiate(&mut store, &module)?;
542         (self.instantiation_end)(self.instantiation_timer);
543 
544         self.store_and_instance = Some((store, instance));
545         Ok(())
546     }
547 
548     fn execute(&mut self) -> Result<()> {
549         let (mut store, instance) = self
550             .store_and_instance
551             .take()
552             .expect("instantiate the module before executing it");
553 
554         let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
555         match start_func.call(&mut store, ()) {
556             Ok(_) => Ok(()),
557             Err(trap) => {
558                 // Since _start will likely return by using the system `exit` call, we must
559                 // check the trap code to see if it actually represents a successful exit.
560                 if let Some(exit) = trap.downcast_ref::<I32Exit>() {
561                     if exit.0 == 0 {
562                         return Ok(());
563                     }
564                 }
565 
566                 Err(trap)
567             }
568         }
569     }
570 }
571