diff --git a/.husky/pre-push b/.husky/pre-push index 7fa476fefcf..ffbe860b734 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -3,7 +3,7 @@ target=$(rustc -vV | awk '/^host/ { print $2 }' | tr '[:lower:]' '[:upper:]' | tr '-' '_') export CARGO_TARGET_${target}_RUSTFLAGS='-D warnings' -if ! command -v cargo-make >/dev/null 2>&1; then +if ! cargo help make >/dev/null 2>&1; then echo "cargo-make is not installed. Install it with:" echo " cargo install cargo-make" exit 1 diff --git a/cli/src/executor.rs b/cli/src/executor.rs index e4c198aba51..3a0c38f97f6 100644 --- a/cli/src/executor.rs +++ b/cli/src/executor.rs @@ -117,8 +117,7 @@ impl Executor { /// Continually run all pending async jobs. // /// This does not need to yield to the async executor after every run because - /// it assumes that every async job will not would never - /// exit. + /// it assumes that every async job will eventually yield to the executor. async fn run_async_jobs(&self, context: &RefCell<&mut Context>) { let mut group = FutureGroup::new(); let mut listener = pin!(EventListener::new(&self.wake_event)); @@ -198,7 +197,7 @@ impl JobExecutor for Executor { Job::TimeoutJob(job) => { let event = Rc::new(Event::new()); let listener = EventListenerRc::new(Rc::clone(&event)); - job.set_cancellation_callback(move || { + job.cancellation_token().push_callback(move |_| { event.notify(u8::MAX); }); self.async_jobs @@ -215,6 +214,32 @@ impl JobExecutor for Executor { timer.or(cancel).await })); } + Job::IntervalJob(job) => { + let event = Rc::new(Event::new()); + let listener = EventListenerRc::new(Rc::clone(&event)); + let printer = self.printer.clone(); + job.cancellation_token().push_callback(move |_| { + event.notify(u8::MAX); + }); + self.async_jobs + .borrow_mut() + .push_back(NativeAsyncJob::new(async move |context| { + let timer = async { + let mut interval = smol::Timer::interval(job.interval().into()); + loop { + interval.next().await; + if let Err(err) = job.call(&mut context.borrow_mut()) { + printer.print(uncaught_job_error(&err)); + } + } + }; + let cancel = async { + listener.await; + Ok(JsValue::undefined()) + }; + timer.or(cancel).await + })); + } Job::GenericJob(job) => self.generic_jobs.borrow_mut().push_back(job), Job::FinalizationRegistryCleanupJob(job) => { self.finalization_registry_jobs.borrow_mut().push_back(job); diff --git a/core/engine/src/builtins/atomics/futex.rs b/core/engine/src/builtins/atomics/futex.rs index 75dbe39366d..ec067423d4d 100644 --- a/core/engine/src/builtins/atomics/futex.rs +++ b/core/engine/src/builtins/atomics/futex.rs @@ -588,7 +588,7 @@ pub(super) unsafe fn wait_async( timeout, ); - let tc = job.cancellation_token(); + let tc = job.cancellation_token().clone(); // 4. Perform HostEnqueueTimeoutJob(timeoutJob, currentRealm, 𝔽(waiterRecord.[[TimeoutTime]]) - now). context.enqueue_job(job.into()); @@ -619,7 +619,7 @@ pub(super) unsafe fn wait_async( } if let Some(token) = timeout_cancel { - token.cancel(); + token.cancel(&mut context.borrow_mut()); } Ok(JsValue::undefined()) diff --git a/core/engine/src/context/mod.rs b/core/engine/src/context/mod.rs index b9791334283..78453d26078 100644 --- a/core/engine/src/context/mod.rs +++ b/core/engine/src/context/mod.rs @@ -1,5 +1,6 @@ //! The ECMAScript context. +use std::any::Any; use std::{cell::Cell, path::Path, rc::Rc}; use boa_ast::StatementList; @@ -19,7 +20,7 @@ use crate::js_error; use crate::module::DynModuleLoader; use crate::vm::{CodeBlock, RuntimeLimits, create_function_object_fast}; use crate::{ - HostDefined, JsNativeError, JsResult, JsString, JsValue, NativeObject, Source, builtins, + HostDefined, JsNativeError, JsResult, JsString, JsValue, Source, builtins, class::{Class, ClassBuilder}, job::{JobExecutor, SimpleJobExecutor}, js_string, @@ -129,7 +130,7 @@ pub struct Context { /// Unique identifier for each parser instance used during the context lifetime. parser_identifier: u32, - data: HostDefined, + data: HostDefined, } impl std::fmt::Debug for Context { @@ -601,29 +602,35 @@ impl Context { self.can_block } + /// Gets a mutable reference to the inner [`HostDefined`] field. + #[inline] + pub fn host_defined_mut(&mut self) -> &mut HostDefined { + &mut self.data + } + /// Insert a type into the context-specific [`HostDefined`] field. #[inline] - pub fn insert_data(&mut self, value: T) -> Option> { + pub fn insert_data(&mut self, value: T) -> Option> { self.data.insert(value) } /// Check if the context-specific [`HostDefined`] has type T. #[inline] #[must_use] - pub fn has_data(&self) -> bool { + pub fn has_data(&self) -> bool { self.data.has::() } /// Remove type T from the context-specific [`HostDefined`], if it exists. #[inline] - pub fn remove_data(&mut self) -> Option> { + pub fn remove_data(&mut self) -> Option> { self.data.remove::() } /// Get type T from the context-specific [`HostDefined`], if it exists. #[inline] #[must_use] - pub fn get_data(&self) -> Option<&T> { + pub fn get_data(&self) -> Option<&T> { self.data.get::() } } diff --git a/core/engine/src/host_defined.rs b/core/engine/src/host_defined.rs index 3585c8ccc10..96ea02e1f46 100644 --- a/core/engine/src/host_defined.rs +++ b/core/engine/src/host_defined.rs @@ -1,6 +1,6 @@ -use std::any::TypeId; +use std::any::{Any, TypeId}; -use boa_macros::{Finalize, Trace}; +use boa_gc::{Finalize, Trace, custom_trace}; use hashbrown::hash_map::HashMap; use crate::object::NativeObject; @@ -8,48 +8,119 @@ use crate::object::NativeObject; /// This represents a `ECMAScript` specification \[`HostDefined`\] field. /// /// This allows storing types which are mapped by their [`TypeId`]. -#[derive(Default, Trace, Finalize)] #[allow(missing_debug_implementations)] -pub struct HostDefined { +pub struct HostDefined { // INVARIANT: All key-value pairs `(id, obj)` satisfy: // `id == TypeId::of::() && obj.is::()` // for some type `T : NativeObject`. - types: HashMap>, + types: HashMap>, } -// TODO: Track https://github.com/rust-lang/rust/issues/65991 and -// https://github.com/rust-lang/rust/issues/90850 to remove this -// when those are stabilized. -fn downcast_boxed_native_object_unchecked(obj: Box) -> Box { - let raw: *mut dyn NativeObject = Box::into_raw(obj); +impl Default for HostDefined { + fn default() -> Self { + Self { + types: HashMap::default(), + } + } +} - // SAFETY: We know that `obj` is of type `T` (due to the INVARIANT of `HostDefined`). - // See `HostDefined::insert`, `HostDefined::insert_default` and `HostDefined::remove`. - unsafe { Box::from_raw(raw.cast::()) } +// SAFETY: All traceable values are marked here, making this implementation +// safe. +unsafe impl Trace for HostDefined { + custom_trace!(this, mark, { + for value in this.types.values() { + mark(value); + } + }); } -impl HostDefined { +impl Finalize for HostDefined {} + +impl HostDefined { /// Insert a type into the [`HostDefined`]. #[track_caller] - pub fn insert_default(&mut self) -> Option> { + pub fn insert_default(&mut self) -> Option> { self.types .insert(TypeId::of::(), Box::::default()) - .map(downcast_boxed_native_object_unchecked) + .and_then(|t| t.downcast().ok()) } /// Insert a type into the [`HostDefined`]. #[track_caller] - pub fn insert(&mut self, value: T) -> Option> { + pub fn insert(&mut self, value: T) -> Option> { self.types .insert(TypeId::of::(), Box::new(value)) - .map(downcast_boxed_native_object_unchecked) + .and_then(|t| t.downcast().ok()) } - /// Check if the [`HostDefined`] has type T. - #[must_use] + /// Remove type T from [`HostDefined`], if it exists. + /// + /// Returns [`Some`] with the object if it exits, [`None`] otherwise. #[track_caller] - pub fn has(&self) -> bool { - self.types.contains_key(&TypeId::of::()) + pub fn remove(&mut self) -> Option> { + self.types + .remove(&TypeId::of::()) + .and_then(|t| t.downcast().ok()) + } + + /// Get type T from [`HostDefined`], if it exists. + #[track_caller] + pub fn get(&self) -> Option<&T> { + self.types + .get(&TypeId::of::()) + .map(Box::as_ref) + .and_then(::downcast_ref::) + } + + /// Get type T from [`HostDefined`], if it exists. + #[track_caller] + pub fn get_mut(&mut self) -> Option<&mut T> { + self.types + .get_mut(&TypeId::of::()) + .map(Box::as_mut) + .and_then(::downcast_mut::) + } + + /// Get a tuple of types from [`HostDefined`], returning `None` for the types that are not on the map. + #[track_caller] + pub fn get_many_mut(&mut self) -> T::NativeTupleMutRef<'_> + where + T: NativeTuple, + { + let ids = T::as_type_ids(); + let refs: [&TypeId; SIZE] = std::array::from_fn(|i| &ids[i]); + let anys = self + .types + .get_disjoint_mut(refs) + .map(|o| o.map(|v| &mut **v)); + + T::mut_ref_from_anys(anys) + } +} + +impl HostDefined { + /// Insert a type into the [`HostDefined`]. + #[track_caller] + pub fn insert_default(&mut self) -> Option> { + self.types + .insert(TypeId::of::(), Box::::default()) + .and_then(|t| { + // triggers downcast from NativeObject to Any + let t: Box = t; + t.downcast().ok() + }) + } + + /// Insert a type into the [`HostDefined`]. + #[track_caller] + pub fn insert(&mut self, value: T) -> Option> { + self.types + .insert(TypeId::of::(), Box::new(value)) + .and_then(|t| { + // triggers downcast from NativeObject to Any + let t: Box = t; + t.downcast().ok() + }) } /// Remove type T from [`HostDefined`], if it exists. @@ -57,9 +128,11 @@ impl HostDefined { /// Returns [`Some`] with the object if it exits, [`None`] otherwise. #[track_caller] pub fn remove(&mut self) -> Option> { - self.types - .remove(&TypeId::of::()) - .map(downcast_boxed_native_object_unchecked) + self.types.remove(&TypeId::of::()).and_then(|t| { + // triggers downcast from NativeObject to Any + let t: Box = t; + t.downcast().ok() + }) } /// Get type T from [`HostDefined`], if it exists. @@ -88,8 +161,23 @@ impl HostDefined { { let ids = T::as_type_ids(); let refs: [&TypeId; SIZE] = std::array::from_fn(|i| &ids[i]); + let anys = self.types.get_disjoint_mut(refs).map(|o| { + o.map(|v| { + let v: &mut dyn Any = &mut **v; + v + }) + }); + + T::mut_ref_from_anys(anys) + } +} - T::mut_ref_from_anys(self.types.get_disjoint_mut(refs)) +impl HostDefined { + /// Check if the [`HostDefined`] has type T. + #[must_use] + #[track_caller] + pub fn has(&self) -> bool { + self.types.contains_key(&TypeId::of::()) } /// Clears all the objects. @@ -108,14 +196,12 @@ pub trait NativeTuple { fn as_type_ids() -> [TypeId; SIZE]; - fn mut_ref_from_anys( - anys: [Option<&'_ mut Box>; SIZE], - ) -> Self::NativeTupleMutRef<'_>; + fn mut_ref_from_anys(anys: [Option<&'_ mut dyn Any>; SIZE]) -> Self::NativeTupleMutRef<'_>; } macro_rules! impl_native_tuple { ($size:literal $(,$name:ident)* ) => { - impl<$($name: NativeObject,)*> NativeTuple<$size> for ($($name,)*) { + impl<$($name: Any,)*> NativeTuple<$size> for ($($name,)*) { type NativeTupleMutRef<'a> = ($(Option<&'a mut $name>,)*); fn as_type_ids() -> [TypeId; $size] { @@ -124,7 +210,7 @@ macro_rules! impl_native_tuple { #[allow(unused_variables, unused_mut, clippy::unused_unit)] fn mut_ref_from_anys( - anys: [Option<&'_ mut Box>; $size], + anys: [Option<&'_ mut dyn Any>; $size], ) -> Self::NativeTupleMutRef<'_> { let mut anys = anys.into_iter(); ($( diff --git a/core/engine/src/job.rs b/core/engine/src/job.rs index 812ff8144d0..58f514772bc 100644 --- a/core/engine/src/job.rs +++ b/core/engine/src/job.rs @@ -55,7 +55,7 @@ use std::{cell::RefCell, collections::VecDeque, fmt::Debug, future::Future, pin: /// This is basically a synchronous task that needs to be run to progress [`Promise`] objects, /// or unblock threads waiting on [`Atomics.waitAsync`]. /// -/// [Job]: https://tc39.es/ecma262/#sec-jobs +/// [Job Abstract Closure]: https://tc39.es/ecma262/#sec-jobs /// [`Promise`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise /// [`Atomics.waitAsync`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync pub struct NativeJob { @@ -127,19 +127,93 @@ impl NativeJob { } } -type Callback = Box; +/// An ECMAScript [Job Abstract Closure] that can be called multiple times. +/// +/// This is basically a synchronous task that needs to be run to progress [`Promise`] objects, +/// or unblock threads waiting on [`Atomics.waitAsync`]. +/// +/// [Job Abstract Closure]: https://tc39.es/ecma262/#sec-jobs +/// [`Promise`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +/// [`Atomics.waitAsync`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync +pub struct NativeJobFn { + #[allow(clippy::type_complexity)] + f: Box JsResult>, + realm: Option, +} + +impl Debug for NativeJobFn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NativeJobFn").finish_non_exhaustive() + } +} + +impl NativeJobFn { + /// Creates a new `NativeJobFn` from a closure. + pub fn new(f: F) -> Self + where + F: Fn(&mut Context) -> JsResult + 'static, + { + Self { + f: Box::new(f), + realm: None, + } + } + + /// Creates a new `NativeJob` from a closure and an execution realm. + pub fn with_realm(f: F, realm: Realm) -> Self + where + F: Fn(&mut Context) -> JsResult + 'static, + { + Self { + f: Box::new(f), + realm: Some(realm), + } + } -/// Token to cancel a [`TimeoutJob`] + /// Gets a reference to the execution realm of the job. + #[must_use] + pub const fn realm(&self) -> Option<&Realm> { + self.realm.as_ref() + } + + /// Calls the native job with the specified [`Context`]. + /// + /// # Note + /// + /// If the native job has an execution realm defined, this sets the running execution + /// context to the realm's before calling the inner closure, and resets it after execution. + pub fn call(&self, context: &mut Context) -> JsResult { + // If realm is not null, each time job is invoked the implementation must perform + // implementation-defined steps such that execution is prepared to evaluate ECMAScript + // code at the time of job's invocation. + if let Some(realm) = self.realm.clone() { + let old_realm = context.enter_realm(realm); + + // Let scriptOrModule be GetActiveScriptOrModule() at the time HostEnqueuePromiseJob is + // invoked. If realm is not null, each time job is invoked the implementation must + // perform implementation-defined steps such that scriptOrModule is the active script or + // module at the time of job's invocation. + let result = (self.f)(context); + + context.enter_realm(old_realm); + + result + } else { + (self.f)(context) + } + } +} + +type Callback = Box; + +/// Token to cancel a [`TimeoutJob`] and [`IntervalJob`]. #[derive(Clone)] -pub(crate) struct CancellationToken(Rc>>); +pub struct CancellationToken(Rc>>); impl Debug for CancellationToken { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let flag = self.0.take(); - let is_set = flag.is_none(); - self.0.set(flag); - f.debug_struct("OnceFlag") - .field("is_set", &is_set) + f.debug_struct("CancellationToken") + .field("cancelled", &self.revoked()) .finish_non_exhaustive() } } @@ -147,34 +221,43 @@ impl Debug for CancellationToken { impl CancellationToken { /// Creates a new cancellation token. pub(crate) fn new() -> Self { - Self(Rc::new(Cell::new(Some(Box::new(|| {}))))) + Self(Rc::new(Cell::new(vec![Box::new(|_| {})]))) } /// Sets a callback to run when the cancellation token gets used. /// /// On debug builds, this will panic if the cancellation token was already /// used. - pub(crate) fn set_callback(&self, f: impl FnOnce() + 'static) { + pub fn push_callback(&self, f: impl FnOnce(&mut Context) + 'static) { + let mut vec = self.0.take(); debug_assert!( - self.0.take().is_some(), + !vec.is_empty(), "setting a callback on an already used cancellation token" ); - self.0.set(Some(Box::new(f))); + vec.push(Box::new(f)); + self.0.set(vec); } - /// Cancels the [`TimeoutJob`] associated with this cancellation token. - pub(crate) fn cancel(&self) { - if let Some(fun) = self.0.take() { - fun(); + /// Cancels the [`TimeoutJob`] or [`IntervalJob`] associated with this cancellation token. + pub fn cancel(&self, context: &mut Context) { + for job in self.0.take() { + job(context); } } - /// Returns `true` if this cancellation token was used. - pub(crate) fn cancelled(&self) -> bool { - let flag = self.0.take(); - let is_set = flag.is_none(); - self.0.set(flag); - is_set + /// Revokes this cancellation token, making it unusable to cancel its associated job. + pub(crate) fn revoke(&self) { + self.0.take(); + } + + /// Returns `true` if this cancellation token has been revoked, either because + /// `cancel` was called or because its associated job has completed. + #[must_use] + pub fn revoked(&self) -> bool { + let callbacks = self.0.take(); + let cancelled = callbacks.is_empty(); + self.0.set(callbacks); + cancelled } } @@ -188,14 +271,16 @@ pub struct TimeoutJob { /// The distance in milliseconds in the future when the job should run. /// This will be added to the current time when the job is enqueued. timeout: JsDuration, - /// The job to run after the time has passed. - job: NativeJob, + /// The job to run after the specified timeout. + job: Option, /// Signals if the timeout job was cancelled. - cancelled: CancellationToken, - /// Signals that this job is recurring. A recurring job shouldn't be - /// awaited for when considering whether a run of the event loop is - /// done. - recurring: bool, + cancellation_token: CancellationToken, +} + +impl Drop for TimeoutJob { + fn drop(&mut self) { + self.cancellation_token.revoke(); + } } impl TimeoutJob { @@ -204,20 +289,8 @@ impl TimeoutJob { pub fn new(job: NativeJob, timeout_in_millis: u64) -> Self { Self { timeout: JsDuration::from_millis(timeout_in_millis), - job, - cancelled: CancellationToken::new(), - recurring: false, - } - } - - /// Create a new `TimeoutJob` that is marked as recurring. - #[must_use] - pub fn recurring(job: NativeJob, timeout_in_millis: u64) -> Self { - Self { - timeout: JsDuration::from_millis(timeout_in_millis), - job, - cancelled: CancellationToken::new(), - recurring: true, + job: Some(job), + cancellation_token: CancellationToken::new(), } } @@ -245,8 +318,13 @@ impl TimeoutJob { /// /// If the native job has an execution realm defined, this sets the running execution /// context to the realm's before calling the inner closure, and resets it after execution. - pub fn call(self, context: &mut Context) -> JsResult { - self.job.call(context) + pub fn call(mut self, context: &mut Context) -> JsResult { + let result = self + .job + .take() + .map_or_else(|| Ok(JsValue::undefined()), |job| job.call(context)); + self.cancellation_token.revoke(); + result } /// Returns the timeout value in milliseconds since epoch. @@ -260,26 +338,98 @@ impl TimeoutJob { #[inline] #[must_use] pub fn cancelled(&self) -> bool { - self.cancelled.cancelled() + self.cancellation_token.revoked() + } + + /// Returns the [`CancellationToken`] for this timeout job. + #[must_use] + pub fn cancellation_token(&self) -> &CancellationToken { + &self.cancellation_token + } +} + +/// An ECMAScript [Job] that runs at a certain interval of time. +/// +/// This represents jobs enqueued by APIs such as [`setInterval`]. +/// +/// [`setInterval`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval +#[derive(Debug)] +pub struct IntervalJob { + /// The distance in milliseconds in the future when the job should run. + /// This will be added to the current time when the job is enqueued. + interval: JsDuration, + /// The job to run after every interval of time. + job: NativeJobFn, + /// Signals if the timeout job was cancelled. + cancellation_token: CancellationToken, +} + +impl Drop for IntervalJob { + fn drop(&mut self) { + self.cancellation_token.revoke(); } +} - /// Sets a cancellation callback for the timeout job. +impl IntervalJob { + /// Create a new `IntervalJob` with an interval and a job. + #[must_use] + pub fn new(job: NativeJobFn, interval_in_millis: u64) -> Self { + Self { + interval: JsDuration::from_millis(interval_in_millis), + job, + cancellation_token: CancellationToken::new(), + } + } + + /// Creates a new `IntervalJob` from a closure and an interval as [`std::time::Duration`]. + #[must_use] + pub fn from_duration(f: F, interval: impl Into) -> Self + where + F: Fn(&mut Context) -> JsResult + 'static, + { + Self::new(NativeJobFn::new(f), interval.into().as_millis()) + } + + /// Creates a new `TimeoutJob` from a closure, an interval, and an execution realm. + #[must_use] + pub fn with_realm(f: F, realm: Realm, interval: time::Duration) -> Self + where + F: Fn(&mut Context) -> JsResult + 'static, + { + Self::new( + NativeJobFn::with_realm(f, realm), + interval.as_millis() as u64, + ) + } + + /// Calls the interval job with the specified [`Context`]. + /// + /// # Note /// - /// This callback will get called if the timeout job gets cancelled. + /// If the interval job has an execution realm defined, this sets the running execution + /// context to the realm's before calling the inner closure, and resets it after execution. + pub fn call(&self, context: &mut Context) -> JsResult { + self.job.call(context) + } + + /// Returns the interval value in milliseconds. #[inline] - pub fn set_cancellation_callback(&self, f: impl FnOnce() + 'static) { - self.cancelled.set_callback(f); + #[must_use] + pub fn interval(&self) -> JsDuration { + self.interval } - /// Returns the [`CancellationToken`] for this timeout job. - pub(crate) fn cancellation_token(&self) -> CancellationToken { - self.cancelled.clone() + /// Returns `true` if the interval job was cancelled, and its execution can be skipped. + #[inline] + #[must_use] + pub fn cancelled(&self) -> bool { + self.cancellation_token.revoked() } - /// Returns `true` if the job is recurring (meaning it happens regularly). + /// Returns the [`CancellationToken`] for this interval job. #[must_use] - pub fn is_recurring(&self) -> bool { - self.recurring + pub fn cancellation_token(&self) -> &CancellationToken { + &self.cancellation_token } } @@ -569,6 +719,10 @@ pub enum Job { /// /// See [`TimeoutJob`] for more information. TimeoutJob(TimeoutJob), + /// A generic job that is to be executed after intervals of a number of milliseconds. + /// + /// See [`TimeoutJob`] for more information. + IntervalJob(IntervalJob), /// A generic job. /// /// See [`GenericJob`] for more information. @@ -621,6 +775,12 @@ impl From for Job { } } +impl From for Job { + fn from(job: IntervalJob) -> Self { + Job::IntervalJob(job) + } +} + impl From for Job { fn from(job: GenericJob) -> Self { Job::GenericJob(job) @@ -685,6 +845,21 @@ impl JobExecutor for IdleJobExecutor { } } +#[derive(Debug)] +enum ClockJob { + Timeout(TimeoutJob), + Interval(IntervalJob), +} + +impl ClockJob { + fn cancelled(&self) -> bool { + match self { + ClockJob::Timeout(t) => t.cancelled(), + ClockJob::Interval(i) => i.cancelled(), + } + } +} + /// A simple FIFO executor that bails on the first error. /// /// This is the default job executor for the [`Context`], but it is mostly pretty limited @@ -696,7 +871,7 @@ pub struct SimpleJobExecutor { promise_jobs: RefCell>, async_jobs: RefCell>, finalization_registry_jobs: RefCell>, - timeout_jobs: RefCell>>, + clock_jobs: RefCell>>, generic_jobs: RefCell>, stop: Arc, } @@ -705,7 +880,7 @@ impl SimpleJobExecutor { fn clear(&self) { self.promise_jobs.borrow_mut().clear(); self.async_jobs.borrow_mut().clear(); - self.timeout_jobs.borrow_mut().clear(); + self.clock_jobs.borrow_mut().clear(); self.generic_jobs.borrow_mut().clear(); } } @@ -735,7 +910,7 @@ impl SimpleJobExecutor { self.promise_jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() && self.generic_jobs.borrow().is_empty() - && self.timeout_jobs.borrow().is_empty() + && self.clock_jobs.borrow().is_empty() } } @@ -746,11 +921,19 @@ impl JobExecutor for SimpleJobExecutor { Job::AsyncJob(a) => self.async_jobs.borrow_mut().push_back(a), Job::TimeoutJob(t) => { let now = context.clock().now(); - self.timeout_jobs + self.clock_jobs .borrow_mut() .entry(now + t.timeout()) .or_default() - .push(t); + .push(ClockJob::Timeout(t)); + } + Job::IntervalJob(i) => { + let now = context.clock().now(); + self.clock_jobs + .borrow_mut() + .entry(now + i.interval()) + .or_default() + .push(ClockJob::Interval(i)); } Job::GenericJob(g) => self.generic_jobs.borrow_mut().push_back(g), Job::FinalizationRegistryCleanupJob(fr) => { @@ -788,7 +971,7 @@ impl JobExecutor for SimpleJobExecutor { { let now = context.borrow().clock().now(); let jobs_to_run = { - let mut timeout_jobs = self.timeout_jobs.borrow_mut(); + let mut timeout_jobs = self.clock_jobs.borrow_mut(); let mut jobs_to_keep = timeout_jobs.split_off(&now); jobs_to_keep.retain(|_, jobs| { jobs.retain(|job| !job.cancelled()); @@ -799,11 +982,28 @@ impl JobExecutor for SimpleJobExecutor { for jobs in jobs_to_run.into_values() { for job in jobs { - if !job.cancelled() - && let Err(err) = job.call(&mut context.borrow_mut()) - { - self.clear(); - return Err(err); + if !job.cancelled() { + match job { + ClockJob::Timeout(job) => { + if let Err(err) = job.call(&mut context.borrow_mut()) { + self.clear(); + return Err(err); + } + } + ClockJob::Interval(job) => { + let context = &mut context.borrow_mut(); + let now = context.clock().now(); + if let Err(err) = job.call(context) { + self.clear(); + return Err(err); + } + self.clock_jobs + .borrow_mut() + .entry(now + job.interval()) + .or_default() + .push(ClockJob::Interval(job)); + } + } } } } diff --git a/core/runtime/src/interval.rs b/core/runtime/src/interval.rs index cc89611d5c3..d408ce2ea49 100644 --- a/core/runtime/src/interval.rs +++ b/core/runtime/src/interval.rs @@ -2,96 +2,53 @@ //! timeouts. use boa_engine::interop::JsRest; +use boa_engine::job::{CancellationToken, IntervalJob, NativeJobFn}; use boa_engine::job::{NativeJob, TimeoutJob}; use boa_engine::object::builtins::JsFunction; use boa_engine::value::{IntegerOrInfinity, Nullable}; -use boa_engine::{ - Context, Finalize, IntoJsFunctionCopied, JsData, JsResult, JsValue, Trace, js_error, js_string, -}; -use boa_gc::{Gc, GcRefCell}; -use std::collections::HashSet; +use boa_engine::{Context, IntoJsFunctionCopied, JsResult, JsValue, js_error, js_string}; +use std::collections::HashMap; #[cfg(test)] mod tests; /// The internal state of the interval module. The value is whether the interval /// function is still active. -#[derive(Default, Trace, Finalize, JsData)] +#[derive(Default)] struct IntervalInnerState { - active_map: HashSet, - next_id: u32, + active_map: HashMap, + id: u32, } impl IntervalInnerState { /// Get the interval handler map from the context, or add it to the context if not /// present. - fn from_context(context: &mut Context) -> Gc> { - if !context.has_data::>>() { - context.insert_data(Gc::new(GcRefCell::new(Self::default()))); + fn from_context(context: &mut Context) -> &mut Self { + if !context.has_data::() { + context.insert_data(Self::default()); } context - .get_data::>>() + .host_defined_mut() + .get_mut::() .expect("Should have inserted.") - .clone() - } - - /// Get whether an interval is still active. - #[inline] - fn is_interval_valid(&self, id: u32) -> bool { - self.active_map.contains(&id) } - /// Create an interval ID, insert it in the active map and return it. - fn new_interval(&mut self) -> JsResult { - if self.next_id == u32::MAX { - return Err(js_error!(Error: "Interval ID overflow")); - } - self.next_id += 1; - self.active_map.insert(self.next_id); - Ok(self.next_id) + /// Create an interval ID. + fn next_id(&mut self) -> JsResult { + self.active_map.retain(|_, v| !v.revoked()); + let id = self.id; + self.id = id + .checked_add(1) + .ok_or_else(|| js_error!(Error: "Interval ID overflow"))?; + Ok(id) } /// Delete an interval ID from the active map. - fn clear_interval(&mut self, id: u32) { - self.active_map.remove(&id); - } -} - -/// Inner handler function for handling intervals and timeout. -#[allow(clippy::too_many_arguments)] -fn handle( - handler_map: Gc>, - id: u32, - function_ref: JsFunction, - args: Vec, - reschedule: Option, - context: &mut Context, -) -> JsResult { - // Check if it's still valid. - if !handler_map.borrow().is_interval_valid(id) { - return Ok(JsValue::undefined()); - } - - // Call the handler function. - // The spec says we should still reschedule an interval even if the function - // throws an error. - let result = function_ref.call(&JsValue::undefined(), &args, context); - if let Some(delay) = reschedule { - if handler_map.borrow().is_interval_valid(id) { - let job = TimeoutJob::recurring( - NativeJob::new(move |context| { - handle(handler_map, id, function_ref, args, reschedule, context) - }), - delay, - ); - context.enqueue_job(job.into()); - } - return result; + fn clear_interval(&mut self, id: u32) -> Option { + self.active_map.retain(|_, v| !v.revoked()); + self.active_map.remove(&id) } - - handler_map.borrow_mut().clear_interval(id); - result } /// Set a timeout to call the given function after the given delay. @@ -112,9 +69,6 @@ pub fn set_timeout( return Ok(0); }; - let handler_map = IntervalInnerState::from_context(context); - let id = handler_map.borrow_mut().new_interval()?; - // Spec says if delay is not a number, it should be equal to 0. let delay = delay_in_msec .unwrap_or_default() @@ -123,13 +77,30 @@ pub fn set_timeout( // The spec converts the delay to a 32-bit signed integer. let delay = u64::from(delay.clamp_finite(0, u32::MAX)); + let state = IntervalInnerState::from_context(context); + let id = state.next_id()?; + // Get ownership of rest arguments. let rest = rest.to_vec(); let job = TimeoutJob::new( - NativeJob::new(move |context| handle(handler_map, id, function_ref, rest, None, context)), + NativeJob::new(move |context| { + let result = function_ref.call(&JsValue::undefined(), &rest, context); + let state = IntervalInnerState::from_context(context); + state.active_map.remove(&id); + result + }), delay, ); + let token = job.cancellation_token().clone(); + + token.push_callback(move |context| { + let state = IntervalInnerState::from_context(context); + state.active_map.remove(&id); + }); + + state.active_map.insert(id, token); + context.enqueue_job(job.into()); Ok(id) @@ -153,9 +124,6 @@ pub fn set_interval( return Ok(0); }; - let handler_map = IntervalInnerState::from_context(context); - let id = handler_map.borrow_mut().new_interval()?; - // Spec says if delay is not a number, it should be equal to 0. let delay = delay_in_msec .unwrap_or_default() @@ -163,15 +131,25 @@ pub fn set_interval( .unwrap_or(IntegerOrInfinity::Integer(0)); let delay = u64::from(delay.clamp_finite(0, u32::MAX)); + let state = IntervalInnerState::from_context(context); + let id = state.next_id()?; + // Get ownership of rest arguments. let rest = rest.to_vec(); - let job = TimeoutJob::new( - NativeJob::new(move |context| { - handle(handler_map, id, function_ref, rest, Some(delay), context) - }), + let job = IntervalJob::new( + NativeJobFn::new(move |context| function_ref.call(&JsValue::undefined(), &rest, context)), delay, ); + let token = job.cancellation_token().clone(); + + token.push_callback(move |context| { + let state = IntervalInnerState::from_context(context); + state.active_map.remove(&id); + }); + + state.active_map.insert(id, token); + context.enqueue_job(job.into()); Ok(id) @@ -188,7 +166,9 @@ pub fn clear_timeout(id: Nullable>, context: &mut Context) { return; }; let handler_map = IntervalInnerState::from_context(context); - handler_map.borrow_mut().clear_interval(id); + if let Some(token) = handler_map.clear_interval(id) { + token.cancel(context); + } } /// Register the interval module into the given context.