Skip to content

Commit b6fb64b

Browse files
authored
bench-api: add Wasm component support (#13215)
Detect Wasm components via `wasmtime::CodeHint` and route them through the appropriate component-model paths.
1 parent 5a1932b commit b6fb64b

2 files changed

Lines changed: 181 additions & 50 deletions

File tree

crates/bench-api/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ wasmtime-cli-flags = { workspace = true, default-features = true, features = [
2626
"cranelift",
2727
"gc",
2828
] }
29-
wasmtime-wasi = { workspace = true, features = ['p1'] }
29+
wasmtime-wasi = { workspace = true, features = ['p1', 'p2'] }
3030
wasmtime-wasi-nn = { workspace = true, optional = true }
3131
cap-std = { workspace = true }
3232
clap = { workspace = true }

crates/bench-api/src/lib.rs

Lines changed: 180 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,14 @@ use clap::Parser;
140140
use std::os::raw::{c_int, c_void};
141141
use std::slice;
142142
use std::{env, path::PathBuf};
143-
use wasmtime::{Engine, Instance, Linker, Module, Result, Store, error::Context as _};
143+
use wasmtime::component::ResourceTable;
144+
use wasmtime::{
145+
CodeBuilder, CodeHint, Engine, Instance, Linker, Module, Result, Store, error::Context as _,
146+
format_err,
147+
};
144148
use wasmtime_cli_flags::CommonOptions;
145149
use wasmtime_wasi::cli::{InputFile, OutputFile};
146-
use wasmtime_wasi::{DirPerms, FilePerms, I32Exit, WasiCtx, p1::WasiP1Ctx};
150+
use wasmtime_wasi::{DirPerms, FilePerms, I32Exit, WasiCtx, WasiCtxView, WasiView, p1::WasiP1Ctx};
147151

148152
pub type ExitCode = c_int;
149153
pub const OK: ExitCode = 0;
@@ -271,12 +275,19 @@ pub extern "C" fn wasm_bench_create(
271275
out_bench_ptr: *mut *mut c_void,
272276
) -> ExitCode {
273277
let result = (|| -> Result<_> {
278+
// Clone paths for the core Wasm WASI closure.
274279
let working_dir = config.working_dir()?;
275280
let stdout_path = config.stdout_path()?;
276281
let stderr_path = config.stderr_path()?;
277282
let stdin_path = config.stdin_path()?;
278283
let options = config.execution_flags()?;
279284

285+
// Clone paths for the component WASI closure.
286+
let working_dir2 = working_dir.clone();
287+
let stdout_path2 = stdout_path.clone();
288+
let stderr_path2 = stderr_path.clone();
289+
let stdin_path2 = stdin_path.clone();
290+
280291
let state = Box::new(BenchState::new(
281292
options,
282293
config.compilation_timer,
@@ -317,6 +328,34 @@ pub extern "C" fn wasm_bench_create(
317328

318329
Ok(cx.build_p1())
319330
},
331+
move || {
332+
let mut cx = WasiCtx::builder();
333+
334+
let stdout = std::fs::File::create(&stdout_path2)
335+
.with_context(|| format!("failed to create {}", stdout_path2.display()))?;
336+
cx.stdout(OutputFile::new(stdout));
337+
338+
let stderr = std::fs::File::create(&stderr_path2)
339+
.with_context(|| format!("failed to create {}", stderr_path2.display()))?;
340+
cx.stderr(OutputFile::new(stderr));
341+
342+
if let Some(stdin_path) = &stdin_path2 {
343+
let stdin = std::fs::File::open(stdin_path)
344+
.with_context(|| format!("failed to open {}", stdin_path.display()))?;
345+
cx.stdin(InputFile::new(stdin));
346+
}
347+
348+
cx.preopened_dir(working_dir2.clone(), ".", DirPerms::READ, FilePerms::READ)?;
349+
350+
if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") {
351+
cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val);
352+
}
353+
354+
Ok(ComponentHostState {
355+
wasi: cx.build(),
356+
table: ResourceTable::new(),
357+
})
358+
},
320359
)?);
321360
Ok(Box::into_raw(state) as _)
322361
})();
@@ -386,15 +425,20 @@ fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode {
386425
/// to manage the Wasmtime engine between calls.
387426
struct BenchState {
388427
linker: Linker<HostState>,
428+
component_linker: wasmtime::component::Linker<ComponentHostState>,
389429
compilation_timer: *mut u8,
390430
compilation_start: extern "C" fn(*mut u8),
391431
compilation_end: extern "C" fn(*mut u8),
392432
instantiation_timer: *mut u8,
393433
instantiation_start: extern "C" fn(*mut u8),
394434
instantiation_end: extern "C" fn(*mut u8),
395435
make_wasi_cx: Box<dyn FnMut() -> Result<WasiP1Ctx>>,
436+
make_component_wasi_cx: Box<dyn FnMut() -> Result<ComponentHostState>>,
396437
module: Option<Module>,
438+
component: Option<wasmtime::component::Component>,
397439
store_and_instance: Option<(Store<HostState>, Instance)>,
440+
component_store_and_instance:
441+
Option<(Store<ComponentHostState>, wasmtime::component::Instance)>,
398442
epoch_interruption: bool,
399443
fuel: Option<u64>,
400444
}
@@ -405,6 +449,20 @@ struct HostState {
405449
wasi_nn: wasmtime_wasi_nn::witx::WasiNnCtx,
406450
}
407451

452+
struct ComponentHostState {
453+
wasi: WasiCtx,
454+
table: ResourceTable,
455+
}
456+
457+
impl WasiView for ComponentHostState {
458+
fn ctx(&mut self) -> WasiCtxView<'_> {
459+
WasiCtxView {
460+
ctx: &mut self.wasi,
461+
table: &mut self.table,
462+
}
463+
}
464+
}
465+
408466
impl BenchState {
409467
fn new(
410468
mut options: CommonOptions,
@@ -418,12 +476,14 @@ impl BenchState {
418476
execution_start: extern "C" fn(*mut u8),
419477
execution_end: extern "C" fn(*mut u8),
420478
make_wasi_cx: impl FnMut() -> Result<WasiP1Ctx> + 'static,
479+
make_component_wasi_cx: impl FnMut() -> Result<ComponentHostState> + 'static,
421480
) -> Result<Self> {
422481
let mut config = options.config(None)?;
423482
// NB: always disable the compilation cache.
424483
config.cache(None);
425484
let engine = Engine::new(&config)?;
426485
let mut linker = Linker::<HostState>::new(&engine);
486+
let mut component_linker = wasmtime::component::Linker::<ComponentHostState>::new(&engine);
427487

428488
// Define the benchmarking start/end functions.
429489
let execution_timer = unsafe {
@@ -440,11 +500,29 @@ impl BenchState {
440500
Ok(())
441501
})?;
442502

503+
// Define the same benchmarking functions on the component linker.
504+
let mut bench_instance = component_linker.instance("bench")?;
505+
bench_instance.func_wrap(
506+
"start",
507+
move |_: wasmtime::StoreContextMut<'_, ComponentHostState>, (): ()| {
508+
execution_start(*execution_timer.get());
509+
Ok(())
510+
},
511+
)?;
512+
bench_instance.func_wrap(
513+
"end",
514+
move |_: wasmtime::StoreContextMut<'_, ComponentHostState>, (): ()| {
515+
execution_end(*execution_timer.get());
516+
Ok(())
517+
},
518+
)?;
519+
443520
let epoch_interruption = options.wasm.epoch_interruption.unwrap_or(false);
444521
let fuel = options.wasm.fuel;
445522

446523
if options.wasi.common != Some(false) {
447524
wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |cx| &mut cx.wasi)?;
525+
wasmtime_wasi::p2::add_to_linker_sync(&mut component_linker)?;
448526
}
449527

450528
#[cfg(feature = "wasi-nn")]
@@ -454,86 +532,139 @@ impl BenchState {
454532

455533
Ok(Self {
456534
linker,
535+
component_linker,
457536
compilation_timer,
458537
compilation_start,
459538
compilation_end,
460539
instantiation_timer,
461540
instantiation_start,
462541
instantiation_end,
463542
make_wasi_cx: Box::new(make_wasi_cx) as _,
543+
make_component_wasi_cx: Box::new(make_component_wasi_cx) as _,
464544
module: None,
545+
component: None,
465546
store_and_instance: None,
547+
component_store_and_instance: None,
466548
epoch_interruption,
467549
fuel,
468550
})
469551
}
470552

471553
fn compile(&mut self, bytes: &[u8]) -> Result<()> {
472554
self.module = None;
555+
self.component = None;
473556

474-
(self.compilation_start)(self.compilation_timer);
475-
let module = Module::from_binary(self.linker.engine(), bytes)?;
476-
(self.compilation_end)(self.compilation_timer);
557+
let mut builder = CodeBuilder::new(self.linker.engine());
558+
builder.wasm_binary(bytes, None)?;
559+
560+
match builder.hint() {
561+
Some(CodeHint::Component) => {
562+
(self.compilation_start)(self.compilation_timer);
563+
let component = builder.compile_component()?;
564+
(self.compilation_end)(self.compilation_timer);
565+
self.component = Some(component);
566+
}
567+
Some(CodeHint::Module) | None => {
568+
(self.compilation_start)(self.compilation_timer);
569+
let module = builder.compile_module()?;
570+
(self.compilation_end)(self.compilation_timer);
571+
self.module = Some(module);
572+
}
573+
}
477574

478-
self.module = Some(module);
479575
Ok(())
480576
}
481577

482578
fn instantiate(&mut self) -> Result<()> {
483579
self.store_and_instance = None;
580+
self.component_store_and_instance = None;
484581

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-
};
582+
if let Some(component) = &self.component {
583+
let host = (self.make_component_wasi_cx)()
584+
.context("failed to create a WASI context for component")?;
498585

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-
}
586+
// NB: Start measuring instantiation time *after* we've created the
587+
// WASI context, since that needs to do file I/O to setup
588+
// stdin/stdout/stderr.
589+
(self.instantiation_start)(self.instantiation_timer);
510590

511-
let instance = self.linker.instantiate(&mut store, &module)?;
512-
(self.instantiation_end)(self.instantiation_timer);
591+
let mut store = Store::new(self.component_linker.engine(), host);
592+
if self.epoch_interruption {
593+
store.set_epoch_deadline(1);
594+
}
595+
if let Some(fuel) = self.fuel {
596+
store.set_fuel(fuel).unwrap();
597+
}
513598

514-
self.store_and_instance = Some((store, instance));
599+
let instance = self.component_linker.instantiate(&mut store, component)?;
600+
601+
(self.instantiation_end)(self.instantiation_timer);
602+
603+
self.component_store_and_instance = Some((store, instance));
604+
} else {
605+
let module = self
606+
.module
607+
.as_ref()
608+
.expect("compile the module before instantiating it");
609+
610+
let host = HostState {
611+
wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?,
612+
#[cfg(feature = "wasi-nn")]
613+
wasi_nn: {
614+
let (backends, registry) = wasmtime_wasi_nn::preload(&[])?;
615+
wasmtime_wasi_nn::witx::WasiNnCtx::new(backends, registry)
616+
},
617+
};
618+
619+
// NB: Start measuring instantiation time *after* we've created the
620+
// WASI context, since that needs to do file I/O to setup
621+
// stdin/stdout/stderr.
622+
(self.instantiation_start)(self.instantiation_timer);
623+
624+
let mut store = Store::new(self.linker.engine(), host);
625+
if self.epoch_interruption {
626+
store.set_epoch_deadline(1);
627+
}
628+
if let Some(fuel) = self.fuel {
629+
store.set_fuel(fuel).unwrap();
630+
}
631+
632+
let instance = self.linker.instantiate(&mut store, &module)?;
633+
634+
(self.instantiation_end)(self.instantiation_timer);
635+
636+
self.store_and_instance = Some((store, instance));
637+
}
515638
Ok(())
516639
}
517640

518641
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(());
642+
if let Some((mut store, instance)) = self.component_store_and_instance.take() {
643+
let command = wasmtime_wasi::p2::bindings::sync::Command::new(&mut store, &instance)?;
644+
match command.wasi_cli_run().call_run(&mut store)? {
645+
Ok(()) => Ok(()),
646+
Err(()) => Err(format_err!("calling `run` failed")),
647+
}
648+
} else {
649+
let (mut store, instance) = self
650+
.store_and_instance
651+
.take()
652+
.expect("instantiate the module before executing it");
653+
654+
let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
655+
match start_func.call(&mut store, ()) {
656+
Ok(_) => Ok(()),
657+
Err(trap) => {
658+
// Since _start will likely return by using the system `exit` call, we must
659+
// check the trap code to see if it actually represents a successful exit.
660+
if let Some(exit) = trap.downcast_ref::<I32Exit>() {
661+
if exit.0 == 0 {
662+
return Ok(());
663+
}
533664
}
534-
}
535665

536-
Err(trap)
666+
Err(trap)
667+
}
537668
}
538669
}
539670
}

0 commit comments

Comments
 (0)