#786: make the expression budget tests independent of machine load

fanning_out_reads_stop_at_a_budget expects the run-count error, but under heavy load
the 5 s wall-clock budget could fire first (different error). Add a test-only
per-thread time limit and lift it in that test. Wall-clock assertions in the other
new tests are now only generous hang guards (>=10x), the fuzzer's hang deadline is
24x MAX_TIME and the perf bound is 10x looser.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
vangel76andClaude Sonnet 5.5 committed 2026-10-11 20:52:39 +02:00
1 parent 9da66d9063
commit d7a03ef3a7
3 files changed
+31 -7

No files matched your search

+20 -1
View File
@@ -328,11 +328,30 @@ fn evaluate_in_time(script: &Script, ctx: &mut Context) -> Option<JsResult<JsVal
}
}
#[cfg(test)]
thread_local! {
/// Test hook: this thread's time budget (tests of the run-count budget lift it so that a
/// loaded machine cannot reach the clock first).
pub static TIME_LIMIT: std::cell::Cell<std::time::Duration> = const { std::cell::Cell::new(crate::MAX_TIME) };
}
#[cfg(not(target_arch = "wasm32"))]
fn max_time() -> std::time::Duration {
#[cfg(test)]
{
TIME_LIMIT.with(|t| t.get())
}
#[cfg(not(test))]
{
crate::MAX_TIME
}
}
/// Whether the outermost evaluation on this thread has used up [`crate::MAX_TIME`].
pub fn out_of_time() -> bool {
#[cfg(not(target_arch = "wasm32"))]
{
STARTED.with(|s| s.get()).is_some_and(|t| t.elapsed() > crate::MAX_TIME)
STARTED.with(|s| s.get()).is_some_and(|t| t.elapsed() > max_time())
}
#[cfg(target_arch = "wasm32")]
{
+10 -5
View File
@@ -385,7 +385,8 @@ fn circular_references_terminate() {
f.set_expr(f.b, OPA, "thisComp.layer('A').transform.opacity");
let start = Instant::now();
let r = eval_property(&f.p, f.cid, f.a, OPA, 0.0);
assert!(start.elapsed().as_secs_f64() < 10.0);
// Only a hang guard (the budgets stop it far sooner); generous for a loaded machine.
assert!(start.elapsed().as_secs_f64() < 100.0, "{:?}", start.elapsed());
// The cycle is caught when A needs itself again, and falls back to keyframed values.
assert!(matches!(r, Ok(Value::Scalar(_))) || r.is_err(), "{r:?}");
}
@@ -399,7 +400,7 @@ fn an_expression_needing_its_own_value_fails_fast() {
f.set_expr(f.a, ROT, "fromWorld(toWorld(anchorPoint))[0]");
let start = Instant::now();
let r = eval_property(&f.p, f.cid, f.a, ROT, 0.5);
assert!(start.elapsed().as_secs_f64() < 5.0, "{:?}", start.elapsed());
assert!(start.elapsed().as_secs_f64() < 100.0, "{:?}", start.elapsed());
// The inner read falls back to the keyframed rotation, as in a standalone evaluation.
assert_eq!(r, f.ev(f.a, ROT, "fromWorld(toWorld(anchorPoint))[0]", 0.5));
}
@@ -412,9 +413,13 @@ fn fanning_out_reads_stop_at_a_budget() {
let text = |other: &str| format!("var r = thisComp.layer('{other}').transform.rotation; r.valueAtTime(time + 0.001) + r.valueAtTime(time + 0.002)");
f.set_expr(f.a, ROT, &text("B"));
f.set_expr(f.b, ROT, &text("A"));
// Take the clock out of the race: on a loaded machine the time budget could otherwise stop
// the evaluation before the run count does, with a different error.
crate::runtime::TIME_LIMIT.with(|t| t.set(std::time::Duration::from_secs(3600)));
let start = Instant::now();
let r = eval_property(&f.p, f.cid, f.a, ROT, 0.5);
assert!(start.elapsed().as_secs_f64() < 10.0, "{:?}", start.elapsed());
crate::runtime::TIME_LIMIT.with(|t| t.set(crate::MAX_TIME));
assert!(start.elapsed().as_secs_f64() < 300.0, "{:?}", start.elapsed());
assert!(r.unwrap_err().contains("too many"));
// The budget is per outermost evaluation: the next one starts afresh.
assert_close!(f.num("time * 2", 1.0), 2.0);
@@ -440,7 +445,7 @@ fn a_long_running_script_stops_at_the_time_budget() {
let start = Instant::now();
let e = f.err("function g() { for (var i = 0; i < 900000; i++) {} }\nfor (var k = 0; k < 900000; k++) g()");
assert!(e.contains("too long"), "{e}");
assert!(start.elapsed() < crate::MAX_TIME * 3, "{:?}", start.elapsed());
assert!(start.elapsed() < crate::MAX_TIME * 12, "{:?}", start.elapsed());
// The thread's runtime starts afresh.
assert_close!(f.num("time * 2", 1.0), 2.0);
}
@@ -1075,7 +1080,7 @@ fn hundred_wiggles_are_fast() {
let per_frame = start.elapsed().as_secs_f64() * 1000.0 / n as f64;
println!("100 wiggle properties: {per_frame:.3} ms/frame (checksum {acc})");
// Generous bound so unoptimised CI builds pass; release is far below.
assert!(per_frame < 100.0, "{per_frame} ms/frame");
assert!(per_frame < 1000.0, "{per_frame} ms/frame");
}
#[test]
+1 -1
View File
@@ -326,7 +326,7 @@ fn fuzz_expressions_never_crash() {
let budget = Duration::from_secs(env_u64("EFFECTCRAFT_FUZZ_SECS", 5));
let verbose = env_u64("EFFECTCRAFT_FUZZ_VERBOSE", 0) != 0;
// Every evaluation gives up after `MAX_TIME`, and an input is evaluated a handful of times.
let deadline = crate::MAX_TIME * 12;
let deadline = crate::MAX_TIME * 24;
let mut worker = Worker::start().unwrap();
// The unmutated seeds evaluate everywhere without crashing.
for (i, s) in SEEDS.iter().enumerate() {