Skip to content

Commit a91172d

Browse files
authored
Add benchmark for Wasm invocations (#4664)
Only create a single linker, and reuse that to create all subsequent instances. Add benchmarks to measure it. Before: ``` test wasm::tests::bench_invoke ... bench: 246,826 ns/iter (+/- 49,215) ``` After: ``` test wasm::tests::bench_invoke ... bench: 45,246 ns/iter (+/- 1,894) ``` Ref #3757
1 parent ac6709f commit a91172d

File tree

3 files changed

+74
-29
lines changed

3 files changed

+74
-29
lines changed

oak_functions_service/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#![cfg_attr(not(feature = "std"), no_std)]
1818
#![feature(never_type)]
1919
#![feature(unwrap_infallible)]
20+
// Required for enabling benchmark tests.
21+
#![feature(test)]
2022

2123
extern crate alloc;
2224

oak_functions_service/src/wasm/mod.rs

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use log::Level;
3232
use micro_rpc::StatusCode;
3333
use oak_functions_abi::{Request, Response};
3434
use spinning_top::Spinlock;
35-
use wasmi::{MemoryType, Store};
35+
use wasmi::Store;
3636

3737
use crate::{
3838
logger::{OakLogger, StandaloneLogger},
@@ -119,20 +119,9 @@ impl<L> OakLinker<L>
119119
where
120120
L: OakLogger,
121121
{
122-
fn new(engine: &wasmi::Engine, store: &mut Store<UserState<L>>) -> Self {
122+
fn new(engine: &wasmi::Engine) -> Self {
123123
let mut linker: wasmi::Linker<UserState<L>> = wasmi::Linker::new(engine);
124124

125-
// Add memory to linker.
126-
// TODO(#3783): Find a sensible value for initial pages.
127-
let initial_pages = 100;
128-
let memory_type =
129-
MemoryType::new(initial_pages, None).expect("failed to define Wasm memory type");
130-
let memory =
131-
wasmi::Memory::new(store, memory_type).expect("failed to initialize Wasm memory");
132-
linker
133-
.define(OAK_FUNCTIONS, MEMORY_NAME, wasmi::Extern::Memory(memory))
134-
.expect("failed to define Wasm memory in linker");
135-
136125
linker
137126
.func_wrap(
138127
OAK_FUNCTIONS,
@@ -218,13 +207,11 @@ where
218207

219208
/// Instantiates the Oak Linker and checks whether the instance exports `main`, `alloc` and a
220209
/// memory is attached.
221-
///
222-
/// Use the same store used when creating the linker.
223210
fn instantiate(
224-
self,
225-
mut store: Store<UserState<L>>,
211+
&self,
212+
mut store: &mut Store<UserState<L>>,
226213
module: Arc<wasmi::Module>,
227-
) -> Result<(wasmi::Instance, Store<UserState<L>>), micro_rpc::Status> {
214+
) -> Result<wasmi::Instance, micro_rpc::Status> {
228215
let instance = self
229216
.linker
230217
.instantiate(&mut store, &module)
@@ -245,7 +232,7 @@ where
245232

246233
// Check that the instance exports "main".
247234
let _ = &instance
248-
.get_typed_func::<(), ()>(&store, MAIN_FUNCTION_NAME)
235+
.get_typed_func::<(), ()>(&mut store, MAIN_FUNCTION_NAME)
249236
.map_err(|err| {
250237
micro_rpc::Status::new_with_message(
251238
micro_rpc::StatusCode::Internal,
@@ -255,7 +242,7 @@ where
255242

256243
// Check that the instance exports "alloc".
257244
let _ = &instance
258-
.get_typed_func::<i32, AbiPointer>(&store, ALLOC_FUNCTION_NAME)
245+
.get_typed_func::<i32, AbiPointer>(&mut store, ALLOC_FUNCTION_NAME)
259246
.map_err(|err| {
260247
micro_rpc::Status::new_with_message(
261248
micro_rpc::StatusCode::Internal,
@@ -272,7 +259,7 @@ where
272259
)
273260
})?;
274261

275-
Ok((instance, store))
262+
Ok(instance)
276263
}
277264
}
278265

@@ -423,9 +410,9 @@ where
423410
}
424411

425412
// A request handler with a Wasm module for handling multiple requests.
426-
#[derive(Clone)]
427413
pub struct WasmHandler<L: OakLogger> {
428414
wasm_module: Arc<wasmi::Module>,
415+
linker: OakLinker<L>,
429416
wasm_api_factory: Arc<dyn WasmApiFactory<L> + Send + Sync>,
430417
logger: L,
431418
#[cfg_attr(not(feature = "std"), allow(dead_code))]
@@ -466,8 +453,11 @@ where
466453
let module = wasmi::Module::new(&engine, wasm_module_bytes)
467454
.map_err(|err| anyhow::anyhow!("couldn't load module from buffer: {:?}", err))?;
468455

456+
let linker = OakLinker::new(module.engine());
457+
469458
Ok(WasmHandler {
470459
wasm_module: Arc::new(module),
460+
linker,
471461
wasm_api_factory,
472462
logger,
473463
observer,
@@ -489,8 +479,7 @@ where
489479
let user_state = UserState::new(wasm_api.transport(), self.logger.clone());
490480
// For isolated requests we need to create a new store for every request.
491481
let mut store = wasmi::Store::new(module.engine(), user_state);
492-
let linker = OakLinker::new(module.engine(), &mut store);
493-
let (instance, mut store) = linker.instantiate(store, module)?;
482+
let instance = self.linker.instantiate(&mut store, module)?;
494483

495484
instance.exports(&store).for_each(|export| {
496485
store

oak_functions_service/src/wasm/tests.rs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@
1414
// limitations under the License.
1515
//
1616

17+
extern crate test;
18+
1719
use alloc::{sync::Arc, vec::Vec};
20+
use std::time::Duration;
1821

1922
use byteorder::{ByteOrder, LittleEndian};
2023
use hashbrown::HashMap;
24+
use oak_functions_abi::Request;
2125
use spinning_top::Spinlock;
26+
use test::Bencher;
2227

2328
use super::{
2429
api::StdWasmApiFactory, OakLinker, UserState, WasmApiFactory, WasmHandler, ALLOC_FUNCTION_NAME,
@@ -125,9 +130,57 @@ fn test_read_request() {
125130
assert_eq!(request_bytes, test_state.request.clone());
126131
}
127132

133+
#[test]
134+
fn test_invoke() {
135+
let test_state = create_test_state();
136+
let data = b"Hello, world!";
137+
let response = test_state
138+
.wasm_handler
139+
.handle_invoke(Request {
140+
body: data.to_vec(),
141+
})
142+
.unwrap();
143+
assert_eq!(response.body, data.to_vec());
144+
}
145+
146+
#[bench]
147+
fn bench_invoke(bencher: &mut Bencher) {
148+
let test_state = create_test_state();
149+
let data = b"Hello, world!";
150+
151+
let summary = bencher.bench(|bencher| {
152+
bencher.iter(|| {
153+
let response = test_state
154+
.wasm_handler
155+
.handle_invoke(Request {
156+
body: data.to_vec(),
157+
})
158+
.unwrap();
159+
assert_eq!(response.body, data.to_vec());
160+
});
161+
Ok(())
162+
});
163+
164+
// When running `cargo test` this benchmark test gets executed too, but `summary` will be `None`
165+
// in that case. So, here we first check that `summary` is not empty.
166+
if let Ok(Some(summary)) = summary {
167+
// `summary.mean` is in nanoseconds, even though it is not explicitly documented in
168+
// https://doc.rust-lang.org/test/stats/struct.Summary.html.
169+
let elapsed = Duration::from_nanos(summary.mean as u64);
170+
// We expect the `mean` time for loading the test Wasm module and running its main function
171+
// to be less than a fixed threshold.
172+
assert!(
173+
elapsed < Duration::from_micros(100),
174+
"elapsed time: {:.0?}",
175+
elapsed
176+
);
177+
}
178+
}
179+
128180
struct TestState {
129181
instance: wasmi::Instance,
130182
store: wasmi::Store<UserState<StandaloneLogger>>,
183+
wasm_handler: WasmHandler<StandaloneLogger>,
131184
request: Vec<u8>,
132185
}
133186

@@ -155,16 +208,17 @@ fn create_test_state() -> TestState {
155208

156209
let user_state = UserState::new(wasm_api.transport(), logger.clone());
157210

158-
let module = wasm_handler.wasm_module;
211+
let module = wasm_handler.wasm_module.clone();
159212
let mut store = wasmi::Store::new(module.engine(), user_state);
160-
let linker = OakLinker::new(module.engine(), &mut store);
161-
let (instance, store) = linker
162-
.instantiate(store, module)
213+
let linker = OakLinker::new(module.engine());
214+
let instance = linker
215+
.instantiate(&mut store, module)
163216
.expect("couldn't instantiate Wasm module");
164217

165218
TestState {
166-
store,
167219
instance,
220+
store,
221+
wasm_handler,
168222
request: request.clone(),
169223
}
170224
}

0 commit comments

Comments
 (0)