Skip to content
This repository was archived by the owner on Mar 24, 2022. It is now read-only.

Commit 86ca1d1

Browse files
committed
Add a "bounded runtime" mode to guest execution.
This modifies the instruction-counting mechanism to track an instruction-count bound as well; and when a bound is set, the Wasm guest will make a hostcall to yield back to the host context. This is all placed under the `run_async()` function, so bounded-execution yields manifest as futures that are immediately ready to continue execution. This should give an executor main loop a chance to do other work at regular intervals.
1 parent 689bc87 commit 86ca1d1

File tree

13 files changed

+446
-127
lines changed

13 files changed

+446
-127
lines changed

lucet-module/src/runtime.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@
44
#[repr(align(8))]
55
pub struct InstanceRuntimeData {
66
pub globals_ptr: *mut i64,
7-
pub instruction_count: u64,
7+
/// `instruction_count_bound + instruction_count_adj` gives the total
8+
/// instructions executed. We deconstruct the count into a signed adjustment
9+
/// and a "bound" because we want to be able to set a runtime bound beyond
10+
/// which we yield to the caller. We do this by beginning execution with
11+
/// `instruction_count_adj` set to some negative value and
12+
/// `instruction_count_bound` adjusted upward in compensation.
13+
/// `instruction_count_adj` is incremented as execution proceeds; on each
14+
/// increment, the Wasm code checks the carry flag. If the value crosses
15+
/// zero (becomes positive), then we have exceeded the bound and we must
16+
/// yield. At any point, the `adj` value can be adjusted downward by
17+
/// transferring the count to the `bound`.
18+
///
19+
/// Note that the bound-yield is only triggered if the `adj` value
20+
/// transitions from negative to non-negative; in other words, it is
21+
/// edge-triggered, not level-triggered. So entering code that has been
22+
/// instrumented for instruction counting with `adj` >= 0 will result in no
23+
/// bound ever triggered (until 2^64 instructions execute).
24+
pub instruction_count_adj: i64,
25+
pub instruction_count_bound: i64,
826
pub stack_limit: u64,
927
}

lucet-runtime/include/lucet_vmctx.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,10 @@ void *lucet_vmctx_get_func_from_idx(struct lucet_vmctx const *ctx, uint32_t tabl
4242
// Mostly for tests - this conversion is builtin to lucetc
4343
int64_t *lucet_vmctx_get_globals(struct lucet_vmctx const *ctx);
4444

45+
// Yield that is meant to be inserted by compiler instrumentation, transparent
46+
// to Wasm code execution. It is intended to be invoked periodically (e.g.,
47+
// every N instructions) to bound runtime of any particular execution slice of
48+
// Wasm code.
49+
void lucet_vmctx_yield_at_bound_expiration(struct lucet_vmctx const *ctx);
50+
4551
#endif // LUCET_VMCTX_H

lucet-runtime/lucet-runtime-internals/src/future.rs

Lines changed: 57 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use crate::error::Error;
2-
use crate::instance::{InstanceHandle, RunResult, State, TerminationDetails};
2+
use crate::instance::{InstanceHandle, InternalRunResult, RunResult, State, TerminationDetails};
33
use crate::val::{UntypedRetVal, Val};
44
use crate::vmctx::{Vmctx, VmctxInternal};
55
use std::any::Any;
66
use std::future::Future;
77
use std::pin::Pin;
8+
use std::task::{Context, Poll};
89

910
/// This is the same type defined by the `futures` library, but we don't need the rest of the
1011
/// library for this purpose.
@@ -75,23 +76,44 @@ impl Vmctx {
7576
// Wrap the computation in `YieldedFuture` so that
7677
// `Instance::run_async` can catch and run it. We will get the
7778
// `ResumeVal` we applied to `f` above.
78-
self.yield_impl::<YieldedFuture, ResumeVal>(YieldedFuture(f), false);
79+
self.yield_impl::<YieldedFuture, ResumeVal>(YieldedFuture(f), false, false);
7980
let ResumeVal(v) = self.take_resumed_val();
8081
// We may now downcast and unbox the returned Box<dyn Any> into an `R`
8182
// again.
8283
*v.downcast().expect("run_async broke invariant")
8384
}
8485
}
8586

86-
/// This struct needs to be exposed publicly in order for the signature of a
87-
/// "block_in_place" function to be writable, a concession we must make because
88-
/// Rust does not have rank 2 types. To prevent the user from inspecting or
89-
/// constructing the inside of this type, it is completely opaque.
90-
pub struct Bounce<'a>(BounceInner<'a>);
91-
92-
enum BounceInner<'a> {
87+
enum Bounce<'a> {
9388
Done(UntypedRetVal),
9489
More(BoxFuture<'a, ResumeVal>),
90+
BoundExpired,
91+
}
92+
93+
/// A simple future that yields once. We use this to yield when a runtime bound is reached.
94+
///
95+
/// Inspired by Tokio's `yield_now()`.
96+
struct YieldNow {
97+
yielded: bool,
98+
}
99+
100+
impl YieldNow {
101+
fn new() -> Self {
102+
Self { yielded: false }
103+
}
104+
}
105+
106+
impl Future for YieldNow {
107+
type Output = ();
108+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
109+
if self.yielded {
110+
Poll::Ready(())
111+
} else {
112+
self.yielded = true;
113+
cx.waker().wake_by_ref();
114+
Poll::Pending
115+
}
116+
}
95117
}
96118

97119
impl InstanceHandle {
@@ -101,51 +123,20 @@ impl InstanceHandle {
101123
/// that use `Vmctx::block_on` and provides the trampoline that `.await`s those futures on
102124
/// behalf of the guest.
103125
///
126+
/// If `runtime_bound` is provided, it will also pause the Wasm execution and yield a future
127+
/// that resumes it after (approximately) that many Wasm opcodes have executed.
128+
///
104129
/// # `Vmctx` Restrictions
105130
///
106131
/// This method permits the use of `Vmctx::block_on`, but disallows all other uses of `Vmctx::
107132
/// yield_val_expecting_val` and family (`Vmctx::yield_`, `Vmctx::yield_expecting_val`,
108133
/// `Vmctx::yield_val`).
109-
///
110-
/// # Blocking thread
111-
///
112-
/// The `wrap_blocking` argument is a function that is called with a closure that runs the Wasm
113-
/// program. Since Wasm may execute for an arbitrarily long time without `await`ing, we need to
114-
/// make sure that it runs on a thread that is allowed to block.
115-
///
116-
/// This argument is designed with [`tokio::task::block_in_place`][tokio] in mind. The odd type
117-
/// is a concession to the fact that we don't have rank 2 types in Rust, and so must fall back
118-
/// to trait objects in order to be able to take an argument that is itself a function that
119-
/// takes a closure.
120-
///
121-
/// In order to provide an appropriate function, you may have to wrap the library function in
122-
/// another closure so that the types are compatible. For example:
123-
///
124-
/// ```no_run
125-
/// # async fn f() {
126-
/// # let instance: lucet_runtime_internals::instance::InstanceHandle = unimplemented!();
127-
/// fn block_in_place<F, R>(f: F) -> R
128-
/// where
129-
/// F: FnOnce() -> R,
130-
/// {
131-
/// // ...
132-
/// # f()
133-
/// }
134-
///
135-
/// instance.run_async("entrypoint", &[], |f| block_in_place(f)).await.unwrap();
136-
/// # }
137-
/// ```
138-
///
139-
/// [tokio]: https://docs.rs/tokio/0.2.21/tokio/task/fn.block_in_place.html
140-
pub async fn run_async<'a, F>(
134+
pub async fn run_async<'a>(
141135
&'a mut self,
142136
entrypoint: &'a str,
143137
args: &'a [Val],
144-
wrap_blocking: F,
145-
) -> Result<UntypedRetVal, Error>
146-
where
147-
F: for<'b> Fn(&mut (dyn FnMut() -> Result<Bounce<'b>, Error>)) -> Result<Bounce<'b>, Error>,
148-
{
138+
runtime_bound: Option<u64>,
139+
) -> Result<UntypedRetVal, Error> {
149140
if self.is_yielded() {
150141
return Err(Error::Unsupported(
151142
"cannot run_async a yielded instance".to_owned(),
@@ -156,37 +147,40 @@ impl InstanceHandle {
156147
let mut resume_val: Option<ResumeVal> = None;
157148
loop {
158149
// Run the WebAssembly program
159-
let bounce = wrap_blocking(&mut || {
150+
let bounce = {
160151
let run_result = if self.is_yielded() {
161152
// A previous iteration of the loop stored the ResumeVal in
162153
// `resume_val`, send it back to the guest ctx and continue
163154
// running:
164-
self.resume_with_val_impl(
155+
let result = self.resume_with_val_impl(
165156
resume_val
166157
.take()
167158
.expect("is_yielded implies resume_value is some"),
168159
true,
160+
)?;
161+
Ok(InternalRunResult::Normal(result))
162+
} else if self.is_bound_expired() {
163+
self.resume_bounded(
164+
runtime_bound.expect("should have bound if guest had expired bound"),
169165
)
170166
} else {
171167
// This is the first iteration, call the entrypoint:
172168
let func = self.module.get_export_func(entrypoint)?;
173-
self.run_func(func, args, true)
169+
self.run_func(func, args, true, runtime_bound)
174170
};
175171
match run_result? {
176-
RunResult::Returned(rval) => {
172+
InternalRunResult::Normal(RunResult::Returned(rval)) => {
177173
// Finished running, return UntypedReturnValue
178-
return Ok(Bounce(BounceInner::Done(rval)));
174+
Ok(Bounce::Done(rval))
179175
}
180-
RunResult::Yielded(yval) => {
176+
InternalRunResult::Normal(RunResult::Yielded(yval)) => {
181177
// Check if the yield came from Vmctx::block_on:
182178
if yval.is::<YieldedFuture>() {
183179
let YieldedFuture(future) = *yval.downcast::<YieldedFuture>().unwrap();
184180
// Rehydrate the lifetime from `'static` to `'a`, which
185181
// is morally the same lifetime as was passed into
186182
// `Vmctx::block_on`.
187-
Ok(Bounce(BounceInner::More(
188-
future as BoxFuture<'a, ResumeVal>,
189-
)))
183+
Ok(Bounce::More(future as BoxFuture<'a, ResumeVal>))
190184
} else {
191185
// Any other yielded value is not supported - die with an error.
192186
Err(Error::Unsupported(
@@ -195,17 +189,22 @@ impl InstanceHandle {
195189
))
196190
}
197191
}
192+
InternalRunResult::BoundExpired => Ok(Bounce::BoundExpired),
198193
}
199-
})?;
194+
}?;
200195
match bounce {
201-
Bounce(BounceInner::Done(rval)) => return Ok(rval),
202-
Bounce(BounceInner::More(fut)) => {
196+
Bounce::Done(rval) => return Ok(rval),
197+
Bounce::More(fut) => {
203198
// await on the computation. Store its result in
204199
// `resume_val`.
205200
resume_val = Some(fut.await);
206201
// Now we want to `Instance::resume_with_val` and start
207202
// this cycle over.
208203
}
204+
Bounce::BoundExpired => {
205+
// Await on a simple future that yields once then is ready.
206+
YieldNow::new().await
207+
}
209208
}
210209
}
211210
}

0 commit comments

Comments
 (0)