From 2f42ae09cfb1fbf975b3cdecb07cdd97f862b598 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Thu, 7 May 2026 11:34:10 -0300 Subject: [PATCH 1/7] Remove thread from WaitAsyncJob --- .../structured_data/atomics_object.rs | 116 +++++++++--------- .../src/ecmascript/types/spec/data_block.rs | 6 +- 2 files changed, 60 insertions(+), 62 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 1a530432b..4d68e938f 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -6,8 +6,7 @@ use std::{ hint::assert_unchecked, ops::ControlFlow, sync::Arc, - thread::{self, JoinHandle}, - time::Duration, + time::{Duration, Instant}, }; use ecmascript_atomics::Ordering; @@ -1621,52 +1620,65 @@ fn create_wait_result_object<'gc>( .expect("Should perform GC here") } -#[derive(Debug)] struct WaitAsyncJobInner { + data_block: SharedDataBlock, + byte_index_in_buffer: usize, + waiter_record: Arc, promise_to_resolve: Global>, - join_handle: JoinHandle, - _has_timeout: bool, + created_at: Instant, + t: u64, } -#[derive(Debug)] #[repr(transparent)] pub(crate) struct WaitAsyncJob(Box); impl WaitAsyncJob { pub(crate) fn is_finished(&self) -> bool { - self.0.join_handle.is_finished() + let is_notified = self.0.waiter_record.is_notified(); + let timeout_expired = + self.0.t != u64::MAX && self.0.created_at.elapsed() >= Duration::from_millis(self.0.t); + is_notified || timeout_expired } pub(crate) fn _will_halt(&self) -> bool { - self.0._has_timeout + self.0.t != u64::MAX } - // NOTE: The reason for using `GcScope` here even though we could've gotten - // away with `NoGcScope` is that this is essentially a trait impl method, - // but currently without the trait. The job trait will be added eventually - // and we can get rid of this lint exception. - #[allow(unknown_lints, can_use_no_gc_scope)] - pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope) -> JsResult<'gc, ()> { + pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { let gc = gc.into_nogc(); - let promise = self.0.promise_to_resolve.take(agent).bind(gc); - let Ok(result) = self.0.join_handle.join() else { - // Foreign thread died; we can never resolve. - return Ok(()); - }; + + // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. + let waiters = unsafe { self.0.data_block.get_or_init_waiters() }; // a. Perform EnterCriticalSection(WL). - // b. If WL.[[Waiters]] contains waiterRecord, then - // i. Let timeOfJobExecution be the time value (UTC) identifying the current time. - // ii. Assert: ℝ(timeOfJobExecution) ≥ waiterRecord.[[TimeoutTime]] (ignoring potential non-monotonicity of time values). - // iii. Set waiterRecord.[[Result]] to "timed-out". - // iv. Perform RemoveWaiter(WL, waiterRecord). - // v. Perform NotifyWaiter(WL, waiterRecord). - // c. Perform LeaveCriticalSection(WL). - let promise_capability = PromiseCapability::from_promise(promise, true); - let result = match result { - WaitResult::Ok => BUILTIN_STRING_MEMORY.ok.into(), - WaitResult::TimedOut => BUILTIN_STRING_MEMORY.timed_out.into(), - }; - unwrap_try(promise_capability.try_resolve(agent, result, gc)); + let mut guard = waiters.lock().unwrap(); + + if self.0.waiter_record.is_notified() { + let promise = self.0.promise_to_resolve.take(agent).bind(gc); + let promise_capability = PromiseCapability::from_promise(promise, true); + unwrap_try(promise_capability.try_resolve(agent, BUILTIN_STRING_MEMORY.ok.into(), gc)); + drop(guard); + } else { + // b. If WL.[[Waiters]] contains waiterRecord, then + // i. Let timeOfJobExecution be the time value (UTC) identifying the current time. + // ii. Assert: ℝ(timeOfJobExecution) ≥ waiterRecord.[[TimeoutTime]] (ignoring potential non-monotonicity of time values). + // iii. Set waiterRecord.[[Result]] to "timed-out". + // iv. Perform RemoveWaiter(WL, waiterRecord). + // v. Perform NotifyWaiter(WL, waiterRecord). + + guard.remove_from_list(self.0.byte_index_in_buffer, self.0.waiter_record); + + let promise = self.0.promise_to_resolve.take(agent).bind(gc); + let promise_capability = PromiseCapability::from_promise(promise, true); + unwrap_try(promise_capability.try_resolve( + agent, + BUILTIN_STRING_MEMORY.timed_out.into(), + gc, + )); + + // c. Perform LeaveCriticalSection(WL). + drop(guard); + } + // d. Return unused. Ok(()) } @@ -1689,40 +1701,22 @@ fn enqueue_atomics_wait_async_job( // 1. Let timeoutJob be a new Job Abstract Closure with no parameters that // captures WL and waiterRecord and performs the following steps when // called: - let handle = thread::spawn(move || { - // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. - let waiters = unsafe { data_block.get_or_init_waiters() }; - let mut guard = waiters.lock().unwrap(); - - if t == u64::MAX { - waiter_record.wait(guard); - } else { - let dur = Duration::from_millis(t); - let (new_guard, timeout) = waiter_record.wait_timeout(guard, dur); - guard = new_guard; - if timeout.timed_out() { - guard.remove_from_list(byte_index_in_buffer, waiter_record); - - // 31. Perform LeaveCriticalSection(WL). - drop(guard); - - // 32. If mode is sync, return waiterRecord.[[Result]]. - return WaitResult::TimedOut; - } - } - WaitResult::Ok - }); + // 2. Let now be the time value (UTC) identifying the current time. + // 3. Let currentRealm be the current Realm Record. + // 4. Perform HostEnqueueTimeoutJob(timeoutJob, currentRealm, 𝔽(waiterRecord.[[TimeoutTime]]) - now). let wait_async_job = Job { realm: Some(Global::new(agent, agent.current_realm(gc).unbind())), inner: InnerJob::WaitAsync(WaitAsyncJob(Box::new(WaitAsyncJobInner { + data_block, + byte_index_in_buffer, + waiter_record, promise_to_resolve: promise, - join_handle: handle, - _has_timeout: t != u64::MAX, + t, + created_at: Instant::now(), }))), }; - // 2. Let now be the time value (UTC) identifying the current time. - // 3. Let currentRealm be the current Realm Record. - // 4. Perform HostEnqueueTimeoutJob(timeoutJob, currentRealm, 𝔽(waiterRecord.[[TimeoutTime]]) - now). - agent.host_hooks.enqueue_generic_job(wait_async_job); + agent.host_hooks.enqueue_timeout_job(wait_async_job, t); + // agent.host_hooks.enqueue_generic_job(wait_async_job); + // 5. Return unused. } diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 5efb6cfdd..189aea3c2 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -426,7 +426,7 @@ impl SharedDataBlockMaxByteLength { } #[cfg(feature = "shared-array-buffer")] -#[derive(Default)] +#[derive(Default, Debug)] pub(crate) struct WaiterRecord { condvar: Condvar, notified: AtomicBool, @@ -472,6 +472,10 @@ impl WaiterRecord { ), } } + + pub(crate) fn is_notified(&self) -> bool { + self.notified.load(Ordering::Relaxed) + } } /// Result of an `Atomics.wait` or `Atomics.waitAsync` operation. From b47b6cf306844f321129da2ce76d52de1ad57922 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Thu, 7 May 2026 11:41:38 -0300 Subject: [PATCH 2/7] Remove unused code --- .../builtins/structured_data/atomics_object.rs | 14 +++++++------- nova_vm/src/ecmascript/types/spec/data_block.rs | 8 -------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 4d68e938f..3098d2ed8 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -17,13 +17,13 @@ use crate::{ BigInt, Builtin, ExceptionType, InnerJob, Job, JsResult, Number, Numeric, OrdinaryObject, Promise, PromiseCapability, Realm, SharedArrayBuffer, SharedDataBlock, SharedTypedArray, String, TryError, TryResult, TypedArrayAbstractOperations, - TypedArrayWithBufferWitnessRecords, Value, WaitResult, WaiterRecord, - builders::OrdinaryObjectBuilder, compare_exchange_in_buffer, for_any_typed_array, - get_modify_set_value_in_buffer, get_value_from_buffer, - make_typed_array_with_buffer_witness_record, number_convert_to_integer_or_infinity, - set_value_in_buffer, to_big_int, to_big_int64, to_big_int64_big_int, to_index, to_int32, - to_int32_number, to_integer_number_or_infinity, to_integer_or_infinity, to_number, - try_result_into_js, try_to_index, unwrap_try, validate_index, validate_typed_array, + TypedArrayWithBufferWitnessRecords, Value, WaiterRecord, builders::OrdinaryObjectBuilder, + compare_exchange_in_buffer, for_any_typed_array, get_modify_set_value_in_buffer, + get_value_from_buffer, make_typed_array_with_buffer_witness_record, + number_convert_to_integer_or_infinity, set_value_in_buffer, to_big_int, to_big_int64, + to_big_int64_big_int, to_index, to_int32, to_int32_number, to_integer_number_or_infinity, + to_integer_or_infinity, to_number, try_result_into_js, try_to_index, unwrap_try, + validate_index, validate_typed_array, }, engine::{Bindable, GcScope, Global, NoGcScope, Scopable}, heap::{ObjectEntry, WellKnownSymbols}, diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 189aea3c2..de1c2a12b 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -478,14 +478,6 @@ impl WaiterRecord { } } -/// Result of an `Atomics.wait` or `Atomics.waitAsync` operation. -#[derive(Debug)] -#[cfg(feature = "shared-array-buffer")] -pub(crate) enum WaitResult { - Ok, - TimedOut, -} - #[cfg(feature = "shared-array-buffer")] #[derive(Default)] #[repr(transparent)] From 058b25559605d5bddebd56b5148f167189509c17 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Thu, 7 May 2026 13:16:18 -0300 Subject: [PATCH 3/7] Implement `enqueue_timeout_job` --- nova_cli/src/lib/child_hooks.rs | 21 ++++++++++++++------- nova_cli/src/lib/host_hooks.rs | 29 +++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/nova_cli/src/lib/child_hooks.rs b/nova_cli/src/lib/child_hooks.rs index 343d21934..06957b469 100644 --- a/nova_cli/src/lib/child_hooks.rs +++ b/nova_cli/src/lib/child_hooks.rs @@ -10,7 +10,7 @@ use std::{ collections::VecDeque, sync::{atomic::AtomicBool, mpsc}, thread, - time::Duration, + time::{Duration, Instant}, }; use nova_vm::ecmascript::{HostHooks, Job}; @@ -19,7 +19,7 @@ use crate::{ChildToHostMessage, HostToChildMessage}; pub struct CliChildHooks { promise_job_queue: RefCell>, - macrotask_queue: RefCell>, + macrotask_queue: RefCell, Job)>>, pub(crate) receiver: mpsc::Receiver, pub(crate) host_sender: mpsc::SyncSender, ready_to_leave: AtomicBool, @@ -78,9 +78,11 @@ impl CliChildHooks { let mut counter = 0u8; while !off_thread_job_queue.is_empty() { counter = counter.wrapping_add(1); - for (i, job) in off_thread_job_queue.iter().enumerate() { - if job.is_finished() { - let job = off_thread_job_queue.swap_remove(i); + let now = Instant::now(); + for (i, (deadline, job)) in off_thread_job_queue.iter().enumerate() { + let deadline_reached = deadline.map_or(true, |d| now >= d); + if deadline_reached && job.is_finished() { + let (_, job) = off_thread_job_queue.swap_remove(i); return Some(job); } } @@ -96,14 +98,19 @@ impl CliChildHooks { impl HostHooks for CliChildHooks { fn enqueue_generic_job(&self, job: Job) { - self.macrotask_queue.borrow_mut().push(job); + self.macrotask_queue.borrow_mut().push((None, job)); } fn enqueue_promise_job(&self, job: Job) { self.promise_job_queue.borrow_mut().push_back(job); } - fn enqueue_timeout_job(&self, _timeout_job: Job, _milliseconds: u64) {} + fn enqueue_timeout_job(&self, timeout_job: Job, milliseconds: u64) { + let deadline = Instant::now() + Duration::from_millis(milliseconds); + self.macrotask_queue + .borrow_mut() + .push((Some(deadline), timeout_job)); + } fn get_host_data(&self) -> &dyn std::any::Any { self diff --git a/nova_cli/src/lib/host_hooks.rs b/nova_cli/src/lib/host_hooks.rs index 71444afaa..545cf5b23 100644 --- a/nova_cli/src/lib/host_hooks.rs +++ b/nova_cli/src/lib/host_hooks.rs @@ -5,8 +5,14 @@ //! The [`HostHooks`] implementation for the main thread. use std::{ - cell::RefCell, collections::VecDeque, fmt::Debug, path::PathBuf, rc::Rc, sync::mpsc, thread, - time::Duration, + cell::RefCell, + collections::VecDeque, + fmt::Debug, + path::PathBuf, + rc::Rc, + sync::mpsc, + thread, + time::{Duration, Instant}, }; use nova_vm::{ @@ -29,7 +35,7 @@ pub enum ChildToHostMessage { pub struct CliHostHooks { promise_job_queue: RefCell>, - macrotask_queue: RefCell>, + macrotask_queue: RefCell, Job)>>, pub(crate) receiver: mpsc::Receiver, pub(crate) own_sender: mpsc::SyncSender, pub(crate) child_senders: RefCell>>, @@ -83,9 +89,11 @@ impl CliHostHooks { let mut counter = 0u8; while !off_thread_job_queue.is_empty() { counter = counter.wrapping_add(1); - for (i, job) in off_thread_job_queue.iter().enumerate() { - if job.is_finished() { - let job = off_thread_job_queue.swap_remove(i); + let now = Instant::now(); + for (i, (deadline, job)) in off_thread_job_queue.iter().enumerate() { + let deadline_reached = deadline.map_or(true, |d| now >= d); + if deadline_reached && job.is_finished() { + let (_, job) = off_thread_job_queue.swap_remove(i); return Some(job); } } @@ -101,14 +109,19 @@ impl CliHostHooks { impl HostHooks for CliHostHooks { fn enqueue_generic_job(&self, job: Job) { - self.macrotask_queue.borrow_mut().push(job); + self.macrotask_queue.borrow_mut().push((None, job)); } fn enqueue_promise_job(&self, job: Job) { self.promise_job_queue.borrow_mut().push_back(job); } - fn enqueue_timeout_job(&self, _timeout_job: Job, _milliseconds: u64) {} + fn enqueue_timeout_job(&self, timeout_job: Job, milliseconds: u64) { + let deadline = Instant::now() + Duration::from_millis(milliseconds); + self.macrotask_queue + .borrow_mut() + .push((Some(deadline), timeout_job)); + } fn load_imported_module<'gc>( &self, From 0ec4eedca96f8fc1af8b3782963104e7b91574aa Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Thu, 7 May 2026 16:41:24 -0300 Subject: [PATCH 4/7] Timeout implemented by host system --- .../structured_data/atomics_object.rs | 144 +++++++++++++----- nova_vm/src/ecmascript/execution/agent.rs | 8 +- .../src/ecmascript/types/spec/data_block.rs | 37 ++++- 3 files changed, 146 insertions(+), 43 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 3098d2ed8..ae6054167 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -17,13 +17,13 @@ use crate::{ BigInt, Builtin, ExceptionType, InnerJob, Job, JsResult, Number, Numeric, OrdinaryObject, Promise, PromiseCapability, Realm, SharedArrayBuffer, SharedDataBlock, SharedTypedArray, String, TryError, TryResult, TypedArrayAbstractOperations, - TypedArrayWithBufferWitnessRecords, Value, WaiterRecord, builders::OrdinaryObjectBuilder, - compare_exchange_in_buffer, for_any_typed_array, get_modify_set_value_in_buffer, - get_value_from_buffer, make_typed_array_with_buffer_witness_record, - number_convert_to_integer_or_infinity, set_value_in_buffer, to_big_int, to_big_int64, - to_big_int64_big_int, to_index, to_int32, to_int32_number, to_integer_number_or_infinity, - to_integer_or_infinity, to_number, try_result_into_js, try_to_index, unwrap_try, - validate_index, validate_typed_array, + TypedArrayWithBufferWitnessRecords, Value, WaitResult, WaiterRecord, + builders::OrdinaryObjectBuilder, compare_exchange_in_buffer, for_any_typed_array, + get_modify_set_value_in_buffer, get_value_from_buffer, + make_typed_array_with_buffer_witness_record, number_convert_to_integer_or_infinity, + set_value_in_buffer, to_big_int, to_big_int64, to_big_int64_big_int, to_index, to_int32, + to_int32_number, to_integer_number_or_infinity, to_integer_or_infinity, to_number, + try_result_into_js, try_to_index, unwrap_try, validate_index, validate_typed_array, }, engine::{Bindable, GcScope, Global, NoGcScope, Scopable}, heap::{ObjectEntry, WellKnownSymbols}, @@ -1625,7 +1625,6 @@ struct WaitAsyncJobInner { byte_index_in_buffer: usize, waiter_record: Arc, promise_to_resolve: Global>, - created_at: Instant, t: u64, } @@ -1634,10 +1633,7 @@ pub(crate) struct WaitAsyncJob(Box); impl WaitAsyncJob { pub(crate) fn is_finished(&self) -> bool { - let is_notified = self.0.waiter_record.is_notified(); - let timeout_expired = - self.0.t != u64::MAX && self.0.created_at.elapsed() >= Duration::from_millis(self.0.t); - is_notified || timeout_expired + self.0.waiter_record.is_notified() } pub(crate) fn _will_halt(&self) -> bool { @@ -1651,34 +1647,85 @@ impl WaitAsyncJob { let waiters = unsafe { self.0.data_block.get_or_init_waiters() }; // a. Perform EnterCriticalSection(WL). let mut guard = waiters.lock().unwrap(); + let waiter_record = self.0.waiter_record; + guard.remove_from_list(self.0.byte_index_in_buffer, waiter_record.clone()); + + let result = match waiter_record.get_result() { + Some(WaitResult::TimedOut) => WaitResult::TimedOut, + Some(WaitResult::Ok) => WaitResult::Ok, + None => { + waiter_record.set_result(WaitResult::Ok); + WaitResult::Ok + } + }; - if self.0.waiter_record.is_notified() { - let promise = self.0.promise_to_resolve.take(agent).bind(gc); - let promise_capability = PromiseCapability::from_promise(promise, true); - unwrap_try(promise_capability.try_resolve(agent, BUILTIN_STRING_MEMORY.ok.into(), gc)); - drop(guard); - } else { - // b. If WL.[[Waiters]] contains waiterRecord, then - // i. Let timeOfJobExecution be the time value (UTC) identifying the current time. - // ii. Assert: ℝ(timeOfJobExecution) ≥ waiterRecord.[[TimeoutTime]] (ignoring potential non-monotonicity of time values). - // iii. Set waiterRecord.[[Result]] to "timed-out". - // iv. Perform RemoveWaiter(WL, waiterRecord). - // v. Perform NotifyWaiter(WL, waiterRecord). - - guard.remove_from_list(self.0.byte_index_in_buffer, self.0.waiter_record); - - let promise = self.0.promise_to_resolve.take(agent).bind(gc); - let promise_capability = PromiseCapability::from_promise(promise, true); - unwrap_try(promise_capability.try_resolve( - agent, - BUILTIN_STRING_MEMORY.timed_out.into(), - gc, - )); + let promise = self.0.promise_to_resolve.take(agent).bind(gc); + let promise_capability = PromiseCapability::from_promise(promise, true); + match result { + WaitResult::Ok => { + unwrap_try(promise_capability.try_resolve( + agent, + BUILTIN_STRING_MEMORY.ok.into(), + gc, + )); + } + WaitResult::TimedOut => { + unwrap_try(promise_capability.try_resolve( + agent, + BUILTIN_STRING_MEMORY.timed_out.into(), + gc, + )); + } + } + // c. Perform LeaveCriticalSection(WL). + drop(guard); + + // d. Return unused. + Ok(()) + } +} + +struct WaitAsyncTimeoutJobInner { + data_block: SharedDataBlock, + byte_index_in_buffer: usize, + waiter_record: Arc, +} - // c. Perform LeaveCriticalSection(WL). - drop(guard); +pub(crate) struct WaitAsyncTimeoutJob(Box); + +impl WaitAsyncTimeoutJob { + pub(crate) fn is_finished(&self) -> bool { + true // Always execute when the timeout is reached + } + + pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { + let gc = gc.into_nogc(); + + if self.0.waiter_record.get_result().is_some() { + return Ok(()); } + // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. + let waiters = unsafe { self.0.data_block.get_or_init_waiters() }; + // a. Perform EnterCriticalSection(WL). + let mut guard = waiters.lock().unwrap(); + + // b. If WL.[[Waiters]] contains waiterRecord, then + // i. Let timeOfJobExecution be the time value (UTC) identifying the current time. + // ii. Assert: ℝ(timeOfJobExecution) ≥ waiterRecord.[[TimeoutTime]] (ignoring potential non-monotonicity of time values). + // iii. Set waiterRecord.[[Result]] to "timed-out". + self.0.waiter_record.set_result(WaitResult::TimedOut); + + // iv. Perform RemoveWaiter(WL, waiterRecord). + let waiter_record = self.0.waiter_record.clone(); + guard.remove_from_list(self.0.byte_index_in_buffer, self.0.waiter_record); + + // v. Perform NotifyWaiter(WL, waiterRecord). + waiter_record.notify_waiters(); + + // c. Perform LeaveCriticalSection(WL). + drop(guard); + // d. Return unused. Ok(()) } @@ -1704,6 +1751,17 @@ fn enqueue_atomics_wait_async_job( // 2. Let now be the time value (UTC) identifying the current time. // 3. Let currentRealm be the current Realm Record. // 4. Perform HostEnqueueTimeoutJob(timeoutJob, currentRealm, 𝔽(waiterRecord.[[TimeoutTime]]) - now). + + let timeout_job_data = if t != u64::MAX { + Some(WaitAsyncTimeoutJobInner { + data_block: data_block.clone(), + byte_index_in_buffer, + waiter_record: waiter_record.clone(), + }) + } else { + None + }; + let wait_async_job = Job { realm: Some(Global::new(agent, agent.current_realm(gc).unbind())), inner: InnerJob::WaitAsync(WaitAsyncJob(Box::new(WaitAsyncJobInner { @@ -1712,11 +1770,19 @@ fn enqueue_atomics_wait_async_job( waiter_record, promise_to_resolve: promise, t, - created_at: Instant::now(), }))), }; - agent.host_hooks.enqueue_timeout_job(wait_async_job, t); - // agent.host_hooks.enqueue_generic_job(wait_async_job); + agent.host_hooks.enqueue_generic_job(wait_async_job); + + if let Some(inner) = timeout_job_data { + let wait_async_timeout_job = Job { + realm: Some(Global::new(agent, agent.current_realm(gc).unbind())), + inner: InnerJob::WaitAsyncTimeout(WaitAsyncTimeoutJob(Box::new(inner))), + }; + agent + .host_hooks + .enqueue_timeout_job(wait_async_timeout_job, t); + } // 5. Return unused. } diff --git a/nova_vm/src/ecmascript/execution/agent.rs b/nova_vm/src/ecmascript/execution/agent.rs index 1289c6be8..c3535364a 100644 --- a/nova_vm/src/ecmascript/execution/agent.rs +++ b/nova_vm/src/ecmascript/execution/agent.rs @@ -25,10 +25,10 @@ use ahash::AHashMap; use crate::ecmascript::GlobalEnvironment; #[cfg(feature = "shared-array-buffer")] use crate::ecmascript::SharedArrayBuffer; -#[cfg(feature = "atomics")] -use crate::ecmascript::WaitAsyncJob; #[cfg(feature = "weak-refs")] use crate::ecmascript::{FinalizationRegistryCleanupJob, clear_kept_objects}; +#[cfg(feature = "atomics")] +use crate::ecmascript::{WaitAsyncJob, WaitAsyncTimeoutJob}; use crate::{ ecmascript::{ AbstractModuleMethods, Environment, ErrorHeapData, ExecutionContext, Function, @@ -258,6 +258,8 @@ pub(crate) enum InnerJob { PromiseReaction(PromiseReactionJob), #[cfg(feature = "atomics")] WaitAsync(WaitAsyncJob), + #[cfg(feature = "atomics")] + WaitAsyncTimeout(WaitAsyncTimeoutJob), #[cfg(feature = "weak-refs")] FinalizationRegistry(FinalizationRegistryCleanupJob), } @@ -315,6 +317,8 @@ impl Job { InnerJob::PromiseReaction(job) => job.run(agent, gc), #[cfg(feature = "atomics")] InnerJob::WaitAsync(job) => job.run(agent, gc), + #[cfg(feature = "atomics")] + InnerJob::WaitAsyncTimeout(job) => job.run(agent, gc), #[cfg(feature = "weak-refs")] InnerJob::FinalizationRegistry(job) => { job.run(agent, gc); diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index de1c2a12b..003baadb1 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -10,7 +10,7 @@ use std::{ hint::assert_unchecked, sync::{ Arc, Condvar, Mutex, MutexGuard, WaitTimeoutResult, - atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicPtr, AtomicU8, AtomicUsize, Ordering}, }, time::Duration, }; @@ -426,10 +426,22 @@ impl SharedDataBlockMaxByteLength { } #[cfg(feature = "shared-array-buffer")] -#[derive(Default, Debug)] +#[derive(Debug)] pub(crate) struct WaiterRecord { condvar: Condvar, notified: AtomicBool, + result: AtomicU8, +} + +#[cfg(feature = "shared-array-buffer")] +impl Default for WaiterRecord { + fn default() -> Self { + Self { + condvar: Condvar::default(), + notified: AtomicBool::default(), + result: AtomicU8::new(u8::MAX), + } + } } #[cfg(feature = "shared-array-buffer")] @@ -448,6 +460,7 @@ impl WaiterRecord { let lock_result = self .condvar .wait_while(guard, |_| !self.notified.load(Ordering::Relaxed)); + match lock_result { Ok(_) => (), Err(e) => panic!( @@ -476,6 +489,26 @@ impl WaiterRecord { pub(crate) fn is_notified(&self) -> bool { self.notified.load(Ordering::Relaxed) } + + pub(crate) fn set_result(&self, result: WaitResult) { + self.result.store(result as u8, Ordering::Relaxed); + } + + pub(crate) fn get_result(&self) -> Option { + match self.result.load(Ordering::Relaxed) { + 0 => Some(WaitResult::Ok), + 1 => Some(WaitResult::TimedOut), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(feature = "shared-array-buffer")] +#[repr(u8)] +pub(crate) enum WaitResult { + Ok = 0, + TimedOut = 1, } #[cfg(feature = "shared-array-buffer")] From 6b60e676ae4d8c59aa1cafb7e7b7a57bd8a6abfd Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 8 May 2026 07:17:46 -0300 Subject: [PATCH 5/7] Fix linter --- .../structured_data/atomics_object.rs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index ae6054167..7faa0caed 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -2,12 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use std::{ - hint::assert_unchecked, - ops::ControlFlow, - sync::Arc, - time::{Duration, Instant}, -}; +use std::{hint::assert_unchecked, ops::ControlFlow, sync::Arc, time::Duration}; use ecmascript_atomics::Ordering; @@ -1625,7 +1620,7 @@ struct WaitAsyncJobInner { byte_index_in_buffer: usize, waiter_record: Arc, promise_to_resolve: Global>, - t: u64, + has_timeout: bool, } #[repr(transparent)] @@ -1637,7 +1632,7 @@ impl WaitAsyncJob { } pub(crate) fn _will_halt(&self) -> bool { - self.0.t != u64::MAX + self.0.has_timeout } pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { @@ -1694,13 +1689,7 @@ struct WaitAsyncTimeoutJobInner { pub(crate) struct WaitAsyncTimeoutJob(Box); impl WaitAsyncTimeoutJob { - pub(crate) fn is_finished(&self) -> bool { - true // Always execute when the timeout is reached - } - - pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { - let gc = gc.into_nogc(); - + pub(crate) fn run<'gc>(self, _agent: &mut Agent, _gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { if self.0.waiter_record.get_result().is_some() { return Ok(()); } @@ -1769,7 +1758,7 @@ fn enqueue_atomics_wait_async_job( byte_index_in_buffer, waiter_record, promise_to_resolve: promise, - t, + has_timeout: t != u64::MAX, }))), }; agent.host_hooks.enqueue_generic_job(wait_async_job); From 981dd762ba432f09661008de8b38cf09911e2507 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 8 May 2026 07:20:45 -0300 Subject: [PATCH 6/7] Fix warn --- .../ecmascript/builtins/structured_data/atomics_object.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 7faa0caed..ca6bf2794 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1620,7 +1620,7 @@ struct WaitAsyncJobInner { byte_index_in_buffer: usize, waiter_record: Arc, promise_to_resolve: Global>, - has_timeout: bool, + _has_timeout: bool, } #[repr(transparent)] @@ -1632,7 +1632,7 @@ impl WaitAsyncJob { } pub(crate) fn _will_halt(&self) -> bool { - self.0.has_timeout + self.0._has_timeout } pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { @@ -1758,7 +1758,7 @@ fn enqueue_atomics_wait_async_job( byte_index_in_buffer, waiter_record, promise_to_resolve: promise, - has_timeout: t != u64::MAX, + _has_timeout: t != u64::MAX, }))), }; agent.host_hooks.enqueue_generic_job(wait_async_job); From f475b39265ca7566ba6520d5f908b427869500e6 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 8 May 2026 07:47:50 -0300 Subject: [PATCH 7/7] Fix linter --- nova_cli/src/lib/child_hooks.rs | 2 +- nova_cli/src/lib/host_hooks.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova_cli/src/lib/child_hooks.rs b/nova_cli/src/lib/child_hooks.rs index 06957b469..6ebb14970 100644 --- a/nova_cli/src/lib/child_hooks.rs +++ b/nova_cli/src/lib/child_hooks.rs @@ -80,7 +80,7 @@ impl CliChildHooks { counter = counter.wrapping_add(1); let now = Instant::now(); for (i, (deadline, job)) in off_thread_job_queue.iter().enumerate() { - let deadline_reached = deadline.map_or(true, |d| now >= d); + let deadline_reached = deadline.is_none_or(|d| now >= d); if deadline_reached && job.is_finished() { let (_, job) = off_thread_job_queue.swap_remove(i); return Some(job); diff --git a/nova_cli/src/lib/host_hooks.rs b/nova_cli/src/lib/host_hooks.rs index 545cf5b23..09b3bd88f 100644 --- a/nova_cli/src/lib/host_hooks.rs +++ b/nova_cli/src/lib/host_hooks.rs @@ -91,7 +91,7 @@ impl CliHostHooks { counter = counter.wrapping_add(1); let now = Instant::now(); for (i, (deadline, job)) in off_thread_job_queue.iter().enumerate() { - let deadline_reached = deadline.map_or(true, |d| now >= d); + let deadline_reached = deadline.is_none_or(|d| now >= d); if deadline_reached && job.is_finished() { let (_, job) = off_thread_job_queue.swap_remove(i); return Some(job);