Skip to content

Commit a027349

Browse files
committed
Compile the standard provider once per module
`run` compiled the embedded standard provider (Javy plugin or Shopify Function provider) from its bytes on every call. Library callers that run the same Function many times paid that cost for every run. Make `ValidatedModule` public and compile the provider when it is built. `FunctionRunParams` now borrows a `ValidatedModule`, and `run` takes the engine from the module, so callers build the module once and reuse it.
1 parent 2b9944b commit a027349

4 files changed

Lines changed: 87 additions & 56 deletions

File tree

‎src/engine.rs‎

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use wasmtime_wasi::I32Exit;
77

88
use crate::function_run_result::FunctionRunResult;
99
use crate::io::{IOHandler, OutputAndLogs};
10-
use crate::validated_module::ValidatedModule;
10+
pub use crate::validated_module::ValidatedModule;
1111
use crate::{BytesContainer, BytesContainerType};
1212

1313
#[derive(Clone)]
@@ -31,8 +31,9 @@ pub struct FunctionRunParams<'a> {
3131
pub export: &'a str,
3232
pub profile_opts: Option<&'a ProfileOpts>,
3333
pub scale_factor: f64,
34-
pub module: Module,
35-
pub engine: Engine,
34+
/// The validated Function module. Reuse it across runs to compile the
35+
/// Function and its provider once.
36+
pub module: &'a ValidatedModule,
3637
}
3738

3839
const STARTING_FUEL: u64 = u64::MAX;
@@ -95,15 +96,15 @@ pub fn run(params: FunctionRunParams) -> Result<FunctionRunResult> {
9596
export,
9697
profile_opts,
9798
scale_factor,
98-
engine,
9999
module,
100100
} = params;
101101

102-
let mut io_handler = IOHandler::new(ValidatedModule::new(module)?, input.clone());
102+
let engine = module.inner().engine();
103+
let mut io_handler = IOHandler::new(module, input.clone());
103104

104105
let mut error_logs: String = String::new();
105106

106-
let mut linker = Linker::new(&engine);
107+
let mut linker = Linker::new(engine);
107108
let wasi = io_handler.wasi();
108109
if wasi.is_some() {
109110
wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |ctx: &mut FunctionContext| {
@@ -113,10 +114,10 @@ pub fn run(params: FunctionRunParams) -> Result<FunctionRunResult> {
113114
}
114115

115116
let function_context = FunctionContext::new(wasi);
116-
let mut store = Store::new(&engine, function_context);
117+
let mut store = Store::new(engine, function_context);
117118
store.limiter(|s| &mut s.limiter);
118119

119-
io_handler.initialize(&engine, &mut linker, &mut store)?;
120+
io_handler.initialize(&mut linker, &mut store)?;
120121

121122
store.set_fuel(STARTING_FUEL)?;
122123
store.set_epoch_deadline(1);
@@ -235,8 +236,7 @@ mod tests {
235236
function_path: Path::new("tests/fixtures/build/js_function.wasm").to_path_buf(),
236237
input,
237238
export: DEFAULT_EXPORT,
238-
module,
239-
engine,
239+
module: &ValidatedModule::new(module)?,
240240
scale_factor: 1.0,
241241
profile_opts: None,
242242
})?;
@@ -260,8 +260,7 @@ mod tests {
260260
function_path: Path::new("tests/fixtures/build/js_function_v2.wasm").to_path_buf(),
261261
input,
262262
export: DEFAULT_EXPORT,
263-
module,
264-
engine,
263+
module: &ValidatedModule::new(module)?,
265264
scale_factor: 1.0,
266265
profile_opts: None,
267266
})?;
@@ -285,8 +284,7 @@ mod tests {
285284
function_path: Path::new("tests/fixtures/build/js_function_v3.wasm").to_path_buf(),
286285
input,
287286
export: DEFAULT_EXPORT,
288-
module,
289-
engine,
287+
module: &ValidatedModule::new(module)?,
290288
scale_factor: 1.0,
291289
profile_opts: None,
292290
})?;
@@ -295,6 +293,37 @@ mod tests {
295293
Ok(())
296294
}
297295

296+
#[test]
297+
fn test_validated_module_can_be_reused_across_runs() -> Result<()> {
298+
let path = Path::new("tests/fixtures/build/js_function_javy_plugin_v3.wasm");
299+
let engine = new_engine()?;
300+
let module = ValidatedModule::new(Module::from_file(&engine, path)?)?;
301+
302+
for _ in 0..3 {
303+
let input = BytesContainer::new(
304+
BytesContainerType::Input,
305+
Codec::Messagepack,
306+
serde_json::to_vec(&json!({ "hello": "world" }))?,
307+
)?;
308+
let function_run_result = run(FunctionRunParams {
309+
function_path: path.to_path_buf(),
310+
input,
311+
export: DEFAULT_EXPORT,
312+
module: &module,
313+
scale_factor: 1.0,
314+
profile_opts: None,
315+
})?;
316+
317+
assert!(function_run_result.success);
318+
assert_eq!(
319+
function_run_result.output.json_value,
320+
Some(json!({ "hello": "world output" }))
321+
);
322+
}
323+
324+
Ok(())
325+
}
326+
298327
#[test]
299328
fn test_js_functions_javy_v1() -> Result<()> {
300329
let engine = new_engine()?;
@@ -311,8 +340,7 @@ mod tests {
311340
.to_path_buf(),
312341
input,
313342
export: DEFAULT_EXPORT,
314-
module,
315-
engine,
343+
module: &ValidatedModule::new(module)?,
316344
scale_factor: 1.0,
317345
profile_opts: None,
318346
})?;
@@ -329,8 +357,7 @@ mod tests {
329357
function_path: Path::new("tests/fixtures/build/exit_code.wasm").to_path_buf(),
330358
input: json_input(&serde_json::to_vec(&json!({ "code": 0 }))?)?,
331359
export: DEFAULT_EXPORT,
332-
module,
333-
engine,
360+
module: &ValidatedModule::new(module)?,
334361
scale_factor: 1.0,
335362
profile_opts: None,
336363
})?;
@@ -347,8 +374,7 @@ mod tests {
347374
function_path: Path::new("tests/fixtures/build/exit_code.wasm").to_path_buf(),
348375
input: json_input(&serde_json::to_vec(&json!({ "code": 1 }))?)?,
349376
export: DEFAULT_EXPORT,
350-
module,
351-
engine,
377+
module: &ValidatedModule::new(module)?,
352378
scale_factor: 1.0,
353379
profile_opts: None,
354380
})?;
@@ -368,8 +394,7 @@ mod tests {
368394
function_path: Path::new("tests/fixtures/build/linear_memory.wasm").to_path_buf(),
369395
input: json_input(&serde_json::to_vec(&json!({}))?)?,
370396
export: DEFAULT_EXPORT,
371-
module,
372-
engine,
397+
module: &ValidatedModule::new(module)?,
373398
scale_factor: 1.0,
374399
profile_opts: None,
375400
})?;
@@ -390,8 +415,7 @@ mod tests {
390415
function_path: Path::new("tests/fixtures/build/log_truncation_function.wasm")
391416
.to_path_buf(),
392417
export: DEFAULT_EXPORT,
393-
module,
394-
engine,
418+
module: &ValidatedModule::new(module)?,
395419
scale_factor: 1.0,
396420
profile_opts: None,
397421
})?;
@@ -416,8 +440,7 @@ mod tests {
416440
function_path: file_path.to_path_buf(),
417441
input: json_input(&serde_json::to_vec(&json!({ "code": 0 }))?)?,
418442
export: DEFAULT_EXPORT,
419-
module,
420-
engine,
443+
module: &ValidatedModule::new(module)?,
421444
scale_factor: 1.0,
422445
profile_opts: None,
423446
})?;
@@ -445,8 +468,7 @@ mod tests {
445468
function_path: trampolined_path.to_path_buf(),
446469
input: input_bytes.unwrap(),
447470
export: DEFAULT_EXPORT,
448-
module,
449-
engine,
471+
module: &ValidatedModule::new(module)?,
450472
scale_factor: 1.0,
451473
profile_opts: None,
452474
});

‎src/io.rs‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::{anyhow, Result};
2-
use wasmtime::{AsContext, AsContextMut, Engine, Instance, Linker, Module, Store};
2+
use wasmtime::{AsContext, AsContextMut, Instance, Linker, Module, Store};
33
use wasmtime_wasi::{
44
p2::pipe::{MemoryInputPipe, MemoryOutputPipe},
55
preview1::WasiP1Ctx,
@@ -25,14 +25,14 @@ enum IOStrategy {
2525
Memory(Option<Instance>),
2626
}
2727

28-
pub(crate) struct IOHandler {
28+
pub(crate) struct IOHandler<'a> {
2929
strategy: IOStrategy,
30-
module: ValidatedModule,
30+
module: &'a ValidatedModule,
3131
input: BytesContainer,
3232
}
3333

34-
impl IOHandler {
35-
pub(crate) fn new(module: ValidatedModule, input: BytesContainer) -> Self {
34+
impl<'a> IOHandler<'a> {
35+
pub(crate) fn new(module: &'a ValidatedModule, input: BytesContainer) -> Self {
3636
Self {
3737
strategy: if module.uses_mem_io() {
3838
IOStrategy::Memory(None)
@@ -68,14 +68,13 @@ impl IOHandler {
6868

6969
pub(crate) fn initialize<T>(
7070
&mut self,
71-
engine: &Engine,
7271
linker: &mut Linker<T>,
7372
store: &mut Store<T>,
7473
) -> Result<()> {
7574
store.set_epoch_deadline(1); // Need to make sure we don't timeout during initialization.
7675
let old_fuel = store.get_fuel()?;
7776
store.set_fuel(u64::MAX)?; // Make sure we have fuel for initialization.
78-
let mem_io_instance = instantiate_imports(&self.module, engine, linker, store);
77+
let mem_io_instance = instantiate_imports(self.module, linker, store);
7978
if let IOStrategy::Memory(ref mut instance) = self.strategy {
8079
*instance = mem_io_instance;
8180
}
@@ -157,18 +156,14 @@ impl IOHandler {
157156

158157
fn instantiate_imports<T>(
159158
module: &ValidatedModule,
160-
engine: &Engine,
161159
linker: &mut Linker<T>,
162160
mut store: &mut Store<T>,
163161
) -> Option<Instance> {
164162
let mut mem_io_instance = None;
165163

166164
if let Some(std_import) = module.std_import() {
167-
let imported_module = Module::from_binary(engine, &std_import.bytes)
168-
.unwrap_or_else(|_| panic!("Failed to load module {}", std_import.name));
169-
170165
let imported_module_instance = linker
171-
.instantiate(&mut store, &imported_module)
166+
.instantiate(&mut store, &std_import.module)
172167
.expect("Failed to instantiate imported instance");
173168

174169
if std_import.is_mem_io_provider() {

‎src/main.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use anyhow::{anyhow, Result};
1111
use clap::Parser;
1212
use function_runner::{
1313
bluejay_schema_analyzer::BluejaySchemaAnalyzer,
14-
engine::{run, FunctionRunParams, ProfileOpts},
14+
engine::{run, FunctionRunParams, ProfileOpts, ValidatedModule},
1515
};
1616

1717
use is_terminal::IsTerminal;
@@ -144,6 +144,8 @@ fn main() -> Result<()> {
144144
Codec::Json
145145
};
146146

147+
let module = ValidatedModule::new(module)?;
148+
147149
let input = BytesContainer::new(BytesContainerType::Input, codec, buffer)?;
148150
let scale_factor = if let (Some(schema_string), Some(query_string), Some(json_value)) =
149151
(schema_string, query_string, input.json_value.clone())
@@ -167,8 +169,7 @@ fn main() -> Result<()> {
167169
export: opts.export.as_ref(),
168170
profile_opts: profile_opts.as_ref(),
169171
scale_factor,
170-
module,
171-
engine,
172+
module: &module,
172173
})?;
173174

174175
if opts.json {

‎src/validated_module.rs‎

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
use std::borrow::Cow;
2-
3-
use anyhow::{bail, Result};
1+
use anyhow::{anyhow, bail, Result};
42
use rust_embed::RustEmbed;
53
use wasmtime::Module;
64

75
#[derive(RustEmbed)]
86
#[folder = "providers/"]
97
struct StandardProviders;
108

11-
#[derive(Debug)]
9+
#[derive(Debug, Clone)]
1210
pub(crate) struct Provider {
13-
pub(crate) bytes: Cow<'static, [u8]>,
11+
pub(crate) module: Module,
1412
pub(crate) name: String,
1513
}
1614

@@ -38,14 +36,21 @@ impl Provider {
3836
}
3937
}
4038

41-
#[derive(Debug)]
42-
pub(crate) struct ValidatedModule {
39+
/// A Function module that has been checked for a valid combination of imports,
40+
/// together with the compiled standard provider it imports, if any.
41+
///
42+
/// Building a `ValidatedModule` compiles the provider once. Reuse the same
43+
/// `ValidatedModule` for every run of the Function to avoid compiling the
44+
/// provider again for each input.
45+
#[derive(Debug, Clone)]
46+
pub struct ValidatedModule {
4347
module: Module,
4448
std_import: Option<Provider>,
4549
}
4650

4751
impl ValidatedModule {
48-
pub(crate) fn new(module: Module) -> Result<Self> {
52+
/// Validates `module` and compiles its standard provider with the module's engine.
53+
pub fn new(module: Module) -> Result<Self> {
4954
// Need to track with deterministic order so don't use a hash
5055
let mut imports = vec![];
5156
for import in module.imports().map(|i| i.module().to_string()) {
@@ -56,12 +61,20 @@ impl ValidatedModule {
5661

5762
let uses_wasi = imports.contains(&"wasi_snapshot_preview1".to_string());
5863

59-
let std_import = imports.iter().find_map(|import| {
60-
StandardProviders::get(&format!("{import}.wasm")).map(|file| Provider {
61-
bytes: file.data,
62-
name: import.into(),
64+
// Compile the provider once so every run of this module can reuse it.
65+
let std_import = imports
66+
.iter()
67+
.find_map(|import| {
68+
StandardProviders::get(&format!("{import}.wasm")).map(|file| {
69+
Module::from_binary(module.engine(), &file.data)
70+
.map(|module| Provider {
71+
module,
72+
name: import.into(),
73+
})
74+
.map_err(|e| anyhow!("Failed to load provider {import}: {e}"))
75+
})
6376
})
64-
});
77+
.transpose()?;
6578

6679
// If there are multiple standard imports or more than zero unknown imports,
6780
// the module will fail to instantiate because we only link the one

0 commit comments

Comments
 (0)