mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
Merge pull request #4190 from KaboomFox/plugin-eager-compile
Plug-ins: compiling a plug-in's code takes none of a run's instructions
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
//! The wasmi host: loading, instantiating and calling a plug-in under resource limits.
|
||||
|
||||
use wasmi::{
|
||||
Config, EnforcedLimits, Engine, Instance, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, TypedFunc, TypedResumableCall,
|
||||
WasmParams, WasmResults,
|
||||
Config, CustomFuelCosts, EnforcedLimits, Engine, Instance, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, TypedFunc,
|
||||
TypedResumableCall, WasmParams, WasmResults,
|
||||
};
|
||||
|
||||
use crate::manifest::{MAX_MANIFEST_BYTES, Manifest};
|
||||
@@ -14,6 +14,14 @@ pub const ABI_VERSION: i32 = 1;
|
||||
/// Fuel handed out per slice; between slices the host checks the deadline.
|
||||
const FUEL_SLICE: u64 = 20_000_000;
|
||||
|
||||
/// wasmi's fuel costs, but compiling costs nothing. wasmi compiles a function when it is first
|
||||
/// called, and charges the run that calls it by the function's size. When the slice a run was in
|
||||
/// ran out meanwhile, wasmi ended the run with an error, not an out-of-fuel pause the host resumes:
|
||||
/// a first run failed with `Limit("instruction budget")` and most of its budget left, as where the
|
||||
/// slices fell decided. So a run pays for its own instructions only, as many on a first run as on
|
||||
/// the next. Copying keeps wasmi's default cost, a unit for every 64 bytes.
|
||||
const FUEL_COSTS: CustomFuelCosts = CustomFuelCosts { bytes_copied_per_fuel: 64, fuel_per_bytes_translated: 0, fuel_per_bytes_validated: 0 };
|
||||
|
||||
/// Instructions a heavy filter's run may use besides [`Limits::fuel_per_byte`] for each byte of
|
||||
/// input ([`crate::Budget::Heavy`]): 20 times a light one's. Its output's size can't add to it:
|
||||
/// the host learns that only once the run has ended.
|
||||
@@ -152,7 +160,11 @@ impl Plugin {
|
||||
return Err(Error::Module("not a WebAssembly binary (missing \\0asm header)".into()));
|
||||
}
|
||||
let mut config = Config::default();
|
||||
config.consume_fuel(true).enforced_limits(EnforcedLimits::strict()).set_max_recursion_depth(limits.max_recursion_depth.max(16));
|
||||
config
|
||||
.consume_fuel(true)
|
||||
.fuel_cost(FUEL_COSTS)
|
||||
.enforced_limits(EnforcedLimits::strict())
|
||||
.set_max_recursion_depth(limits.max_recursion_depth.max(16));
|
||||
let engine = Engine::new(&config);
|
||||
let module = Module::new(&engine, bytes).map_err(|e| Error::Module(e.to_string()))?;
|
||||
if let Some(imp) = module.imports().next() {
|
||||
|
||||
@@ -164,6 +164,9 @@ fn wall_clock_budget_stops_runaway_plugins() {
|
||||
assert!(t < Duration::from_secs(3), "{t:?}");
|
||||
}
|
||||
|
||||
/// Cancelling stops a run where one slice of its budget ends: an endless loop given all but
|
||||
/// unlimited instructions comes back `Cancelled` within seconds, where outside a cancelled scope the
|
||||
/// same module runs until its budget is spent.
|
||||
#[test]
|
||||
fn cancelling_stops_a_run_between_two_slices_of_its_budget() {
|
||||
let spin = ok_module("(loop $l (br $l)) (i64.const 0)");
|
||||
@@ -179,6 +182,29 @@ fn cancelling_stops_a_run_between_two_slices_of_its_budget() {
|
||||
assert_eq!(e, Error::Limit("instruction budget".into()));
|
||||
}
|
||||
|
||||
/// Compiling a plug-in's code takes none of a run's instructions, so a first run takes as many as the
|
||||
/// next, though wasmi compiles each function only when it is first called. When the compiling was the
|
||||
/// run's to pay for, and the slice of the budget the run was in ran out while a function compiled,
|
||||
/// wasmi ended the run with an error the host can't resume: it failed with most of its budget left.
|
||||
/// Here `vc_run` calls a function about 100 KB long, a `br_table` of 100,000 labels.
|
||||
#[test]
|
||||
fn a_first_run_takes_no_more_instructions_than_the_next() {
|
||||
let big = format!("(func $big (param $i i32) (block (br_table {}0 (local.get $i))))", "0 ".repeat(100_000));
|
||||
let calling = module(MANIFEST, &format!("(call $big (i32.const 0)) {}", tpl::ECHO), &big);
|
||||
let with = |fuel_base| Limits { fuel_base, fuel_per_byte: 0, ..Limits::default() };
|
||||
let warm = Plugin::load(&calling, Limits::default()).unwrap();
|
||||
assert_eq!(warm.run(INPUT.as_bytes(), b"{}"), Ok(INPUT.as_bytes().to_vec()));
|
||||
// The fewest instructions a run takes once the functions are compiled.
|
||||
let (mut lo, mut hi) = (0, 1 << 20);
|
||||
assert!(warm.run_limited(INPUT.as_bytes(), b"{}", &with(hi)).is_ok());
|
||||
while hi - lo > 1 {
|
||||
let mid = (lo + hi) / 2;
|
||||
if warm.run_limited(INPUT.as_bytes(), b"{}", &with(mid)).is_ok() { hi = mid } else { lo = mid }
|
||||
}
|
||||
let first = Plugin::load(&calling, Limits::default()).unwrap();
|
||||
assert_eq!(first.run_limited(INPUT.as_bytes(), b"{}", &with(hi)), Ok(INPUT.as_bytes().to_vec()), "a first run within {hi} instructions");
|
||||
}
|
||||
|
||||
/// A filter that asks for a heavy budget gets 20 times the instructions and five times the time of
|
||||
/// a light one, so it finishes work a light one can't: here 100 million steps of a loop. The
|
||||
/// budget is its own: what it returns can't add to it, since the host learns that only at the end.
|
||||
|
||||
@@ -109,6 +109,11 @@ wasmi is built with its *portable* (loop) dispatcher: the default tail-call disp
|
||||
sibling-call optimisation, and when that doesn't happen a long-running module overflows the host stack, which aborts
|
||||
the process instead of returning an error.
|
||||
|
||||
wasmi compiles each function when it is first called, and the instruction budget doesn't pay for that, so a run
|
||||
takes as many instructions the first time as the next. Charged to the run, as wasmi charges by default, compiling
|
||||
could fail it: when the 20 M-instruction slice the run was in ran out while a function compiled, wasmi ended the run
|
||||
with an error rather than a pause the host resumes, and the run stopped with most of its budget left.
|
||||
|
||||
Drawing evaluates effects often, so live-effect results are cached by plug-in installation, parameters and input
|
||||
geometry: a plug-in runs again only when one of them changes.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user