From 2980c98f2886881dd328a11a239ee4ae4cd53523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81ro=CC=81?= Date: Wed, 16 Sep 2026 20:50:59 +0200 Subject: [PATCH 1/2] GOL-512: sample execution timeout clocks adaptively --- README.md | 12 +- .../skeleton/src/builtin/execution.js | 7 +- .../skeleton/src/builtin/execution.rs | 38 +++++- .../skeleton/src/builtin/execution_timeout.rs | 65 +++++++++++ examples/runtime/execution/src/execution.js | 12 +- tests/runtime/execution.rs | 108 +++++++++++++++++- 6 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 crates/wasm-rquickjs/skeleton/src/builtin/execution_timeout.rs diff --git a/README.md b/README.md index 77caad4f..bbb935b8 100644 --- a/README.md +++ b/README.md @@ -660,8 +660,13 @@ Options accept exactly one of `entry: string` or inline `source: string`, plus ` inline `source` and defaults to JavaScript. With the generated crate's `typescript-runtime` feature enabled, TypeScript entry files are selected by their `.ts`, `.mts`, or `.cts` extension, and inline jobs can select `language: "typescript"`. -Output and completion wake the parent without periodic polling. CPU deadlines start -after child-runtime initialization and are enforced by the QuickJS interrupt handler. Entry modules run for their side effects; an exported +Output and completion wake the parent without periodic polling. Timeout budgets start +after child-runtime initialization. An async wait observes deadlines while jobs yield; for +non-yielding JavaScript, the QuickJS interrupt handler samples the clock at an adaptive stride +targeting one check per 10% of `timeoutMs` during steady CPU work. This is a best-effort budget, +not an exact cutoff or a maximum-overrun guarantee. A job that finishes between checks can +succeed after its literal deadline; no extra clock check occurs at completion. Entry modules run +for their side effects; an exported `default` function (or `run` function when there is no default function) is invoked and awaited. Relative entry paths and imports resolve from `cwd`. Inline `source` is an async function body: top-level `await` and `return` are supported, while static `import`/`export` declarations are not. @@ -673,7 +678,8 @@ With `overflow: "terminate"`, exceeding either stream's bound rejects the job. W `overflow: "truncate"`, execution continues, captured output is bounded, and `overflowed` is `true` when either stream exceeded the bound. Cancellation is cooperative for queued or yielding code and cannot preempt a tight loop already -running on the same thread; use a positive `timeoutMs` when that guarantee is required. A job that +running on the same thread; a positive `timeoutMs` is the best-effort escape mechanism for such a +loop. Cancellation remains a guest-local flag check on every QuickJS interrupt. A job that never settles must be cancelled or given a timeout before the enclosing component invocation can finish. A runtime accepts at most eight active jobs, and child executions cannot recursively create more execution jobs. diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/execution.js b/crates/wasm-rquickjs/skeleton/src/builtin/execution.js index e588c868..e6e6635d 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/execution.js +++ b/crates/wasm-rquickjs/skeleton/src/builtin/execution.js @@ -89,11 +89,16 @@ export function startJavaScript(options) { if (!state.done) continue; settled = true; stdout.end(); stderr.end(); forget_job(id); - if (state.error !== null) rejectResult(new Error(state.error)); + if (state.error !== null) { + const error = new Error(state.error); + if (state.timeoutClockReads !== undefined) error.timeoutClockReads = state.timeoutClockReads; + rejectResult(error); + } else { try { const completed = { value: deserializeFromTransport(state.value), overflowed: state.overflowed }; if (state.profile !== undefined) completed.profile = state.profile; + if (state.timeoutClockReads !== undefined) completed.timeoutClockReads = state.timeoutClockReads; resolveResult(completed); } catch (error) { rejectResult(error); } diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/execution.rs b/crates/wasm-rquickjs/skeleton/src/builtin/execution.rs index 05b5c57b..1336a4dd 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/execution.rs +++ b/crates/wasm-rquickjs/skeleton/src/builtin/execution.rs @@ -14,9 +14,15 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "internal-test-execution")] +use std::sync::atomic::AtomicU64; use std::task::Poll; use std::time::{Duration, Instant}; +#[path = "execution_timeout.rs"] +mod execution_timeout; +use execution_timeout::ExecutionTimeoutSampler; + const MAX_ACTIVE_JOBS: usize = 8; const MAX_TIMEOUT_MS: u64 = u64::MAX / 1_000_000; const MAX_OUTPUT_BYTES: usize = 64 * 1024 * 1024; @@ -63,6 +69,8 @@ pub(crate) struct ExecutionJob { cancel: Arc, overflowed: Arc, timed_out: Arc, + #[cfg(feature = "internal-test-execution")] + timeout_clock_reads: Arc, forgotten: AtomicBool, completion: RefCell>>, event_waker: AtomicWaker, @@ -86,6 +94,8 @@ impl ExecutionJob { cancel: Arc::new(AtomicBool::new(false)), overflowed: Arc::new(AtomicBool::new(false)), timed_out: Arc::new(AtomicBool::new(false)), + #[cfg(feature = "internal-test-execution")] + timeout_clock_reads: Arc::new(AtomicU64::new(0)), forgotten: AtomicBool::new(false), completion: RefCell::default(), event_waker: AtomicWaker::new(), @@ -206,6 +216,8 @@ struct PollResult { value: Option, error: Option, overflowed: bool, + #[cfg(feature = "internal-test-execution")] + timeout_clock_reads: u64, #[cfg(feature = "typescript-compiler-profiling")] #[serde(skip_serializing_if = "Option::is_none")] profile: Option, @@ -328,6 +340,8 @@ pub mod native_module { value, error, overflowed: job.overflowed.load(Ordering::Relaxed), + #[cfg(feature = "internal-test-execution")] + timeout_clock_reads: job.timeout_clock_reads.load(Ordering::Relaxed), #[cfg(feature = "typescript-compiler-profiling")] profile: job.profile.borrow_mut().take(), }) @@ -483,18 +497,34 @@ async fn run_job(options: ExecutionOptions, job: Rc) { // The public timeout budget starts when user code begins, after runtime and // builtin initialization. The interrupt handler is also needed for tight // loops that cannot cooperatively yield to the timer future. - let deadline = options - .timeout_ms - .and_then(|ms| Instant::now().checked_add(Duration::from_millis(ms))); + let timeout = options.timeout_ms.and_then(|ms| { + let started_at = Instant::now(); + let duration = Duration::from_millis(ms); + started_at + .checked_add(duration) + .map(|deadline| (started_at, deadline, duration)) + }); + let deadline = timeout.map(|(_, deadline, _)| deadline); + let mut timeout_sampler = timeout.map(|(started_at, deadline, duration)| { + ExecutionTimeoutSampler::new(started_at, deadline, duration) + }); let cancelled = job.cancel.clone(); let timed_out = job.timed_out.clone(); + #[cfg(feature = "internal-test-execution")] + let timeout_clock_reads = job.timeout_clock_reads.clone(); runtime .rt .set_interrupt_handler(Some(Box::new(move || { if cancelled.load(Ordering::Relaxed) { return true; } - if deadline.is_some_and(|deadline| Instant::now() >= deadline) { + if timeout_sampler.as_mut().is_some_and(|sampler| { + sampler.expired(|| { + #[cfg(feature = "internal-test-execution")] + timeout_clock_reads.fetch_add(1, Ordering::Relaxed); + Instant::now() + }) + }) { timed_out.store(true, Ordering::Relaxed); return true; } diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/execution_timeout.rs b/crates/wasm-rquickjs/skeleton/src/builtin/execution_timeout.rs new file mode 100644 index 00000000..229b6ba0 --- /dev/null +++ b/crates/wasm-rquickjs/skeleton/src/builtin/execution_timeout.rs @@ -0,0 +1,65 @@ +use std::time::{Duration, Instant}; + +const INITIAL_INTERRUPT_STRIDE: u64 = 64; +const MAX_STRIDE_GROWTH: u64 = 16; + +/// Samples a wall-clock deadline without importing the clock on every QuickJS +/// interrupt. The stride is deliberately a soft target: callback frequency can +/// change, and finishing between samples does not trigger another clock read. +pub(super) struct ExecutionTimeoutSampler { + deadline: Instant, + target_interval: Duration, + last_sample_at: Instant, + stride: u64, + interrupts_until_sample: u64, + expired: bool, +} + +impl ExecutionTimeoutSampler { + pub(super) fn new(started_at: Instant, deadline: Instant, timeout: Duration) -> Self { + Self { + deadline, + target_interval: timeout / 10, + last_sample_at: started_at, + stride: INITIAL_INTERRUPT_STRIDE, + interrupts_until_sample: INITIAL_INTERRUPT_STRIDE, + expired: false, + } + } + + /// Returns true only when a sampled clock value reaches the deadline. + /// `clock` is never called on unsampled interrupts. + pub(super) fn expired(&mut self, clock: impl FnOnce() -> Instant) -> bool { + if self.expired { + return true; + } + self.interrupts_until_sample -= 1; + if self.interrupts_until_sample != 0 { + return false; + } + + let now = clock(); + if now >= self.deadline { + self.expired = true; + return true; + } + + let elapsed_nanos = now + .saturating_duration_since(self.last_sample_at) + .as_nanos() + .max(1); + let estimated_stride = self + .target_interval + .as_nanos() + .saturating_mul(u128::from(self.stride)) + / elapsed_nanos; + let growth_limit = self.stride.saturating_mul(MAX_STRIDE_GROWTH); + self.stride = estimated_stride + .clamp(1, u128::from(growth_limit)) + .try_into() + .unwrap_or(u64::MAX); + self.interrupts_until_sample = self.stride; + self.last_sample_at = now; + false + } +} diff --git a/examples/runtime/execution/src/execution.js b/examples/runtime/execution/src/execution.js index c6e563bd..c82a76be 100644 --- a/examples/runtime/execution/src/execution.js +++ b/examples/runtime/execution/src/execution.js @@ -101,11 +101,20 @@ export async function run() { timeoutError = error.message; } let tightLoopTimeoutError; + let tightLoopTimeoutClockReads; try { await runJavaScript({ source: `while (true) {}`, timeoutMs: 5 }); } catch (error) { tightLoopTimeoutError = error.message; + tightLoopTimeoutClockReads = error.timeoutClockReads; } + const finiteCpu = await runJavaScript({ source: ` + let sum = 0; + for (let i = 0; i < 2_000_000; i++) sum += i; + return sum > 0 ? 'done' : 'failed'; + `, timeoutMs: 60_000 }); + const finiteCpuValue = finiteCpu.value; + const finiteCpuClockReads = finiteCpu.timeoutClockReads; const cpuBeforeSuspend = startJavaScript({ source: ` console.log('burn:start:' + Date.now()); const burnStarted = Date.now(); @@ -439,7 +448,8 @@ export async function run() { liveStdout, liveStderr, liveResult, ordering, streamedBeforeResult, parentProgress, left, right, defaultEnvironment, explicitEnvironment, packageCacheFirst, packageCacheSecond, - timeoutSuccess, timeoutError, tightLoopTimeoutError, + timeoutSuccess, timeoutError, tightLoopTimeoutError, tightLoopTimeoutClockReads, + finiteCpuValue, finiteCpuClockReads, cpuBeforeSuspendTimeoutError, cpuBeforeSuspendElapsedMs, zeroTimeoutCode, hugeTimeoutCode, invalidProgramOptions, overflowError, truncated, entry, defaultArgv, largeJavaScript, diff --git a/tests/runtime/execution.rs b/tests/runtime/execution.rs index 33188ac0..d5dc013d 100644 --- a/tests/runtime/execution.rs +++ b/tests/runtime/execution.rs @@ -2,17 +2,108 @@ use crate::common::{CompiledTest, FeatureCombination, invoke_and_capture_output} use camino::Utf8Path; use test_r::{test, test_dep}; +#[path = "../../crates/wasm-rquickjs/skeleton/src/builtin/execution_timeout.rs"] +mod execution_timeout; + +#[test] +fn timeout_sampler_uses_sparse_clock_reads() { + let started = std::time::Instant::now(); + let timeout = std::time::Duration::from_secs(1); + let mut sampler = + execution_timeout::ExecutionTimeoutSampler::new(started, started + timeout, timeout); + let mut now = started; + let mut reads = 0; + for _ in 0..200_000 { + now += std::time::Duration::from_micros(10); + if sampler.expired(|| { + reads += 1; + now + }) { + assert!(now >= started + timeout); + assert!(reads <= 30, "too many clock reads: {reads}"); + assert!(sampler.expired(|| panic!("expired sampler read the clock again"))); + return; + } + } + panic!("continuing CPU work never observed the deadline"); +} + +#[test] +fn timeout_sampler_adapts_to_changed_interrupt_rate() { + let started = std::time::Instant::now(); + let timeout = std::time::Duration::from_secs(1); + let mut sampler = + execution_timeout::ExecutionTimeoutSampler::new(started, started + timeout, timeout); + let mut now = started; + let mut reads = 0; + for interrupt in 0..200_000 { + now += std::time::Duration::from_micros(if interrupt < 50_000 { 10 } else { 100 }); + if sampler.expired(|| { + reads += 1; + now + }) { + assert!(now >= started + timeout); + assert!(reads < 50, "too many clock reads: {reads}"); + return; + } + } + panic!("continuing CPU work never observed the deadline after rate change"); +} + +#[test] +fn timeout_sampler_does_not_check_at_completion() { + let started = std::time::Instant::now(); + let timeout = std::time::Duration::from_millis(1); + let mut sampler = + execution_timeout::ExecutionTimeoutSampler::new(started, started + timeout, timeout); + let mut reads = 0; + for _ in 0..63 { + assert!(!sampler.expired(|| { + reads += 1; + started + timeout * 2 + })); + } + assert_eq!(reads, 0); +} + #[test_dep(tagged_as = "execution", scope = Cloneable)] async fn compiled_execution() -> CompiledTest { CompiledTest::new_with_features( Utf8Path::new("examples/runtime/execution"), true, - FeatureCombination::Normal, + FeatureCombination::InternalTestExecution, ) .await .expect("Failed to compile execution") } +#[test_dep(tagged_as = "execution_normal", scope = Cloneable)] +async fn compiled_execution_normal() -> CompiledTest { + CompiledTest::new_with_features( + Utf8Path::new("examples/runtime/execution"), + true, + FeatureCombination::Normal, + ) + .await + .expect("Failed to compile normal execution") +} + +#[test] +async fn execution_normal_timeout_smoke( + #[tagged_as("execution_normal")] compiled: &CompiledTest, +) -> anyhow::Result<()> { + let (result, _) = invoke_and_capture_output(compiled.wasm_path(), None, "run", &[]).await; + let Some(wasmtime::component::Val::String(json)) = result? else { + anyhow::bail!("expected JSON string result"); + }; + let report: serde_json::Value = serde_json::from_str(&json)?; + assert_eq!(report["tightLoopTimeoutError"], "execution job timed out"); + assert_eq!(report["finiteCpuValue"], "done"); + assert!(report.get("tightLoopTimeoutClockReads").is_none()); + assert!(report.get("finiteCpuClockReads").is_none()); + Ok(()) +} + #[test] async fn execution_isolation( #[tagged_as("execution")] compiled: &CompiledTest, @@ -67,6 +158,21 @@ async fn execution_isolation( assert_eq!(report["timeoutSuccess"]["value"], "quick"); assert_eq!(report["timeoutError"], "execution job timed out"); assert_eq!(report["tightLoopTimeoutError"], "execution job timed out"); + println!( + "timeout clock reads: tight loop={}, finite CPU={}", + report["tightLoopTimeoutClockReads"], report["finiteCpuClockReads"] + ); + assert!( + report["tightLoopTimeoutClockReads"] + .as_u64() + .is_some_and(|reads| (1..=30).contains(&reads)) + ); + assert_eq!(report["finiteCpuValue"], "done"); + assert!( + report["finiteCpuClockReads"] + .as_u64() + .is_some_and(|reads| reads <= 10) + ); assert_eq!( report["cpuBeforeSuspendTimeoutError"], "execution job timed out" From 560ad27e10d5014e50da614c5348d9476d31d918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81ro=CC=81?= Date: Thu, 17 Sep 2026 13:19:28 +0200 Subject: [PATCH 2/2] Simplify adaptive timeout test coverage --- .../skeleton/src/builtin/execution.js | 7 +-- .../skeleton/src/builtin/execution.rs | 18 +------- examples/runtime/execution/src/execution.js | 12 +---- tests/runtime/execution.rs | 44 +------------------ 4 files changed, 4 insertions(+), 77 deletions(-) diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/execution.js b/crates/wasm-rquickjs/skeleton/src/builtin/execution.js index e6e6635d..e588c868 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/execution.js +++ b/crates/wasm-rquickjs/skeleton/src/builtin/execution.js @@ -89,16 +89,11 @@ export function startJavaScript(options) { if (!state.done) continue; settled = true; stdout.end(); stderr.end(); forget_job(id); - if (state.error !== null) { - const error = new Error(state.error); - if (state.timeoutClockReads !== undefined) error.timeoutClockReads = state.timeoutClockReads; - rejectResult(error); - } + if (state.error !== null) rejectResult(new Error(state.error)); else { try { const completed = { value: deserializeFromTransport(state.value), overflowed: state.overflowed }; if (state.profile !== undefined) completed.profile = state.profile; - if (state.timeoutClockReads !== undefined) completed.timeoutClockReads = state.timeoutClockReads; resolveResult(completed); } catch (error) { rejectResult(error); } diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/execution.rs b/crates/wasm-rquickjs/skeleton/src/builtin/execution.rs index 1336a4dd..9d659f8c 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/execution.rs +++ b/crates/wasm-rquickjs/skeleton/src/builtin/execution.rs @@ -14,8 +14,6 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -#[cfg(feature = "internal-test-execution")] -use std::sync::atomic::AtomicU64; use std::task::Poll; use std::time::{Duration, Instant}; @@ -69,8 +67,6 @@ pub(crate) struct ExecutionJob { cancel: Arc, overflowed: Arc, timed_out: Arc, - #[cfg(feature = "internal-test-execution")] - timeout_clock_reads: Arc, forgotten: AtomicBool, completion: RefCell>>, event_waker: AtomicWaker, @@ -94,8 +90,6 @@ impl ExecutionJob { cancel: Arc::new(AtomicBool::new(false)), overflowed: Arc::new(AtomicBool::new(false)), timed_out: Arc::new(AtomicBool::new(false)), - #[cfg(feature = "internal-test-execution")] - timeout_clock_reads: Arc::new(AtomicU64::new(0)), forgotten: AtomicBool::new(false), completion: RefCell::default(), event_waker: AtomicWaker::new(), @@ -216,8 +210,6 @@ struct PollResult { value: Option, error: Option, overflowed: bool, - #[cfg(feature = "internal-test-execution")] - timeout_clock_reads: u64, #[cfg(feature = "typescript-compiler-profiling")] #[serde(skip_serializing_if = "Option::is_none")] profile: Option, @@ -340,8 +332,6 @@ pub mod native_module { value, error, overflowed: job.overflowed.load(Ordering::Relaxed), - #[cfg(feature = "internal-test-execution")] - timeout_clock_reads: job.timeout_clock_reads.load(Ordering::Relaxed), #[cfg(feature = "typescript-compiler-profiling")] profile: job.profile.borrow_mut().take(), }) @@ -510,8 +500,6 @@ async fn run_job(options: ExecutionOptions, job: Rc) { }); let cancelled = job.cancel.clone(); let timed_out = job.timed_out.clone(); - #[cfg(feature = "internal-test-execution")] - let timeout_clock_reads = job.timeout_clock_reads.clone(); runtime .rt .set_interrupt_handler(Some(Box::new(move || { @@ -519,11 +507,7 @@ async fn run_job(options: ExecutionOptions, job: Rc) { return true; } if timeout_sampler.as_mut().is_some_and(|sampler| { - sampler.expired(|| { - #[cfg(feature = "internal-test-execution")] - timeout_clock_reads.fetch_add(1, Ordering::Relaxed); - Instant::now() - }) + sampler.expired(Instant::now) }) { timed_out.store(true, Ordering::Relaxed); return true; diff --git a/examples/runtime/execution/src/execution.js b/examples/runtime/execution/src/execution.js index c82a76be..c6e563bd 100644 --- a/examples/runtime/execution/src/execution.js +++ b/examples/runtime/execution/src/execution.js @@ -101,20 +101,11 @@ export async function run() { timeoutError = error.message; } let tightLoopTimeoutError; - let tightLoopTimeoutClockReads; try { await runJavaScript({ source: `while (true) {}`, timeoutMs: 5 }); } catch (error) { tightLoopTimeoutError = error.message; - tightLoopTimeoutClockReads = error.timeoutClockReads; } - const finiteCpu = await runJavaScript({ source: ` - let sum = 0; - for (let i = 0; i < 2_000_000; i++) sum += i; - return sum > 0 ? 'done' : 'failed'; - `, timeoutMs: 60_000 }); - const finiteCpuValue = finiteCpu.value; - const finiteCpuClockReads = finiteCpu.timeoutClockReads; const cpuBeforeSuspend = startJavaScript({ source: ` console.log('burn:start:' + Date.now()); const burnStarted = Date.now(); @@ -448,8 +439,7 @@ export async function run() { liveStdout, liveStderr, liveResult, ordering, streamedBeforeResult, parentProgress, left, right, defaultEnvironment, explicitEnvironment, packageCacheFirst, packageCacheSecond, - timeoutSuccess, timeoutError, tightLoopTimeoutError, tightLoopTimeoutClockReads, - finiteCpuValue, finiteCpuClockReads, + timeoutSuccess, timeoutError, tightLoopTimeoutError, cpuBeforeSuspendTimeoutError, cpuBeforeSuspendElapsedMs, zeroTimeoutCode, hugeTimeoutCode, invalidProgramOptions, overflowError, truncated, entry, defaultArgv, largeJavaScript, diff --git a/tests/runtime/execution.rs b/tests/runtime/execution.rs index d5dc013d..ae55e4a2 100644 --- a/tests/runtime/execution.rs +++ b/tests/runtime/execution.rs @@ -68,40 +68,13 @@ fn timeout_sampler_does_not_check_at_completion() { #[test_dep(tagged_as = "execution", scope = Cloneable)] async fn compiled_execution() -> CompiledTest { - CompiledTest::new_with_features( - Utf8Path::new("examples/runtime/execution"), - true, - FeatureCombination::InternalTestExecution, - ) - .await - .expect("Failed to compile execution") -} - -#[test_dep(tagged_as = "execution_normal", scope = Cloneable)] -async fn compiled_execution_normal() -> CompiledTest { CompiledTest::new_with_features( Utf8Path::new("examples/runtime/execution"), true, FeatureCombination::Normal, ) .await - .expect("Failed to compile normal execution") -} - -#[test] -async fn execution_normal_timeout_smoke( - #[tagged_as("execution_normal")] compiled: &CompiledTest, -) -> anyhow::Result<()> { - let (result, _) = invoke_and_capture_output(compiled.wasm_path(), None, "run", &[]).await; - let Some(wasmtime::component::Val::String(json)) = result? else { - anyhow::bail!("expected JSON string result"); - }; - let report: serde_json::Value = serde_json::from_str(&json)?; - assert_eq!(report["tightLoopTimeoutError"], "execution job timed out"); - assert_eq!(report["finiteCpuValue"], "done"); - assert!(report.get("tightLoopTimeoutClockReads").is_none()); - assert!(report.get("finiteCpuClockReads").is_none()); - Ok(()) + .expect("Failed to compile execution") } #[test] @@ -158,21 +131,6 @@ async fn execution_isolation( assert_eq!(report["timeoutSuccess"]["value"], "quick"); assert_eq!(report["timeoutError"], "execution job timed out"); assert_eq!(report["tightLoopTimeoutError"], "execution job timed out"); - println!( - "timeout clock reads: tight loop={}, finite CPU={}", - report["tightLoopTimeoutClockReads"], report["finiteCpuClockReads"] - ); - assert!( - report["tightLoopTimeoutClockReads"] - .as_u64() - .is_some_and(|reads| (1..=30).contains(&reads)) - ); - assert_eq!(report["finiteCpuValue"], "done"); - assert!( - report["finiteCpuClockReads"] - .as_u64() - .is_some_and(|reads| reads <= 10) - ); assert_eq!( report["cpuBeforeSuspendTimeoutError"], "execution job timed out"