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 clap::Parser;
140 use std::os::raw::{c_int, c_void};
141 use std::slice;
142 use std::{env, path::PathBuf};
143 use wasmtime::{Engine, Instance, Linker, Module, Result, Store, error::Context as _};
144 use wasmtime_cli_flags::CommonOptions;
145 use wasmtime_wasi::cli::{InputFile, OutputFile};
146 use wasmtime_wasi::{DirPerms, FilePerms, I32Exit, WasiCtx, p1::WasiP1Ctx};
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 {
working_dir(&self) -> Result<PathBuf>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
stdout_path(&self) -> Result<PathBuf>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
stderr_path(&self) -> Result<PathBuf>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
stdin_path(&self) -> Result<Option<PathBuf>>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
execution_flags(&self) -> Result<CommonOptions>241 fn execution_flags(&self) -> Result<CommonOptions> {
242 let flags = if self.execution_flags_ptr.is_null() {
243 ""
244 } else {
245 let execution_flags = unsafe {
246 std::slice::from_raw_parts(self.execution_flags_ptr, self.execution_flags_len)
247 };
248 std::str::from_utf8(execution_flags)
249 .context("given execution flags string is not valid UTF-8")?
250 };
251 let options = CommonOptions::try_parse_from(
252 ["wasmtime"]
253 .into_iter()
254 .chain(flags.split(' ').filter(|s| !s.is_empty())),
255 )
256 .context("failed to parse options")?;
257 Ok(options)
258 }
259 }
260
261 /// Exposes a C-compatible way of creating the engine from the bytes of a single
262 /// Wasm module.
263 ///
264 /// On success, the `out_bench_ptr` is initialized to a pointer to a structure
265 /// that contains the engine's initialized state, and `0` is returned. On
266 /// failure, a non-zero status code is returned and `out_bench_ptr` is left
267 /// untouched.
268 #[unsafe(no_mangle)]
wasm_bench_create( config: WasmBenchConfig, out_bench_ptr: *mut *mut c_void, ) -> ExitCode269 pub extern "C" fn wasm_bench_create(
270 config: WasmBenchConfig,
271 out_bench_ptr: *mut *mut c_void,
272 ) -> ExitCode {
273 let result = (|| -> Result<_> {
274 let working_dir = config.working_dir()?;
275 let stdout_path = config.stdout_path()?;
276 let stderr_path = config.stderr_path()?;
277 let stdin_path = config.stdin_path()?;
278 let options = config.execution_flags()?;
279
280 let state = Box::new(BenchState::new(
281 options,
282 config.compilation_timer,
283 config.compilation_start,
284 config.compilation_end,
285 config.instantiation_timer,
286 config.instantiation_start,
287 config.instantiation_end,
288 config.execution_timer,
289 config.execution_start,
290 config.execution_end,
291 move || {
292 let mut cx = WasiCtx::builder();
293
294 let stdout = std::fs::File::create(&stdout_path)
295 .with_context(|| format!("failed to create {}", stdout_path.display()))?;
296 cx.stdout(OutputFile::new(stdout));
297
298 let stderr = std::fs::File::create(&stderr_path)
299 .with_context(|| format!("failed to create {}", stderr_path.display()))?;
300 cx.stderr(OutputFile::new(stderr));
301
302 if let Some(stdin_path) = &stdin_path {
303 let stdin = std::fs::File::open(stdin_path)
304 .with_context(|| format!("failed to open {}", stdin_path.display()))?;
305 cx.stdin(InputFile::new(stdin));
306 }
307
308 // Allow access to the working directory so that the benchmark can read
309 // its input workload(s).
310 cx.preopened_dir(working_dir.clone(), ".", DirPerms::READ, FilePerms::READ)?;
311
312 // Pass this env var along so that the benchmark program can use smaller
313 // input workload(s) if it has them and that has been requested.
314 if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") {
315 cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val);
316 }
317
318 Ok(cx.build_p1())
319 },
320 )?);
321 Ok(Box::into_raw(state) as _)
322 })();
323
324 if let Ok(bench_ptr) = result {
325 unsafe {
326 assert!(!out_bench_ptr.is_null());
327 *out_bench_ptr = bench_ptr;
328 }
329 }
330
331 to_exit_code(result.map(|_| ()))
332 }
333
334 /// Free the engine state allocated by this library.
335 #[unsafe(no_mangle)]
wasm_bench_free(state: *mut c_void)336 pub extern "C" fn wasm_bench_free(state: *mut c_void) {
337 assert!(!state.is_null());
338 unsafe {
339 drop(Box::from_raw(state as *mut BenchState));
340 }
341 }
342
343 /// Compile the Wasm benchmark module.
344 #[unsafe(no_mangle)]
wasm_bench_compile( state: *mut c_void, wasm_bytes: *const u8, wasm_bytes_length: usize, ) -> ExitCode345 pub extern "C" fn wasm_bench_compile(
346 state: *mut c_void,
347 wasm_bytes: *const u8,
348 wasm_bytes_length: usize,
349 ) -> ExitCode {
350 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
351 let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) };
352 let result = state.compile(wasm_bytes).context("failed to compile");
353 to_exit_code(result)
354 }
355
356 /// Instantiate the Wasm benchmark module.
357 #[unsafe(no_mangle)]
wasm_bench_instantiate(state: *mut c_void) -> ExitCode358 pub extern "C" fn wasm_bench_instantiate(state: *mut c_void) -> ExitCode {
359 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
360 let result = state.instantiate().context("failed to instantiate");
361 to_exit_code(result)
362 }
363
364 /// Execute the Wasm benchmark module.
365 #[unsafe(no_mangle)]
wasm_bench_execute(state: *mut c_void) -> ExitCode366 pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode {
367 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
368 let result = state.execute().context("failed to execute");
369 to_exit_code(result)
370 }
371
372 /// Helper function for converting a Rust result to a C error code.
373 ///
374 /// This will print an error indicating some information regarding the failure.
to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode375 fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode {
376 match result.into() {
377 Ok(_) => OK,
378 Err(error) => {
379 eprintln!("{error:?}");
380 ERR
381 }
382 }
383 }
384
385 /// This structure contains the actual Rust implementation of the state required
386 /// to manage the Wasmtime engine between calls.
387 struct BenchState {
388 linker: Linker<HostState>,
389 compilation_timer: *mut u8,
390 compilation_start: extern "C" fn(*mut u8),
391 compilation_end: extern "C" fn(*mut u8),
392 instantiation_timer: *mut u8,
393 instantiation_start: extern "C" fn(*mut u8),
394 instantiation_end: extern "C" fn(*mut u8),
395 make_wasi_cx: Box<dyn FnMut() -> Result<WasiP1Ctx>>,
396 module: Option<Module>,
397 store_and_instance: Option<(Store<HostState>, Instance)>,
398 epoch_interruption: bool,
399 fuel: Option<u64>,
400 }
401
402 struct HostState {
403 wasi: WasiP1Ctx,
404 #[cfg(feature = "wasi-nn")]
405 wasi_nn: wasmtime_wasi_nn::witx::WasiNnCtx,
406 }
407
408 impl BenchState {
new( mut options: CommonOptions, compilation_timer: *mut u8, compilation_start: extern "C" fn(*mut u8), compilation_end: extern "C" fn(*mut u8), instantiation_timer: *mut u8, instantiation_start: extern "C" fn(*mut u8), instantiation_end: extern "C" fn(*mut u8), execution_timer: *mut u8, execution_start: extern "C" fn(*mut u8), execution_end: extern "C" fn(*mut u8), make_wasi_cx: impl FnMut() -> Result<WasiP1Ctx> + 'static, ) -> Result<Self>409 fn new(
410 mut options: CommonOptions,
411 compilation_timer: *mut u8,
412 compilation_start: extern "C" fn(*mut u8),
413 compilation_end: extern "C" fn(*mut u8),
414 instantiation_timer: *mut u8,
415 instantiation_start: extern "C" fn(*mut u8),
416 instantiation_end: extern "C" fn(*mut u8),
417 execution_timer: *mut u8,
418 execution_start: extern "C" fn(*mut u8),
419 execution_end: extern "C" fn(*mut u8),
420 make_wasi_cx: impl FnMut() -> Result<WasiP1Ctx> + 'static,
421 ) -> Result<Self> {
422 let mut config = options.config(None)?;
423 // NB: always disable the compilation cache.
424 config.cache(None);
425 let engine = Engine::new(&config)?;
426 let mut linker = Linker::<HostState>::new(&engine);
427
428 // Define the benchmarking start/end functions.
429 let execution_timer = unsafe {
430 // Safe because this bench API's contract requires that its methods
431 // are only ever called from a single thread.
432 UnsafeSendSync::new(execution_timer)
433 };
434 linker.func_wrap("bench", "start", move || {
435 execution_start(*execution_timer.get());
436 Ok(())
437 })?;
438 linker.func_wrap("bench", "end", move || {
439 execution_end(*execution_timer.get());
440 Ok(())
441 })?;
442
443 let epoch_interruption = options.wasm.epoch_interruption.unwrap_or(false);
444 let fuel = options.wasm.fuel;
445
446 if options.wasi.common != Some(false) {
447 wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |cx| &mut cx.wasi)?;
448 }
449
450 #[cfg(feature = "wasi-nn")]
451 if options.wasi.nn == Some(true) {
452 wasmtime_wasi_nn::witx::add_to_linker(&mut linker, |cx| &mut cx.wasi_nn)?;
453 }
454
455 Ok(Self {
456 linker,
457 compilation_timer,
458 compilation_start,
459 compilation_end,
460 instantiation_timer,
461 instantiation_start,
462 instantiation_end,
463 make_wasi_cx: Box::new(make_wasi_cx) as _,
464 module: None,
465 store_and_instance: None,
466 epoch_interruption,
467 fuel,
468 })
469 }
470
compile(&mut self, bytes: &[u8]) -> Result<()>471 fn compile(&mut self, bytes: &[u8]) -> Result<()> {
472 self.module = None;
473
474 (self.compilation_start)(self.compilation_timer);
475 let module = Module::from_binary(self.linker.engine(), bytes)?;
476 (self.compilation_end)(self.compilation_timer);
477
478 self.module = Some(module);
479 Ok(())
480 }
481
instantiate(&mut self) -> Result<()>482 fn instantiate(&mut self) -> Result<()> {
483 self.store_and_instance = None;
484
485 let module = self
486 .module
487 .as_ref()
488 .expect("compile the module before instantiating it");
489
490 let host = HostState {
491 wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?,
492 #[cfg(feature = "wasi-nn")]
493 wasi_nn: {
494 let (backends, registry) = wasmtime_wasi_nn::preload(&[])?;
495 wasmtime_wasi_nn::witx::WasiNnCtx::new(backends, registry)
496 },
497 };
498
499 // NB: Start measuring instantiation time *after* we've created the WASI
500 // context, since that needs to do file I/O to setup
501 // stdin/stdout/stderr.
502 (self.instantiation_start)(self.instantiation_timer);
503 let mut store = Store::new(self.linker.engine(), host);
504 if self.epoch_interruption {
505 store.set_epoch_deadline(1);
506 }
507 if let Some(fuel) = self.fuel {
508 store.set_fuel(fuel).unwrap();
509 }
510
511 let instance = self.linker.instantiate(&mut store, &module)?;
512 (self.instantiation_end)(self.instantiation_timer);
513
514 self.store_and_instance = Some((store, instance));
515 Ok(())
516 }
517
execute(&mut self) -> Result<()>518 fn execute(&mut self) -> Result<()> {
519 let (mut store, instance) = self
520 .store_and_instance
521 .take()
522 .expect("instantiate the module before executing it");
523
524 let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
525 match start_func.call(&mut store, ()) {
526 Ok(_) => Ok(()),
527 Err(trap) => {
528 // Since _start will likely return by using the system `exit` call, we must
529 // check the trap code to see if it actually represents a successful exit.
530 if let Some(exit) = trap.downcast_ref::<I32Exit>() {
531 if exit.0 == 0 {
532 return Ok(());
533 }
534 }
535
536 Err(trap)
537 }
538 }
539 }
540 }
541