From 7e8a5b0713f72d592c8a98d783dead0088718ded Mon Sep 17 00:00:00 2001 From: James Sturtevant Date: Tue, 8 Jul 2025 21:42:15 +0000 Subject: [PATCH] Add c Component benchmark Signed-off-by: James Sturtevant --- .devcontainer/Dockerfile | 3 +- .github/workflows/dep_build_wasm_examples.yml | 13 +-- .github/workflows/dep_rust.yml | 1 + .gitignore | 1 + Cargo.lock | 16 --- Justfile | 25 ++-- src/hyperlight_wasm/Cargo.toml | 5 + .../benches/benchmarks_components.rs | 110 ++++++++++++++++++ .../scripts/build-wasm-examples.sh | 53 ++++++++- src/wasmsamples/compile-wasm.bat | 16 +++ src/wasmsamples/components/runcomponent.c | 11 ++ src/wasmsamples/components/runcomponent.wit | 14 +++ 12 files changed, 231 insertions(+), 37 deletions(-) create mode 100644 src/hyperlight_wasm/benches/benchmarks_components.rs create mode 100644 src/wasmsamples/components/runcomponent.c create mode 100644 src/wasmsamples/components/runcomponent.wit diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9c28773..9eb6f80 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -35,4 +35,5 @@ RUN rustup default ${RUST_TOOLCHAIN} \ && cargo install --locked wasm-tools \ && cargo install wkg \ && cargo install wac-cli \ - && cargo install cargo-component --locked + && cargo install cargo-component --locked \ + && cargo install wit-bindgen-cli --locked diff --git a/.github/workflows/dep_build_wasm_examples.yml b/.github/workflows/dep_build_wasm_examples.yml index 5b99316..e6a949c 100644 --- a/.github/workflows/dep_build_wasm_examples.yml +++ b/.github/workflows/dep_build_wasm_examples.yml @@ -78,13 +78,8 @@ jobs: cache-to: ${{ env.CACHE_TO }} - name: Build Modules run: | - for FILENAME in $(find . -name '*.c') - do - echo Building ${FILENAME} - docker run --rm -i -v "${PWD}:/tmp/host" ghcr.io/${{ github.repository_owner }}/wasm-clang-builder:latest /opt/wasi-sdk/bin/clang -flto -ffunction-sections -mexec-model=reactor -O3 -z stack-size=4096 -Wl,--initial-memory=65536 -Wl,--export=__data_end -Wl,--export=__heap_base,--export=malloc,--export=free,--export=__wasm_call_ctors -Wl,--strip-all,--no-entry -Wl,--allow-undefined -Wl,--gc-sections -o /tmp/host/${FILENAME%.*}-wasi-libc.wasm /tmp/host/${FILENAME} - cargo run -p hyperlight-wasm-aot compile ${FILENAME%.*}-wasi-libc.wasm ${FILENAME%.*}.aot - cp ${FILENAME%.*}.aot ${FILENAME%.*}.wasm - done + just ensure-tools + just build-wasm-examples release shell: bash working-directory: src/wasmsamples - name: Upload Wasm Modules @@ -92,5 +87,5 @@ jobs: with: name: guest-modules path: | - src/wasmsamples/*.wasm - src/wasmsamples/*.aot + x64/release/*.wasm + x64/release/*.aot diff --git a/.github/workflows/dep_rust.yml b/.github/workflows/dep_rust.yml index 2bca5c9..926bc28 100644 --- a/.github/workflows/dep_rust.yml +++ b/.github/workflows/dep_rust.yml @@ -82,6 +82,7 @@ jobs: # this must be build before the formatting and other jobs run # because the component model example depends on the wasm component built here just ensure-tools + just compile-wit just build-rust-component-examples ${{ matrix.config }} - name: Fmt diff --git a/.gitignore b/.gitignore index 5cb9465..b99517d 100644 --- a/.gitignore +++ b/.gitignore @@ -479,3 +479,4 @@ target/ # MSVC Windows builds of rustc generate these, which store debugging information *.pdb src/component_sample/**/*.wasm +src/wasmsamples/components/bindings/ diff --git a/Cargo.lock b/Cargo.lock index afa1e89..9ecc42f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,22 +338,6 @@ dependencies = [ "url", ] -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63d2780ac94487eb9f1fea7b0d56300abc9eb488800854ca217f102f5caccca" -dependencies = [ - "semver", - "serde", - "serde-untagged", - "serde-value", - "thiserror 1.0.69", - "toml", - "unicode-xid", - "url", -] - [[package]] name = "cargo_metadata" version = "0.20.0" diff --git a/Justfile b/Justfile index 209b2c0..4d2840f 100644 --- a/Justfile +++ b/Justfile @@ -4,6 +4,7 @@ build-wasm-examples-command := if os() == "windows" { "./src/hyperlight_wasm/scr mkdir-arg := if os() == "windows" { "-Force" } else { "-p" } latest-release:= if os() == "windows" {"$(git tag -l --sort=v:refname | select -last 2 | select -first 1)"} else {`git tag -l --sort=v:refname | tail -n 2 | head -n 1`} wit-world := if os() == "windows" { "$env:WIT_WORLD=\"" + justfile_directory() + "\\src\\component_sample\\wit\\component-world.wasm" + "\";" } else { "WIT_WORLD=" + justfile_directory() + "/src/component_sample/wit/component-world.wasm" } +wit-world-c := if os() == "windows" { "$env:WIT_WORLD=\"" + justfile_directory() + "\\src\\wasmsamples\\components\\runcomponent-world.wasm" + "\";" } else { "WIT_WORLD=" + justfile_directory() + "/src/wasmsamples/components/runcomponent-world.wasm" } set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"] @@ -14,8 +15,9 @@ make-vendor-tar: -C ./src wasm_runtime hyperlight_wasm_macro ensure-tools: - cargo install --locked wasm-tools --version 1.235.0 + cargo install wasm-tools --locked --version 1.235.0 cargo install cargo-component --locked --version 0.21.1 + cargo install wit-bindgen-cli --locked --version 0.43.0 build-all target=default-target: (build target) (build-wasm-examples target) (build-rust-wasm-examples target) (build-wasm-runtime target) (build-rust-component-examples target) @@ -26,10 +28,14 @@ mkdir-redist target=default-target: mkdir {{ mkdir-arg }} x64 mkdir {{ mkdir-arg }} x64/{{ target }} +compile-wit: + wasm-tools component wit ./src/wasmsamples/components/runcomponent.wit -w -o ./src/wasmsamples/components/runcomponent-world.wasm + wasm-tools component wit ./src/component_sample/wit/example.wit -w -o ./src/component_sample/wit/component-world.wasm + build-wasm-runtime target=default-target: cd ./src/wasm_runtime && cargo build --verbose --profile={{ if target == "debug" {"dev"} else { target } }} && rm -R target -build-wasm-examples target=default-target: +build-wasm-examples target=default-target: (compile-wit) {{ build-wasm-examples-command }} {{target}} build-rust-wasm-examples target=default-target: (mkdir-redist target) @@ -38,8 +44,7 @@ build-rust-wasm-examples target=default-target: (mkdir-redist target) cargo run -p hyperlight-wasm-aot compile ./src/rust_wasm_samples/target/wasm32-unknown-unknown/{{ target }}/rust_wasm_samples.wasm ./x64/{{ target }}/rust_wasm_samples.aot cp ./x64/{{ target }}/rust_wasm_samples.aot ./x64/{{ target }}/rust_wasm_samples.wasm -build-rust-component-examples target=default-target: - wasm-tools component wit ./src/component_sample/wit/example.wit -w -o ./src/component_sample/wit/component-world.wasm +build-rust-component-examples target=default-target: (compile-wit) # use cargo component so we don't get all the wasi imports https://github.com/bytecodealliance/cargo-component?tab=readme-ov-file#relationship-with-wasm32-wasip2 # we also explicitly target wasm32-unknown-unknown since cargo component might try to pull in wasi imports https://github.com/bytecodealliance/cargo-component/issues/290 rustup target add wasm32-unknown-unknown @@ -101,10 +106,14 @@ examples-components target=default-target features="": (build-rust-component-exa {{ wit-world }} cargo run {{ if features =="" {''} else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" {"dev"} else { target } }} --example component_example # warning, compares to and then OVERWRITES the given baseline -bench-ci baseline target=default-target features="": - cd src/hyperlight_wasm && cargo bench --profile={{ if target == "debug" {"dev"} else { target } }} {{ if features =="" {''} else { "--features " + features } }} -- --verbose --save-baseline {{baseline}} -bench target=default-target features="": - cd src/hyperlight_wasm && cargo bench --profile={{ if target == "debug" {"dev"} else { target } }} {{ if features =="" {''} else { "--features " + features } }} -- --verbose +bench-ci baseline target="release" features="": + cd src/hyperlight_wasm && cargo bench --profile={{ if target == "debug" {"dev"} else { target } }} {{ if features =="" {''} else { "--features " + features } }} --bench benchmarks -- --verbose --save-baseline {{baseline}} + cd src/hyperlight_wasm; {{wit-world-c}} cargo bench --profile={{ if target == "debug" {"dev"} else { target } }} {{ if features =="" {''} else { "--features " + features } }} --bench benchmarks_components -- --verbose --save-baseline {{baseline}}-components +bench target="release" features="": (bench-wasm target features) (bench-components target features) +bench-wasm target="release" features="": + cd src/hyperlight_wasm && cargo bench --profile={{ if target == "debug" {"dev"} else { target } }} {{ if features =="" {''} else { "--features " + features } }} --bench benchmarks -- --verbose +bench-components target="release" features="": + cd src/hyperlight_wasm; {{wit-world-c}} cargo bench --profile={{ if target == "debug" {"dev"} else { target } }} {{ if features =="" {''} else { "--features " + features } }} --bench benchmarks_components -- --verbose bench-download os hypervisor cpu tag="": gh release download {{ tag }} -D ./src/hyperlight_wasm/target/ -p benchmarks_{{ os }}_{{ hypervisor }}_{{ cpu }}.tar.gz mkdir {{ mkdir-arg }} ./src/hyperlight_wasm/target/criterion diff --git a/src/hyperlight_wasm/Cargo.toml b/src/hyperlight_wasm/Cargo.toml index 96e988d..277a034 100644 --- a/src/hyperlight_wasm/Cargo.toml +++ b/src/hyperlight_wasm/Cargo.toml @@ -88,3 +88,8 @@ mshv3 = ["hyperlight-host/mshv3"] [[bench]] name = "benchmarks" harness = false + +[[bench]] +name = "benchmarks_components" +harness = false + diff --git a/src/hyperlight_wasm/benches/benchmarks_components.rs b/src/hyperlight_wasm/benches/benchmarks_components.rs new file mode 100644 index 0000000..34d7e8c --- /dev/null +++ b/src/hyperlight_wasm/benches/benchmarks_components.rs @@ -0,0 +1,110 @@ +use std::sync::{Arc, Mutex}; + +use criterion::{Bencher, Criterion, criterion_group, criterion_main}; +use hyperlight_wasm::{LoadedWasmSandbox, SandboxBuilder}; + +use crate::bindings::example::runcomponent::Guest; + +extern crate alloc; +mod bindings { + hyperlight_component_macro::host_bindgen!( + "../../src/wasmsamples/components/runcomponent-world.wasm" + ); +} + +pub struct State {} +impl State { + pub fn new() -> Self { + State {} + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +impl bindings::example::runcomponent::Host for State { + fn r#get_time_since_boot_microsecond(&mut self) -> i64 { + let res = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap() + .as_micros(); + i64::try_from(res).unwrap() + } +} + +impl bindings::example::runcomponent::RuncomponentImports for State { + type Host = State; + + fn r#host(&mut self) -> impl ::core::borrow::BorrowMut { + self + } +} + +fn wasm_component_guest_call_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("wasm_component_guest_functions"); + + let bench_guest_function = |b: &mut Bencher<'_>, ext| { + let (sb, rt) = get_loaded_wasm_sandbox(ext); + let mut wrapped = bindings::RuncomponentSandbox { sb, rt }; + let instance = bindings::example::runcomponent::RuncomponentExports::guest(&mut wrapped); + + b.iter(|| { + instance.echo("Hello World!".to_string()); + }); + }; + + group.bench_function("wasm_guest_call", |b: &mut Bencher<'_>| { + bench_guest_function(b, "wasm"); + }); + + group.bench_function("wasm_guest_call_aot", |b: &mut Bencher<'_>| { + bench_guest_function(b, "aot"); + }); + + group.finish(); +} + +fn wasm_component_sandbox_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("wasm_component_sandboxes"); + let create_wasm_sandbox = || { + get_loaded_wasm_sandbox("wasm"); + }; + + group.bench_function("create_sandbox", |b| { + b.iter_with_large_drop(create_wasm_sandbox); + }); + + group.bench_function("create_sandbox_and_drop", |b| { + b.iter(create_wasm_sandbox); + }); + + group.finish(); +} + +fn get_loaded_wasm_sandbox( + ext: &str, +) -> ( + LoadedWasmSandbox, + Arc>>, +) { + let state = State::new(); + let mut sandbox = SandboxBuilder::new().build().unwrap(); + let rt = bindings::register_host_functions(&mut sandbox, state); + + let sb = sandbox.load_runtime().unwrap(); + + let sb = sb + .load_module(format!("../../x64/release/runcomponent.{ext}",)) + .unwrap(); + (sb, rt) +} + +criterion_group! { + name = benches_components; + config = Criterion::default();//.warm_up_time(Duration::from_millis(50)); // If warm_up_time is default 3s warmup, the benchmark will fail due memory error + targets = wasm_component_guest_call_benchmark, wasm_component_sandbox_benchmark +} +criterion_main!(benches_components); diff --git a/src/hyperlight_wasm/scripts/build-wasm-examples.sh b/src/hyperlight_wasm/scripts/build-wasm-examples.sh index a14a5d2..e926358 100755 --- a/src/hyperlight_wasm/scripts/build-wasm-examples.sh +++ b/src/hyperlight_wasm/scripts/build-wasm-examples.sh @@ -12,7 +12,7 @@ OUTPUT_DIR=$(realpath $OUTPUT_DIR) if [ -f "/.dockerenv" ] || grep -q docker /proc/1/cgroup; then # running in a container so use the installed wasi-sdk as the devcontainer has this installed - for FILENAME in $(find . -name '*.c') + for FILENAME in $(find . -name '*.c' -not -path './components/*') do echo Building ${FILENAME} # Build the wasm file with wasi-libc for wasmtime @@ -23,6 +23,29 @@ if [ -f "/.dockerenv" ] || grep -q docker /proc/1/cgroup; then cargo run -p hyperlight-wasm-aot compile ${OUTPUT_DIR}/${FILENAME%.*}-wasi-libc.wasm ${OUTPUT_DIR}/${FILENAME%.*}.aot cp ${OUTPUT_DIR}/${FILENAME%.*}.aot ${OUTPUT_DIR}/${FILENAME%.*}.wasm done + + for WIT_FILE in ${PWD}/components/*.wit; do + COMPONENT_NAME=$(basename ${WIT_FILE} .wit) + echo Building component: ${COMPONENT_NAME} + + # Generate bindings for the component + wit-bindgen c ${WIT_FILE} --out-dir ${PWD}/components/bindings + + # Build the wasm file with wasi-libc for wasmtime + /opt/wasi-sdk/bin/wasm32-wasip2-clang \ + -ffunction-sections -mexec-model=reactor -O3 -z stack-size=4096 \ + -Wl,--initial-memory=65536 -Wl,--export=__data_end -Wl,--export=__heap_base,--export=malloc,--export=free,--export=__wasm_call_ctors \ + -Wl,--strip-all,--no-entry -Wl,--allow-undefined -Wl,--gc-sections \ + -o ${OUTPUT_DIR}/${COMPONENT_NAME}-p2.wasm \ + ${PWD}/components/${COMPONENT_NAME}.c \ + ${PWD}/components/bindings/${COMPONENT_NAME}.c \ + ${PWD}/components/bindings/${COMPONENT_NAME}_component_type.o + + # Build AOT for Wasmtime + cargo run -p hyperlight-wasm-aot compile --component ${OUTPUT_DIR}/${COMPONENT_NAME}-p2.wasm ${OUTPUT_DIR}/${COMPONENT_NAME}.aot + cp ${OUTPUT_DIR}/${COMPONENT_NAME}.aot ${OUTPUT_DIR}/${COMPONENT_NAME}.wasm + done + else # not running in a container so use the docker image to build the wasm files echo Building docker image that has Wasm sdk. Should be quick if preivoulsy built and no changes to dockerfile. @@ -33,17 +56,41 @@ else docker build --build-arg GCC_VERSION=12 --build-arg WASI_SDK_VERSION_FULL=25.0 --cache-from ghcr.io/hyperlight-dev/wasm-clang-builder:latest -t wasm-clang-builder:latest . 2> ${OUTPUT_DIR}/dockerbuild.log - for FILENAME in $(find . -name '*.c') + for FILENAME in $(find . -name '*.c' -not -path './components/*') do echo Building ${FILENAME} # Build the wasm file with wasi-libc for wasmtime - docker run --rm -i -v "${PWD}:/tmp/host" -v "${OUTPUT_DIR}:/tmp/output" wasm-clang-builder:latest /opt/wasi-sdk/bin/clang -flto -ffunction-sections -mexec-model=reactor -O3 -z stack-size=4096 -Wl,--initial-memory=65536 -Wl,--export=__data_end -Wl,--export=__heap_base,--export=malloc,--export=free,--export=__wasm_call_ctors -Wl,--strip-all,--no-entry -Wl,--allow-undefined -Wl,--gc-sections -o /tmp/output/${FILENAME%.*}-wasi-libc.wasm /tmp/host/${FILENAME} + docker run --rm -i -v "${PWD}:/tmp/host" -v "${OUTPUT_DIR}:/tmp/output/" wasm-clang-builder:latest /opt/wasi-sdk/bin/clang -flto -ffunction-sections -mexec-model=reactor -O3 -z stack-size=4096 -Wl,--initial-memory=65536 -Wl,--export=__data_end -Wl,--export=__heap_base,--export=malloc,--export=free,--export=__wasm_call_ctors -Wl,--strip-all,--no-entry -Wl,--allow-undefined -Wl,--gc-sections -o /tmp/output/${FILENAME%.*}-wasi-libc.wasm /tmp/host/${FILENAME} # Build AOT for Wasmtime; note that Wasmtime does not support # interpreting, so its wasm binary is secretly an AOT binary. cargo run -p hyperlight-wasm-aot compile ${OUTPUT_DIR}/${FILENAME%.*}-wasi-libc.wasm ${OUTPUT_DIR}/${FILENAME%.*}.aot cp ${OUTPUT_DIR}/${FILENAME%.*}.aot ${OUTPUT_DIR}/${FILENAME%.*}.wasm done + + echo Building components + # Iterate over all .wit files in the components folder + for WIT_FILE in ${PWD}/components/*.wit; do + COMPONENT_NAME=$(basename ${WIT_FILE} .wit) + echo Building component: ${COMPONENT_NAME} + + # Generate bindings for the component + wit-bindgen c ${WIT_FILE} --out-dir ${PWD}/components/bindings + + # Build the wasm file with wasi-libc for wasmtime + docker run --rm -i -v "${PWD}:/tmp/host" -v "${OUTPUT_DIR}:/tmp/output/" wasm-clang-builder:latest /opt/wasi-sdk/bin/wasm32-wasip2-clang \ + -ffunction-sections -mexec-model=reactor -O3 -z stack-size=4096 \ + -Wl,--initial-memory=65536 -Wl,--export=__data_end -Wl,--export=__heap_base,--export=malloc,--export=free,--export=__wasm_call_ctors \ + -Wl,--strip-all,--no-entry -Wl,--allow-undefined -Wl,--gc-sections \ + -o /tmp/output/${COMPONENT_NAME}-p2.wasm \ + /tmp/host/components/${COMPONENT_NAME}.c \ + /tmp/host/components/bindings/${COMPONENT_NAME}.c \ + /tmp/host/components/bindings/${COMPONENT_NAME}_component_type.o + + # Build AOT for Wasmtime + cargo run -p hyperlight-wasm-aot compile --component ${OUTPUT_DIR}/${COMPONENT_NAME}-p2.wasm ${OUTPUT_DIR}/${COMPONENT_NAME}.aot + cp ${OUTPUT_DIR}/${COMPONENT_NAME}.aot ${OUTPUT_DIR}/${COMPONENT_NAME}.wasm + done fi popd diff --git a/src/wasmsamples/compile-wasm.bat b/src/wasmsamples/compile-wasm.bat index e4c7d10..8b9fd44 100644 --- a/src/wasmsamples/compile-wasm.bat +++ b/src/wasmsamples/compile-wasm.bat @@ -31,6 +31,22 @@ for /R "%1" %%i in (*.c) do ( copy %2\%%~ni.aot %2\%%~ni.wasm ) +echo Building components +for %%j in (%~1\components\*.wit) do ( + set "COMPONENT_NAME=%%~nj" + echo Building component: !COMPONENT_NAME! + + rem Generate bindings for the component + wit-bindgen c %%j --out-dir %~1\components\bindings + + rem Build the wasm file with wasi-libc for wasmtime + %dockercmd% run --rm -i -v !dockerinput!:/tmp/host1 -v !dockeroutput!:/tmp/host2 wasm-clang-builder /opt/wasi-sdk/bin/wasm32-wasip2-clang -ffunction-sections -mexec-model=reactor -O3 -z stack-size=4096 -Wl,--initial-memory=65536 -Wl,--export=__data_end -Wl,--export=__heap_base,--export=malloc,--export=free,--export=__wasm_call_ctors -Wl,--strip-all,--no-entry -Wl,--allow-undefined -Wl,--gc-sections -o /tmp/host2/!COMPONENT_NAME!-p2.wasm /tmp/host1/components/!COMPONENT_NAME!.c /tmp/host1/components/bindings/!COMPONENT_NAME!.c /tmp/host1/components/bindings/!COMPONENT_NAME!_component_type.o + + rem Build AOT for Wasmtime + cargo run -p hyperlight-wasm-aot compile --component %2\!COMPONENT_NAME!-p2.wasm %2\!COMPONENT_NAME!.aot + copy %2\!COMPONENT_NAME!.aot %2\!COMPONENT_NAME!.wasm +) + goto :EOF :Error echo Usage - compile-wasm ^ ^ diff --git a/src/wasmsamples/components/runcomponent.c b/src/wasmsamples/components/runcomponent.c new file mode 100644 index 0000000..0d3d43e --- /dev/null +++ b/src/wasmsamples/components/runcomponent.c @@ -0,0 +1,11 @@ +#include "bindings/runcomponent.h" +#include +#include + +void exports_example_runcomponent_guest_echo(runcomponent_string_t *msg, runcomponent_string_t *ret) +{ + ret->len = msg->len; + ret->ptr = (uint8_t *) malloc(ret->len); + memcpy(ret->ptr, msg->ptr, ret->len); + runcomponent_string_free(msg); +} \ No newline at end of file diff --git a/src/wasmsamples/components/runcomponent.wit b/src/wasmsamples/components/runcomponent.wit new file mode 100644 index 0000000..941fcbf --- /dev/null +++ b/src/wasmsamples/components/runcomponent.wit @@ -0,0 +1,14 @@ +package example:runcomponent; + +interface guest { + echo: func(msg: string) -> string; +} + +interface host { + get-time-since-boot-microsecond: func() -> s64; +} + +world runcomponent { + export guest; + import host; +} \ No newline at end of file