Files
wasmtime/tests/all/epoch_interruption.rs
Chris Fallin 8a55b5c563 Add epoch-based interruption for cooperative async timeslicing.
This PR introduces a new way of performing cooperative timeslicing that
is intended to replace the "fuel" mechanism. The tradeoff is that this
mechanism interrupts with less precision: not at deterministic points
where fuel runs out, but rather when the Engine enters a new epoch. The
generated code instrumentation is substantially faster, however, because
it does not need to do as much work as when tracking fuel; it only loads
the global "epoch counter" and does a compare-and-branch at backedges
and function prologues.

This change has been measured as ~twice as fast as fuel-based
timeslicing for some workloads, especially control-flow-intensive
workloads such as the SpiderMonkey JS interpreter on Wasm/WASI.

The intended interface is that the embedder of the `Engine` performs an
`engine.increment_epoch()` call periodically, e.g. once per millisecond.
An async invocation of a Wasm guest on a `Store` can specify a number of
epoch-ticks that are allowed before an async yield back to the
executor's event loop. (The initial amount and automatic "refills" are
configured on the `Store`, just as for fuel.) This call does only
signal-safe work (it increments an `AtomicU64`) so could be invoked from
a periodic signal, or from a thread that wakes up once per period.
2022-01-20 13:58:17 -08:00

422 lines
11 KiB
Rust

use std::future::Future;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use wasmtime::*;
fn dummy_waker() -> Waker {
return unsafe { Waker::from_raw(clone(5 as *const _)) };
unsafe fn clone(ptr: *const ()) -> RawWaker {
assert_eq!(ptr as usize, 5);
const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
RawWaker::new(ptr, &VTABLE)
}
unsafe fn wake(ptr: *const ()) {
assert_eq!(ptr as usize, 5);
}
unsafe fn wake_by_ref(ptr: *const ()) {
assert_eq!(ptr as usize, 5);
}
unsafe fn drop(ptr: *const ()) {
assert_eq!(ptr as usize, 5);
}
}
fn build_engine() -> Arc<Engine> {
let mut config = Config::new();
config.async_support(true);
config.epoch_interruption(true);
Arc::new(Engine::new(&config).unwrap())
}
fn make_env(engine: &Engine) -> Linker<()> {
let mut linker = Linker::new(engine);
let engine = engine.clone();
linker
.func_new(
"",
"bump_epoch",
FuncType::new(None, None),
move |_caller, _params, _results| {
engine.increment_epoch();
Ok(())
},
)
.unwrap();
linker
}
/// Run a test with the given wasm, giving an initial deadline of
/// `initial` ticks in the future, and either configuring the wasm to
/// yield and set a deadline `delta` ticks in the future if `delta` is
/// `Some(..)` or trapping if `delta` is `None`.
///
/// Returns `Some(yields)` if function completed normally, giving the
/// number of yields that occured, or `None` if a trap occurred.
fn run_and_count_yields_or_trap<F: Fn(Arc<Engine>)>(
wasm: &str,
initial: u64,
delta: Option<u64>,
setup_func: F,
) -> Option<usize> {
let engine = build_engine();
let linker = make_env(&engine);
let module = Module::new(&engine, wasm).unwrap();
let mut store = Store::new(&engine, ());
let instance = linker.instantiate(&mut store, &module).unwrap();
let f = instance.get_func(&mut store, "run").unwrap();
store.set_epoch_deadline(initial);
match delta {
Some(delta) => {
store.epoch_deadline_async_yield_and_update(delta);
}
None => {
store.epoch_deadline_trap();
}
}
let engine_clone = engine.clone();
setup_func(engine_clone);
let mut future = Box::pin(f.call_async(&mut store, &[], &mut []));
let mut yields = 0;
loop {
match future
.as_mut()
.poll(&mut Context::from_waker(&dummy_waker()))
{
Poll::Pending => {
yields += 1;
}
Poll::Ready(Ok(..)) => {
break;
}
Poll::Ready(Err(e)) => match e.downcast::<wasmtime::Trap>() {
Ok(_) => {
return None;
}
e => {
e.unwrap();
}
},
}
}
Some(yields)
}
#[test]
fn epoch_yield_at_func_entry() {
// Should yield at start of call to func $subfunc.
assert_eq!(
Some(1),
run_and_count_yields_or_trap(
"
(module
(import \"\" \"bump_epoch\" (func $bump))
(func (export \"run\")
call $bump ;; bump epoch
call $subfunc) ;; call func; will notice new epoch and yield
(func $subfunc))
",
1,
Some(1),
|_| {},
)
);
}
#[test]
fn epoch_yield_at_loop_header() {
// Should yield at top of loop, once per five iters.
assert_eq!(
Some(2),
run_and_count_yields_or_trap(
"
(module
(import \"\" \"bump_epoch\" (func $bump))
(func (export \"run\")
(local $i i32)
(local.set $i (i32.const 10))
(loop $l
call $bump
(br_if $l (local.tee $i (i32.sub (local.get $i) (i32.const 1)))))))
",
0,
Some(5),
|_| {},
)
);
}
#[test]
fn epoch_yield_immediate() {
// We should see one yield immediately when the initial deadline
// is zero.
assert_eq!(
Some(1),
run_and_count_yields_or_trap(
"
(module
(import \"\" \"bump_epoch\" (func $bump))
(func (export \"run\")))
",
0,
Some(1),
|_| {},
)
);
}
#[test]
fn epoch_yield_only_once() {
// We should yield from the subfunction, and then when we return
// to the outer function and hit another loop header, we should
// not yield again (the double-check block will reload the correct
// epoch).
assert_eq!(
Some(1),
run_and_count_yields_or_trap(
"
(module
(import \"\" \"bump_epoch\" (func $bump))
(func (export \"run\")
(local $i i32)
(call $subfunc)
(local.set $i (i32.const 0))
(loop $l
(br_if $l (i32.eq (i32.const 10)
(local.tee $i (i32.add (i32.const 1) (local.get $i)))))))
(func $subfunc
(call $bump)))
",
1,
Some(1),
|_| {},
)
);
}
#[test]
fn epoch_interrupt_infinite_loop() {
assert_eq!(
None,
run_and_count_yields_or_trap(
"
(module
(import \"\" \"bump_epoch\" (func $bump))
(func (export \"run\")
(loop $l
(br $l))))
",
1,
None,
|engine| {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(50));
engine.increment_epoch();
});
},
)
);
}
#[test]
fn epoch_interrupt_function_entries() {
assert_eq!(
None,
run_and_count_yields_or_trap(
"
(module
(import \"\" \"bump_epoch\" (func $bump))
(func (export \"run\")
call $f1
call $f1
call $f1
call $f1
call $f1
call $f1
call $f1
call $f1
call $f1
call $f1)
(func $f1
call $f2
call $f2
call $f2
call $f2
call $f2
call $f2
call $f2
call $f2
call $f2
call $f2)
(func $f2
call $f3
call $f3
call $f3
call $f3
call $f3
call $f3
call $f3
call $f3
call $f3
call $f3)
(func $f3
call $f4
call $f4
call $f4
call $f4
call $f4
call $f4
call $f4
call $f4
call $f4
call $f4)
(func $f4
call $f5
call $f5
call $f5
call $f5
call $f5
call $f5
call $f5
call $f5
call $f5
call $f5)
(func $f5
call $f6
call $f6
call $f6
call $f6
call $f6
call $f6
call $f6
call $f6
call $f6
call $f6)
(func $f6
call $f7
call $f7
call $f7
call $f7
call $f7
call $f7
call $f7
call $f7
call $f7
call $f7)
(func $f7
call $f8
call $f8
call $f8
call $f8
call $f8
call $f8
call $f8
call $f8
call $f8
call $f8)
(func $f8
call $f9
call $f9
call $f9
call $f9
call $f9
call $f9
call $f9
call $f9
call $f9
call $f9)
(func $f9))
",
1,
None,
|engine| {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(50));
engine.increment_epoch();
});
},
)
);
}
#[test]
fn drop_future_on_epoch_yield() {
let wasm = "
(module
(import \"\" \"bump_epoch\" (func $bump))
(import \"\" \"im_alive\" (func $im_alive))
(import \"\" \"oops\" (func $oops))
(func (export \"run\")
(call $im_alive)
(call $bump)
(call $subfunc) ;; subfunc entry to do epoch check
(call $oops))
(func $subfunc))
";
let engine = build_engine();
let mut linker = make_env(&engine);
// Create a few helpers for the Wasm to call.
let alive_flag = Arc::new(AtomicBool::new(false));
let alive_flag_clone = alive_flag.clone();
linker
.func_new(
"",
"oops",
FuncType::new(None, None),
move |_caller, _params, _results| {
panic!("Should not have reached this point!");
},
)
.unwrap();
linker
.func_new(
"",
"im_alive",
FuncType::new(None, None),
move |_caller, _params, _results| {
alive_flag_clone.store(true, Ordering::Release);
Ok(())
},
)
.unwrap();
let module = Module::new(&engine, wasm).unwrap();
let mut store = Store::new(&engine, ());
let instance = linker.instantiate(&mut store, &module).unwrap();
let f = instance.get_func(&mut store, "run").unwrap();
store.set_epoch_deadline(1);
store.epoch_deadline_async_yield_and_update(1);
let mut future = Box::pin(f.call_async(&mut store, &[], &mut []));
match future
.as_mut()
.poll(&mut Context::from_waker(&dummy_waker()))
{
Poll::Pending => {
// OK: expected yield.
}
Poll::Ready(Ok(..)) => {
panic!("Shoulud not have returned");
}
Poll::Ready(e) => {
e.unwrap();
}
}
drop(future);
assert_eq!(true, alive_flag.load(Ordering::Acquire));
}