Scripts: a return looks only at the last saved finally site

The saved throw sites of running finally blocks are kept in rising slot
order, so leaving a frame or a block now removes entries from the end of
the list and stops at the first one below the slot. Before, every return
looked at every saved site, so a call made inside many nested finally
blocks took time in proportion to their count.
This commit is contained in:
dotCooCoo
2026-11-11 14:03:21 -08:00
parent f04aa61fc1
commit db6b55ab0a
2 changed files with 28 additions and 2 deletions
+15 -2
View File
@@ -300,6 +300,12 @@ pub(crate) const MAX_APPLY_ARGS: usize = 1 << 16;
/// The estimated bytes of a name's entries in the list and the set `for-in` builds.
const KEY_ENTRY_BYTES: usize = 32;
#[cfg(test)]
thread_local! {
/// Tests: the [`Interp::finally_sites`] entries that leaving a block or a frame has looked at.
pub(crate) static SITE_CHECKS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
/// The estimated bytes of the index names `0` to `len - 1` while `for-in` lists them.
fn index_keys_bytes(len: usize) -> usize {
let mut total = 0usize;
@@ -614,9 +620,16 @@ impl Interp {
}
/// Removes the [`Self::finally_sites`] entries whose stack slot is at or above `len`: those of
/// `finally` blocks that were left.
/// `finally` blocks that were left. The slots rise along the list, so only its end is looked at.
fn drop_left_finally_sites(&mut self, len: usize) {
self.finally_sites.retain(|&(_, slot)| slot < len);
while let Some(&(_, slot)) = self.finally_sites.last() {
#[cfg(test)]
SITE_CHECKS.with(|c| c.set(c.get() + 1));
if slot < len {
break;
}
self.finally_sites.pop();
}
}
pub(crate) fn collect_garbage(&mut self) {
+13
View File
@@ -396,6 +396,19 @@ fn a_function_that_returns_from_a_finally_block_does_not_keep_its_code() {
assert!(thrown < plain + 4096, "{thrown} > {plain}");
}
#[test]
fn a_return_looks_only_at_the_finally_sites_of_its_own_frame() {
// 200 finally blocks run at once, each for its own exception, and the innermost calls a
// function 1,000 times. Each return looks at the last saved site only, not at all 200.
let src = "function g() { return 1; }\n\
function f(n) { try { throw n; } finally { if (n) { f(n - 1); } else { for (var i = 0; i < 1000; i++) g(); } } }\n\
try { f(199); } catch (e) { e; }";
crate::interp::SITE_CHECKS.with(|c| c.set(0));
assert_eq!(small(Limits::default()).run(src), Ok(JsValue::Number(0.0)));
let checks = crate::interp::SITE_CHECKS.with(std::cell::Cell::get);
assert!(checks < 10_000, "{checks} saved sites looked at");
}
#[test]
fn for_in_names_take_fuel_and_heap_under_later_limits() {
// The first run, under the default limits, builds an object with many properties. Later runs