Repository navigation
Conversation
|
Supporting this PR from an embedder's point of view, with measurements. Use case. We embed boa_engine 0.22.0 in a desktop/wasm image editor to run user scripts. Dialog event handlers are invoked from Rust via 1. Per-frame loop counting lets split loops escape use boa_engine::{Context, Source};
fn main() {
let mut ctx = Context::default();
ctx.runtime_limits_mut().set_loop_iteration_limit(1000);
let flat = ctx.eval(Source::from_bytes("var n=0; for (var i=0;i<1000000;i++){n++} n"));
println!("flat 1e6 in one frame: {}", if flat.is_err() {"stopped"} else {"ran"});
let split = ctx.eval(Source::from_bytes("var n=0; function g(){for(var j=0;j<900;j++){n++}} for (var i=0;i<900;i++){g()} n"));
println!("900x900 split across frames: {:?}", split.map(|v| v.display().to_string()).map_err(|e| e.to_string()));
}With a limit of 1,000, the flat loop stops, but the 900x900 split loop runs all 810,000 iterations. Invoked as a callback, the split loop blocked about 31-36 ms in our tests with no way to interrupt it. 2. Repeated uncatchable aborts leak value-stack slots of the entry frame (0.22.0,
Entry function above: On this PR's head ( Follow-up question (not for this PR): would you consider an API to run a Rust-invoked call under the same instruction budget as budgeted top-level evaluation, so a long callback can yield across ticks instead of only being aborted? Leak reproduction (0.22.0): use boa_engine::{Context, JsObject, JsResult, JsString, JsValue, NativeFunction, Source, js_string};
use boa_engine::object::{FunctionObjectBuilder, builtins::JsFunction};
use boa_engine::property::Attribute;
use std::cell::Cell; use std::rc::Rc;
struct S { abort: Cell<Option<JsObject>> }
fn host_pause(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
let st = ctx.get_data::<Rc<S>>().cloned().unwrap();
let a = st.abort.take().unwrap();
let saved = ctx.runtime_limits();
ctx.runtime_limits_mut().set_recursion_limit(0);
let r = a.call(&JsValue::undefined(), &[], ctx);
ctx.set_runtime_limits(saved);
st.abort.set(Some(a));
Err(r.unwrap_err())
}
fn mk(locals: u32) -> Context {
let mut ctx = Context::default();
ctx.runtime_limits_mut().set_recursion_limit(128);
ctx.runtime_limits_mut().set_stack_size_limit(10_240);
let abort = ctx.intrinsics().constructors().function().constructor().construct(&[], None, &mut ctx).unwrap();
ctx.insert_data(Rc::new(S { abort: Cell::new(Some(abort)) }));
let p = FunctionObjectBuilder::new(ctx.realm(), NativeFunction::from_fn_ptr(host_pause)).name(js_string!("p")).length(0).build();
ctx.register_global_property(js_string!("hostPause"), p, Attribute::all()).unwrap();
let hl:String=(0..locals).map(|i| format!("var v{i}={i};")).collect();
let src = format!("var healthy=function(){{return 7;}};\
function handler(a,b,c){{{hl}hostPause();return a;}}");
ctx.eval(Source::from_bytes(src.as_bytes())).unwrap();
ctx
}
fn healthy_after(locals: u32, n: u32) -> bool {
let mut ctx = mk(locals);
let h = ctx.global_object().get(JsString::from("handler"), &mut ctx).unwrap();
let f = JsFunction::from_object(h.as_object().unwrap().clone()).unwrap();
for _ in 0..n { let _ = f.call(&JsValue::undefined(), &[], &mut ctx); }
matches!(ctx.eval(Source::from_bytes("healthy()")).map(|v| v.as_number()), Ok(Some(7.0)))
}
fn main() {
for locals in [0u32, 16, 64, 256] {
let (mut lo, mut hi) = (1u32, 20_000u32);
if healthy_after(locals, hi) { println!("locals {locals}: healthy after {hi}"); continue; }
while lo < hi { let m = (lo+hi)/2; if healthy_after(locals, m) { lo = m+1 } else { hi = m } }
println!("locals {locals}: first failing pauses = {hi}");
}
} |
This Pull Request relates to #3442.
It changes the following:
HostHooks::check_interrupt(&self) -> JsResult<()>(defaulting toOk(())) andRuntimeLimitError::Interrupt, checked onOpcode::IncrementLoopIterationand inContext::check_runtime_limits(before function and constructor calls) so embedders can enforce wall-clock deadlines or cancellation flags without adding per-opcode counter overhead to straight-line bytecode.loop_iteration_countonVmacross the active evaluation (resetting when the outermost frame pops) rather than perCallFrame, so calling a function with a loop from inside another loop cannot reset the counter and bypassRuntimeLimits::loop_iteration_limit.exit_early) frame's stack slots and environments when unwinding an uncatchable error inContext::handle_error, matchinghandle_returnandhandle_throwso repeated runtime-limit or interrupt aborts do not leak stack slots across evaluations.