From f2abc2a67ce05e9cc641a0b2d9a39f083bb8746d Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Sat, 11 Jan 2025 00:59:06 -0600 Subject: [PATCH] Revamp `JobQueue` into `JobExecutor` --- cli/src/main.rs | 28 +- core/engine/src/builtins/promise/mod.rs | 22 +- core/engine/src/context/mod.rs | 65 ++-- core/engine/src/job.rs | 297 +++++++++++-------- core/engine/src/object/builtins/jspromise.rs | 8 +- core/engine/src/script.rs | 4 +- examples/src/bin/module_fetch_async.rs | 135 ++++----- examples/src/bin/smol_event_loop.rs | 61 ++-- examples/src/bin/tokio_event_loop.rs | 61 ++-- 9 files changed, 373 insertions(+), 308 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index f62c47d75c9..2ff42930ef1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -13,7 +13,7 @@ mod helper; use boa_engine::{ builtins::promise::PromiseState, context::ContextBuilder, - job::{JobQueue, NativeAsyncJob, NativeJob}, + job::{Job, JobExecutor, NativeAsyncJob, PromiseJob}, module::{Module, SimpleModuleLoader}, optimizer::OptimizerOptions, script::Script, @@ -336,10 +336,10 @@ fn main() -> Result<()> { let args = Opt::parse(); - let queue = Rc::new(Jobs::default()); + let executor = Rc::new(Executor::default()); let loader = Rc::new(SimpleModuleLoader::new(&args.root).map_err(|e| eyre!(e.to_string()))?); let mut context = ContextBuilder::new() - .job_queue(queue) + .job_executor(executor) .module_loader(loader.clone()) .build() .map_err(|e| eyre!(e.to_string()))?; @@ -453,23 +453,23 @@ fn add_runtime(context: &mut Context) { } #[derive(Default)] -struct Jobs { - jobs: RefCell>, +struct Executor { + promise_jobs: RefCell>, async_jobs: RefCell>, } -impl JobQueue for Jobs { - fn enqueue_job(&self, job: NativeJob, _: &mut Context) { - self.jobs.borrow_mut().push_back(job); - } - - fn enqueue_async_job(&self, async_job: NativeAsyncJob, _: &mut Context) { - self.async_jobs.borrow_mut().push_back(async_job); +impl JobExecutor for Executor { + fn enqueue_job(&self, job: Job, _: &mut Context) { + match job { + Job::PromiseJob(job) => self.promise_jobs.borrow_mut().push_back(job), + Job::AsyncJob(job) => self.async_jobs.borrow_mut().push_back(job), + job => eprintln!("unsupported job type {job:?}"), + } } fn run_jobs(&self, context: &mut Context) { loop { - if self.jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() { + if self.promise_jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() { return; } let async_jobs = std::mem::take(&mut *self.async_jobs.borrow_mut()); @@ -477,7 +477,7 @@ impl JobQueue for Jobs { if let Err(err) = pollster::block_on(async_job.call(&RefCell::new(context))) { eprintln!("Uncaught {err}"); } - let jobs = std::mem::take(&mut *self.jobs.borrow_mut()); + let jobs = std::mem::take(&mut *self.promise_jobs.borrow_mut()); for job in jobs { if let Err(e) = job.call(context) { eprintln!("Uncaught {e}"); diff --git a/core/engine/src/builtins/promise/mod.rs b/core/engine/src/builtins/promise/mod.rs index cda49ce2309..d5795f294c7 100644 --- a/core/engine/src/builtins/promise/mod.rs +++ b/core/engine/src/builtins/promise/mod.rs @@ -11,7 +11,7 @@ use crate::{ builtins::{Array, BuiltInObject}, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, error::JsNativeError, - job::{JobCallback, NativeJob}, + job::{JobCallback, PromiseJob}, js_string, native_function::NativeFunction, object::{ @@ -1887,7 +1887,9 @@ impl Promise { new_promise_reaction_job(fulfill_reaction, value.clone(), context); // c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]). - context.job_queue().enqueue_job(fulfill_job, context); + context + .job_executor() + .enqueue_job(fulfill_job.into(), context); } // 11. Else, @@ -1907,7 +1909,9 @@ impl Promise { let reject_job = new_promise_reaction_job(reject_reaction, reason.clone(), context); // e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]). - context.job_queue().enqueue_job(reject_job, context); + context + .job_executor() + .enqueue_job(reject_job.into(), context); // 12. Set promise.[[PromiseIsHandled]] to true. promise @@ -1983,7 +1987,7 @@ impl Promise { let job = new_promise_reaction_job(reaction, argument.clone(), context); // b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). - context.job_queue().enqueue_job(job, context); + context.job_executor().enqueue_job(job.into(), context); } // 2. Return unused. } @@ -2176,7 +2180,7 @@ impl Promise { ); // 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). - context.job_queue().enqueue_job(job, context); + context.job_executor().enqueue_job(job.into(), context); // 16. Return undefined. Ok(JsValue::undefined()) @@ -2237,7 +2241,7 @@ fn new_promise_reaction_job( mut reaction: ReactionRecord, argument: JsValue, context: &mut Context, -) -> NativeJob { +) -> PromiseJob { // Inverting order since `job` captures `reaction` by value. // 2. Let handlerRealm be null. @@ -2318,7 +2322,7 @@ fn new_promise_reaction_job( }; // 4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }. - NativeJob::with_realm(job, realm, context) + PromiseJob::with_realm(job, realm, context) } /// More information: @@ -2330,7 +2334,7 @@ fn new_promise_resolve_thenable_job( thenable: JsValue, then: JobCallback, context: &mut Context, -) -> NativeJob { +) -> PromiseJob { // Inverting order since `job` captures variables by value. // 2. Let getThenRealmResult be Completion(GetFunctionRealm(then.[[Callback]])). @@ -2372,5 +2376,5 @@ fn new_promise_resolve_thenable_job( }; // 6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }. - NativeJob::with_realm(job, realm, context) + PromiseJob::with_realm(job, realm, context) } diff --git a/core/engine/src/context/mod.rs b/core/engine/src/context/mod.rs index e6635edf8d7..3d5415e8b48 100644 --- a/core/engine/src/context/mod.rs +++ b/core/engine/src/context/mod.rs @@ -14,12 +14,12 @@ use intrinsics::Intrinsics; #[cfg(feature = "temporal")] use temporal_rs::tzdb::FsTzdbProvider; -use crate::job::NativeAsyncJob; +use crate::job::Job; use crate::vm::RuntimeLimits; use crate::{ builtins, class::{Class, ClassBuilder}, - job::{JobQueue, NativeJob, SimpleJobQueue}, + job::{JobExecutor, SimpleJobExecutor}, js_string, module::{IdleModuleLoader, ModuleLoader, SimpleModuleLoader}, native_function::NativeFunction, @@ -113,7 +113,7 @@ pub struct Context { host_hooks: &'static dyn HostHooks, - job_queue: Rc, + job_executor: Rc, module_loader: Rc, @@ -135,7 +135,7 @@ impl std::fmt::Debug for Context { .field("interner", &self.interner) .field("vm", &self.vm) .field("strict", &self.strict) - .field("promise_job_queue", &"JobQueue") + .field("job_executor", &"JobExecutor") .field("hooks", &"HostHooks") .field("module_loader", &"ModuleLoader") .field("optimizer_options", &self.optimizer_options); @@ -188,7 +188,7 @@ impl Context { /// ``` /// /// Note that this won't run any scheduled promise jobs; you need to call [`Context::run_jobs`] - /// on the context or [`JobQueue::run_jobs`] on the provided queue to run them. + /// on the context or [`JobExecutor::run_jobs`] on the provided queue to run them. #[allow(clippy::unit_arg, dropping_copy_types)] pub fn eval(&mut self, src: Source<'_, R>) -> JsResult { let main_timer = Profiler::global().start_event("Script evaluation", "Main"); @@ -469,35 +469,31 @@ impl Context { self.strict = strict; } - /// Enqueues a [`NativeJob`] on the [`JobQueue`]. + /// Enqueues a [`Job`] on the [`JobExecutor`]. #[inline] - pub fn enqueue_job(&mut self, job: NativeJob) { - self.job_queue().enqueue_job(job, self); + pub fn enqueue_job(&mut self, job: Job) { + self.job_executor().enqueue_job(job, self); } - /// Enqueues a [`NativeAsyncJob`] on the [`JobQueue`]. - #[inline] - pub fn enqueue_async_job(&mut self, job: NativeAsyncJob) { - self.job_queue().enqueue_async_job(job, self); - } - - /// Runs all the jobs in the job queue. + /// Runs all the jobs with the provided job executor. #[inline] pub fn run_jobs(&mut self) { - self.job_queue().run_jobs(self); + self.job_executor().run_jobs(self); self.clear_kept_objects(); } - /// Asynchronously runs all the jobs in the job queue. + /// Asynchronously runs all the jobs with the provided job executor. /// /// # Note /// /// Concurrent job execution cannot be guaranteed by the engine, since this depends on the - /// specific handling of each [`JobQueue`]. If you want to execute jobs concurrently, you must - /// provide a custom implementor of `JobQueue` to the context. + /// specific handling of each [`JobExecutor`]. If you want to execute jobs concurrently, you must + /// provide a custom implementatin of `JobExecutor` to the context. #[allow(clippy::future_not_send)] pub async fn run_jobs_async(&mut self) { - self.job_queue().run_jobs_async(&RefCell::new(self)).await; + self.job_executor() + .run_jobs_async(&RefCell::new(self)) + .await; self.clear_kept_objects(); } @@ -554,11 +550,11 @@ impl Context { self.host_hooks } - /// Gets the job queue. + /// Gets the job executor. #[inline] #[must_use] - pub fn job_queue(&self) -> Rc { - self.job_queue.clone() + pub fn job_executor(&self) -> Rc { + self.job_executor.clone() } /// Gets the module loader. @@ -889,7 +885,7 @@ impl Context { pub struct ContextBuilder { interner: Option, host_hooks: Option<&'static dyn HostHooks>, - job_queue: Option>, + job_executor: Option>, module_loader: Option>, can_block: bool, #[cfg(feature = "intl")] @@ -901,7 +897,7 @@ pub struct ContextBuilder { impl std::fmt::Debug for ContextBuilder { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { #[derive(Clone, Copy, Debug)] - struct JobQueue; + struct JobExecutor; #[derive(Clone, Copy, Debug)] struct HostHooks; #[derive(Clone, Copy, Debug)] @@ -911,7 +907,10 @@ impl std::fmt::Debug for ContextBuilder { out.field("interner", &self.interner) .field("host_hooks", &self.host_hooks.as_ref().map(|_| HostHooks)) - .field("job_queue", &self.job_queue.as_ref().map(|_| JobQueue)) + .field( + "job_executor", + &self.job_executor.as_ref().map(|_| JobExecutor), + ) .field( "module_loader", &self.module_loader.as_ref().map(|_| ModuleLoader), @@ -1024,10 +1023,10 @@ impl ContextBuilder { self } - /// Initializes the [`JobQueue`] for the context. + /// Initializes the [`JobExecutor`] for the context. #[must_use] - pub fn job_queue(mut self, job_queue: Rc) -> Self { - self.job_queue = Some(job_queue); + pub fn job_executor(mut self, job_executor: Rc) -> Self { + self.job_executor = Some(job_executor); self } @@ -1098,9 +1097,9 @@ impl ContextBuilder { Rc::new(IdleModuleLoader) }; - let job_queue = self - .job_queue - .unwrap_or_else(|| Rc::new(SimpleJobQueue::new())); + let job_executor = self + .job_executor + .unwrap_or_else(|| Rc::new(SimpleJobExecutor::new())); let mut context = Context { interner: self.interner.unwrap_or_default(), @@ -1127,7 +1126,7 @@ impl ContextBuilder { instructions_remaining: self.instructions_remaining, kept_alive: Vec::new(), host_hooks, - job_queue, + job_executor, module_loader, optimizer_options: OptimizerOptions::OPTIMIZE_ALL, root_shape, diff --git a/core/engine/src/job.rs b/core/engine/src/job.rs index 574c66445c7..30581a001cf 100644 --- a/core/engine/src/job.rs +++ b/core/engine/src/job.rs @@ -1,21 +1,33 @@ //! Boa's API to create and customize `ECMAScript` jobs and job queues. //! -//! [`NativeJob`] is an ECMAScript [Job], or a closure that runs an `ECMAScript` computation when -//! there's no other computation running. +//! [`Job`] is an ECMAScript [Job], or a closure that runs an `ECMAScript` computation when +//! there's no other computation running. The module defines several type of jobs: +//! - [`PromiseJob`] for Promise related jobs. +//! - [`NativeAsyncJob`] for jobs that support [`Future`]. +//! - [`NativeJob`] for generic jobs that aren't related to Promises. //! //! [`JobCallback`] is an ECMAScript [`JobCallback`] record, containing an `ECMAScript` function //! that is executed when a promise is either fulfilled or rejected. //! -//! [`JobQueue`] is a trait encompassing the required functionality for a job queue; this allows +//! [`JobExecutor`] is a trait encompassing the required functionality for a job executor; this allows //! implementing custom event loops, custom handling of Jobs or other fun things. //! This trait is also accompanied by two implementors of the trait: -//! - [`IdleJobQueue`], which is a queue that does nothing, and the default queue if no queue is +//! - [`IdleJobExecutor`], which is an executor that does nothing, and the default executor if no executor is //! provided. Useful for hosts that want to disable promises. -//! - [`SimpleJobQueue`], which is a simple FIFO queue that runs all jobs to completion, bailing +//! - [`SimpleJobExecutor`], which is a simple FIFO queue that runs all jobs to completion, bailing //! on the first error encountered. //! +//! ## [`Trace`]? +//! +//! Most of the types defined in this module don't implement `Trace`. This is because most jobs can only +//! be run once, and putting a `JobExecutor` on a garbage collected object is not allowed. +//! +//! In addition to that, not implementing `Trace` makes it so that the garbage collector can consider +//! any captured variables inside jobs as roots, since you cannot store jobs within a [`Gc`]. +//! //! [Job]: https://tc39.es/ecma262/#sec-jobs //! [JobCallback]: https://tc39.es/ecma262/#sec-jobcallback-records +//! [`Gc`]: boa_gc::Gc use std::{cell::RefCell, collections::VecDeque, fmt::Debug, future::Future, pin::Pin}; @@ -26,41 +38,14 @@ use crate::{ }; use boa_gc::{Finalize, Trace}; -/// An ECMAScript [Job] closure. -/// -/// The specification allows scheduling any [`NativeJob`] closure by the host into the job queue. -/// However, host-defined jobs must abide to a set of requirements. -/// -/// ### Requirements -/// -/// - At some future point in time, when there is no running execution context and the execution -/// context stack is empty, the implementation must: -/// - Perform any host-defined preparation steps. -/// - Invoke the Job Abstract Closure. -/// - Perform any host-defined cleanup steps, after which the execution context stack must be empty. -/// - Only one Job may be actively undergoing evaluation at any point in time. -/// - Once evaluation of a Job starts, it must run to completion before evaluation of any other Job starts. -/// - The Abstract Closure must return a normal completion, implementing its own handling of errors. -/// -/// `NativeJob`s API differs slightly on the last requirement, since it allows closures returning -/// [`JsResult`], but it's okay because `NativeJob`s are handled by the host anyways; a host could -/// pass a closure returning `Err` and handle the error on [`JobQueue::run_jobs`], making the closure -/// effectively run as if it never returned `Err`. -/// -/// ## [`Trace`]? -/// -/// `NativeJob` doesn't implement `Trace` because it doesn't need to; all jobs can only be run once -/// and putting a [`JobQueue`] on a garbage collected object is not allowed. +/// An ECMAScript [Job Abstract Closure]. /// -/// On the other hand, it seems like this type breaks all the safety checks of the -/// [`NativeFunction`] API, since you can capture any `Trace` variable into the closure... but it -/// doesn't! -/// The garbage collector doesn't need to trace the captured variables because the closures -/// are always stored on the [`JobQueue`], which is always rooted, which means the captured variables -/// are also rooted, allowing us to capture any variable in the closure for free! +/// 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 -/// [`NativeFunction`]: crate::native_function::NativeFunction +/// [`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 { #[allow(clippy::type_complexity)] f: Box JsResult>, @@ -69,7 +54,7 @@ pub struct NativeJob { impl Debug for NativeJob { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NativeJob").field("f", &"Closure").finish() + f.debug_struct("NativeJob").finish_non_exhaustive() } } @@ -135,17 +120,8 @@ pub type BoxedFuture<'a> = Pin> + 'a>> /// An ECMAScript [Job] that can be run asynchronously. /// -/// ## [`Trace`]? -/// -/// `NativeJob` doesn't implement `Trace` because it doesn't need to; all jobs can only be run once -/// and putting a [`JobQueue`] on a garbage collected object is not allowed. -/// -/// Additionally, the garbage collector doesn't need to trace the captured variables because the closures -/// are always stored on the [`JobQueue`], which is always rooted, which means the captured variables -/// are also rooted. -/// -/// [Job]: https://tc39.es/ecma262/#sec-jobs -/// [`NativeFunction`]: crate::native_function::NativeFunction +/// This is an additional type of job that is not defined by the specification, enabling running `Future` tasks +/// created by ECMAScript code in an easier way. #[allow(clippy::type_complexity)] pub struct NativeAsyncJob { f: Box FnOnce(&'a RefCell<&mut Context>) -> BoxedFuture<'a>>, @@ -238,6 +214,69 @@ impl NativeAsyncJob { } } +/// An ECMAScript [Job Abstract Closure] executing code related to [`Promise`] objects. +/// +/// This represents the [`HostEnqueuePromiseJob`] operation from the specification. +/// +/// ### [Requirements] +/// +/// - 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. +/// - 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. +/// - Jobs must run in the same order as the `HostEnqueuePromiseJob` invocations that scheduled them. +/// +/// Of all the requirements, Boa guarantees the first two by its internal implementation of `NativeJob`, meaning +/// implementations of [`JobExecutor`] must only guarantee that jobs are run in the same order as they're enqueued. +/// +/// [`Promise`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +/// [`HostEnqueuePromiseJob`]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob +/// [Job Abstract Closure]: https://tc39.es/ecma262/#sec-jobs +/// [Requirements]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-hostenqueuepromisejob +/// [`GetActiveScriptOrModule()`]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-getactivescriptormodule +pub struct PromiseJob(NativeJob); + +impl Debug for PromiseJob { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PromiseJob").finish_non_exhaustive() + } +} + +impl PromiseJob { + /// Creates a new `PromiseJob` from a closure. + pub fn new(f: F) -> Self + where + F: FnOnce(&mut Context) -> JsResult + 'static, + { + Self(NativeJob::new(f)) + } + + /// Creates a new `PromiseJob` from a closure and an execution realm. + pub fn with_realm(f: F, realm: Realm, context: &mut Context) -> Self + where + F: FnOnce(&mut Context) -> JsResult + 'static, + { + Self(NativeJob::with_realm(f, realm, context)) + } + + /// Gets a reference to the execution realm of the `PromiseJob`. + #[must_use] + pub const fn realm(&self) -> Option<&Realm> { + self.0.realm() + } + + /// Calls the `PromiseJob` with the specified [`Context`]. + /// + /// # Note + /// + /// If the 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.0.call(context) + } +} + /// [`JobCallback`][spec] records. /// /// [spec]: https://tc39.es/ecma262/#sec-jobcallback-records @@ -287,54 +326,71 @@ impl JobCallback { } } -/// A queue of `ECMAscript` [Jobs]. +/// A job that needs to be handled by a [`JobExecutor`]. /// -/// This is the main API that allows creating custom event loops with custom job queues. +/// # Requirements /// -/// [Jobs]: https://tc39.es/ecma262/#sec-jobs -pub trait JobQueue { - /// [`HostEnqueuePromiseJob ( job, realm )`][spec]. - /// - /// Enqueues a [`NativeJob`] on the job queue. - /// - /// # Requirements - /// - /// Per the [spec]: - /// > An implementation of `HostEnqueuePromiseJob` must conform to the requirements in [9.5][Jobs] as well as the - /// > following: - /// > - 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. - /// > - 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. - /// > - Jobs must run in the same order as the `HostEnqueuePromiseJob` invocations that scheduled them. +/// The specification defines many types of jobs, but all of them must adhere to a set of requirements: +/// +/// - At some future point in time, when there is no running execution context and the execution +/// context stack is empty, the implementation must: +/// - Perform any host-defined preparation steps. +/// - Invoke the Job Abstract Closure. +/// - Perform any host-defined cleanup steps, after which the execution context stack must be empty. +/// - Only one Job may be actively undergoing evaluation at any point in time. +/// - Once evaluation of a Job starts, it must run to completion before evaluation of any other Job starts. +/// - The Abstract Closure must return a normal completion, implementing its own handling of errors. +/// +/// Boa is a little bit flexible on the last requirement, since it allows jobs to return either +/// values or errors, but the rest of the requirements must be followed for all conformant implementations. +/// +/// Additionally, each job type can have additional requirements that must also be followed in addition +/// to the previous ones. +#[non_exhaustive] +#[derive(Debug)] +pub enum Job { + /// A `Promise`-related job. /// - /// Of all the requirements, Boa guarantees the first two by its internal implementation of `NativeJob`, meaning - /// the implementer must only guarantee that jobs are run in the same order as they're enqueued. + /// See [`PromiseJob`] for more information. + PromiseJob(PromiseJob), + /// A [`Future`]-related job. /// - /// [spec]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob - /// [Jobs]: https://tc39.es/ecma262/#sec-jobs - fn enqueue_job(&self, job: NativeJob, context: &mut Context); + /// See [`NativeAsyncJob`] for more information. + AsyncJob(NativeAsyncJob), +} - /// Enqueues a new [`NativeAsyncJob`] job on the job queue. - /// - /// Calling `future` returns a Rust [`Future`] that can be sent to a runtime for concurrent computation. - fn enqueue_async_job(&self, async_job: NativeAsyncJob, context: &mut Context); +impl From for Job { + fn from(native_async_job: NativeAsyncJob) -> Self { + Job::AsyncJob(native_async_job) + } +} + +impl From for Job { + fn from(promise_job: PromiseJob) -> Self { + Job::PromiseJob(promise_job) + } +} - /// Runs all jobs in the queue. +/// An executor of `ECMAscript` [Jobs]. +/// +/// This is the main API that allows creating custom event loops. +/// +/// [Jobs]: https://tc39.es/ecma262/#sec-jobs +pub trait JobExecutor { + /// Enqueues a `Job` on the executor. /// - /// Running a job could enqueue more jobs in the queue. The implementor of the trait - /// determines if the method should loop until there are no more queued jobs or if - /// it should only run one iteration of the queue. + /// This method combines all the host-defined job enqueueing operations into a single method. + /// See the [spec] for more information on the requirements that each operation must follow. + /// + /// [spec]: https://tc39.es/ecma262/#sec-jobs + fn enqueue_job(&self, job: Job, context: &mut Context); + + /// Runs all jobs in the executor. fn run_jobs(&self, context: &mut Context); - /// Asynchronously runs all jobs in the queue. + /// Asynchronously runs all jobs in the executor. /// - /// Running a job could enqueue more jobs in the queue. The implementor of the trait - /// determines if the method should loop until there are no more queued jobs or if - /// it should only run one iteration of the queue. - /// - /// By default forwards to [`JobQueue::run_jobs`]. Implementors using async should override this + /// By default forwards to [`JobExecutor::run_jobs`]. Implementors using async should override this /// with a proper algorithm to run jobs asynchronously. fn run_jobs_async<'a, 'b, 'fut>( &'a self, @@ -348,93 +404,90 @@ pub trait JobQueue { } } -/// A job queue that does nothing. +/// A job executor that does nothing. /// -/// This queue is mostly useful if you want to disable the promise capabilities of the engine. This +/// This executor is mostly useful if you want to disable the promise capabilities of the engine. This /// can be done by passing it to the [`ContextBuilder`]: /// /// ``` /// use boa_engine::{ /// context::ContextBuilder, -/// job::{IdleJobQueue, JobQueue}, +/// job::{IdleJobExecutor, JobExecutor}, /// }; /// use std::rc::Rc; /// -/// let queue = Rc::new(IdleJobQueue); -/// let context = ContextBuilder::new().job_queue(queue).build(); +/// let executor = Rc::new(IdleJobExecutor); +/// let context = ContextBuilder::new().job_executor(executor).build(); /// ``` /// /// [`ContextBuilder`]: crate::context::ContextBuilder #[derive(Debug, Clone, Copy)] -pub struct IdleJobQueue; - -impl JobQueue for IdleJobQueue { - fn enqueue_job(&self, _: NativeJob, _: &mut Context) {} +pub struct IdleJobExecutor; - fn enqueue_async_job(&self, _: NativeAsyncJob, _: &mut Context) {} +impl JobExecutor for IdleJobExecutor { + fn enqueue_job(&self, _: Job, _: &mut Context) {} fn run_jobs(&self, _: &mut Context) {} } -/// A simple FIFO job queue that bails on the first error. +/// A simple FIFO executor that bails on the first error. /// -/// This is the default job queue for the [`Context`], but it is mostly pretty limited for -/// custom event queues. +/// This is the default job executor for the [`Context`], but it is mostly pretty limited for +/// custom event loop. /// -/// To disable running promise jobs on the engine, see [`IdleJobQueue`]. +/// To disable running promise jobs on the engine, see [`IdleJobExecutor`]. #[derive(Default)] -pub struct SimpleJobQueue { - job_queue: RefCell>, - async_job_queue: RefCell>, +pub struct SimpleJobExecutor { + jobs: RefCell>, + async_jobs: RefCell>, } -impl Debug for SimpleJobQueue { +impl Debug for SimpleJobExecutor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("SimpleQueue").field(&"..").finish() + f.debug_struct("SimpleJobExecutor").finish_non_exhaustive() } } -impl SimpleJobQueue { - /// Creates an empty `SimpleJobQueue`. +impl SimpleJobExecutor { + /// Creates a new `SimpleJobExecutor`. #[must_use] pub fn new() -> Self { Self::default() } } -impl JobQueue for SimpleJobQueue { - fn enqueue_job(&self, job: NativeJob, _: &mut Context) { - self.job_queue.borrow_mut().push_back(job); - } - - fn enqueue_async_job(&self, job: NativeAsyncJob, _: &mut Context) { - self.async_job_queue.borrow_mut().push_back(job); +impl JobExecutor for SimpleJobExecutor { + fn enqueue_job(&self, job: Job, _: &mut Context) { + match job { + Job::PromiseJob(p) => self.jobs.borrow_mut().push_back(p), + Job::AsyncJob(a) => self.async_jobs.borrow_mut().push_back(a), + } } fn run_jobs(&self, context: &mut Context) { let context = RefCell::new(context); loop { - let mut next_job = self.async_job_queue.borrow_mut().pop_front(); + let mut next_job = self.async_jobs.borrow_mut().pop_front(); while let Some(job) = next_job { if pollster::block_on(job.call(&context)).is_err() { - self.async_job_queue.borrow_mut().clear(); + self.async_jobs.borrow_mut().clear(); return; }; - next_job = self.async_job_queue.borrow_mut().pop_front(); + next_job = self.async_jobs.borrow_mut().pop_front(); } // Yeah, I have no idea why Rust extends the lifetime of a `RefCell` that should be immediately // dropped after calling `pop_front`. - let mut next_job = self.job_queue.borrow_mut().pop_front(); + let mut next_job = self.jobs.borrow_mut().pop_front(); while let Some(job) = next_job { if job.call(&mut context.borrow_mut()).is_err() { - self.job_queue.borrow_mut().clear(); + self.jobs.borrow_mut().clear(); return; }; - next_job = self.job_queue.borrow_mut().pop_front(); + next_job = self.jobs.borrow_mut().pop_front(); } - if self.async_job_queue.borrow().is_empty() && self.job_queue.borrow().is_empty() { + if self.async_jobs.borrow().is_empty() && self.jobs.borrow().is_empty() { return; } } diff --git a/core/engine/src/object/builtins/jspromise.rs b/core/engine/src/object/builtins/jspromise.rs index e6d24154f40..695c26cd9e3 100644 --- a/core/engine/src/object/builtins/jspromise.rs +++ b/core/engine/src/object/builtins/jspromise.rs @@ -292,7 +292,7 @@ impl JsPromise { { let (promise, resolvers) = Self::new_pending(context); - context.job_queue().enqueue_async_job( + context.enqueue_job( NativeAsyncJob::new(move |context| { Box::pin(async move { let result = future.await; @@ -306,8 +306,8 @@ impl JsPromise { } } }) - }), - context, + }) + .into(), ); promise @@ -1085,7 +1085,7 @@ impl JsPromise { /// Run jobs until this promise is resolved or rejected. This could /// result in an infinite loop if the promise is never resolved or - /// rejected (e.g. with a [`boa_engine::job::JobQueue`] that does + /// rejected (e.g. with a [`boa_engine::job::JobExecutor`] that does /// not prioritize properly). If you need more control over how /// the promise handles timing out, consider using /// [`Context::run_jobs`] directly. diff --git a/core/engine/src/script.rs b/core/engine/src/script.rs index 52c59110e22..4adf0118d91 100644 --- a/core/engine/src/script.rs +++ b/core/engine/src/script.rs @@ -160,9 +160,9 @@ impl Script { /// Evaluates this script and returns its result. /// /// Note that this won't run any scheduled promise jobs; you need to call [`Context::run_jobs`] - /// on the context or [`JobQueue::run_jobs`] on the provided queue to run them. + /// on the context or [`JobExecutor::run_jobs`] on the provided queue to run them. /// - /// [`JobQueue::run_jobs`]: crate::job::JobQueue::run_jobs + /// [`JobExecutor::run_jobs`]: crate::job::JobExecutor::run_jobs pub fn evaluate(&self, context: &mut Context) -> JsResult { let _timer = Profiler::global().start_event("Execution", "Main"); diff --git a/examples/src/bin/module_fetch_async.rs b/examples/src/bin/module_fetch_async.rs index 5925e0900bb..458d70f6d13 100644 --- a/examples/src/bin/module_fetch_async.rs +++ b/examples/src/bin/module_fetch_async.rs @@ -2,7 +2,7 @@ use std::{cell::RefCell, collections::VecDeque, future::Future, pin::Pin, rc::Rc use boa_engine::{ builtins::promise::PromiseState, - job::{JobQueue, NativeAsyncJob, NativeJob}, + job::{Job, JobExecutor, NativeAsyncJob, PromiseJob}, js_string, module::ModuleLoader, Context, JsNativeError, JsResult, JsString, JsValue, Module, @@ -29,58 +29,61 @@ impl ModuleLoader for HttpModuleLoader { let url = specifier.to_std_string_escaped(); // Just enqueue the future for now. We'll advance all the enqueued futures inside our custom - // `JobQueue`. - context.enqueue_async_job(NativeAsyncJob::with_realm( - move |context| { - Box::pin(async move { - // Adding some prints to show the non-deterministic nature of the async fetches. - // Try to run the example several times to see how sometimes the fetches start in order - // but finish in disorder. - println!("Fetching `{url}`..."); - - // This could also retry fetching in case there's an error while requesting the module. - let body: Result<_, isahc::Error> = async { - let mut response = Request::get(&url) - .redirect_policy(RedirectPolicy::Limit(5)) - .body(())? - .send_async() - .await?; - - Ok(response.text().await?) - } - .await; - - println!("Finished fetching `{url}`"); - - let body = match body { - Ok(body) => body, - Err(err) => { - // On error we always call `finish_load` to notify the load promise about the - // error. - finish_load( - Err(JsNativeError::typ().with_message(err.to_string()).into()), - &mut context.borrow_mut(), - ); - - // Just returns anything to comply with `NativeAsyncJob::new`'s signature. - return Ok(JsValue::undefined()); + // `JobExecutor`. + context.enqueue_job( + NativeAsyncJob::with_realm( + move |context| { + Box::pin(async move { + // Adding some prints to show the non-deterministic nature of the async fetches. + // Try to run the example several times to see how sometimes the fetches start in order + // but finish in disorder. + println!("Fetching `{url}`..."); + + // This could also retry fetching in case there's an error while requesting the module. + let body: Result<_, isahc::Error> = async { + let mut response = Request::get(&url) + .redirect_policy(RedirectPolicy::Limit(5)) + .body(())? + .send_async() + .await?; + + Ok(response.text().await?) } - }; - - // Could also add a path if needed. - let source = Source::from_bytes(body.as_bytes()); - - let module = Module::parse(source, None, &mut context.borrow_mut()); - - // We don't do any error handling, `finish_load` takes care of that for us. - finish_load(module, &mut context.borrow_mut()); - - // Also needed to match `NativeAsyncJob::new`. - Ok(JsValue::undefined()) - }) - }, - context.realm().clone(), - )); + .await; + + println!("Finished fetching `{url}`"); + + let body = match body { + Ok(body) => body, + Err(err) => { + // On error we always call `finish_load` to notify the load promise about the + // error. + finish_load( + Err(JsNativeError::typ().with_message(err.to_string()).into()), + &mut context.borrow_mut(), + ); + + // Just returns anything to comply with `NativeAsyncJob::new`'s signature. + return Ok(JsValue::undefined()); + } + }; + + // Could also add a path if needed. + let source = Source::from_bytes(body.as_bytes()); + + let module = Module::parse(source, None, &mut context.borrow_mut()); + + // We don't do any error handling, `finish_load` takes care of that for us. + finish_load(module, &mut context.borrow_mut()); + + // Also needed to match `NativeAsyncJob::new`. + Ok(JsValue::undefined()) + }) + }, + context.realm().clone(), + ) + .into(), + ); } } @@ -108,7 +111,7 @@ fn main() -> JsResult<()> { "#; let context = &mut Context::builder() - .job_queue(Rc::new(Queue::new())) + .job_executor(Rc::new(Queue::new())) // NEW: sets the context module loader to our custom loader .module_loader(Rc::new(HttpModuleLoader)) .build()?; @@ -169,20 +172,20 @@ fn main() -> JsResult<()> { // Taken from the `smol_event_loop.rs` example. /// An event queue using smol to drive futures to completion. struct Queue { - async_jobs: RefCell>, - jobs: RefCell>, + async_jobs: RefCell>, + promise_jobs: RefCell>, } impl Queue { fn new() -> Self { Self { async_jobs: RefCell::default(), - jobs: RefCell::default(), + promise_jobs: RefCell::default(), } } fn drain_jobs(&self, context: &mut Context) { - let jobs = std::mem::take(&mut *self.jobs.borrow_mut()); + let jobs = std::mem::take(&mut *self.promise_jobs.borrow_mut()); for job in jobs { if let Err(e) = job.call(context) { eprintln!("Uncaught {e}"); @@ -191,13 +194,13 @@ impl Queue { } } -impl JobQueue for Queue { - fn enqueue_job(&self, job: NativeJob, _context: &mut Context) { - self.jobs.borrow_mut().push_back(job); - } - - fn enqueue_async_job(&self, async_job: NativeAsyncJob, _context: &mut Context) { - self.async_jobs.borrow_mut().push(async_job); +impl JobExecutor for Queue { + fn enqueue_job(&self, job: Job, _context: &mut Context) { + match job { + Job::PromiseJob(job) => self.promise_jobs.borrow_mut().push_back(job), + Job::AsyncJob(job) => self.async_jobs.borrow_mut().push_back(job), + _ => panic!("unsupported job type"), + } } // While the sync flavor of `run_jobs` will block the current thread until all the jobs have finished... @@ -216,7 +219,7 @@ impl JobQueue for Queue { { Box::pin(async move { // Early return in case there were no jobs scheduled. - if self.jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() { + if self.promise_jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() { return; } let mut group = FutureGroup::new(); @@ -225,7 +228,7 @@ impl JobQueue for Queue { group.insert(job.call(context)); } - if self.jobs.borrow().is_empty() { + if self.promise_jobs.borrow().is_empty() { let Some(result) = group.next().await else { // Both queues are empty. We can exit. return; diff --git a/examples/src/bin/smol_event_loop.rs b/examples/src/bin/smol_event_loop.rs index c673b5f96c0..5d4998654cb 100644 --- a/examples/src/bin/smol_event_loop.rs +++ b/examples/src/bin/smol_event_loop.rs @@ -9,7 +9,7 @@ use std::{ use boa_engine::{ context::ContextBuilder, - job::{JobQueue, NativeAsyncJob, NativeJob}, + job::{Job, JobExecutor, NativeAsyncJob, PromiseJob}, js_string, native_function::NativeFunction, property::Attribute, @@ -35,19 +35,19 @@ fn main() { /// An event queue using smol to drive futures to completion. struct Queue { async_jobs: RefCell>, - jobs: RefCell>, + promise_jobs: RefCell>, } impl Queue { fn new() -> Self { Self { async_jobs: RefCell::default(), - jobs: RefCell::default(), + promise_jobs: RefCell::default(), } } fn drain_jobs(&self, context: &mut Context) { - let jobs = std::mem::take(&mut *self.jobs.borrow_mut()); + let jobs = std::mem::take(&mut *self.promise_jobs.borrow_mut()); for job in jobs { if let Err(e) = job.call(context) { eprintln!("Uncaught {e}"); @@ -56,13 +56,13 @@ impl Queue { } } -impl JobQueue for Queue { - fn enqueue_job(&self, job: NativeJob, _context: &mut Context) { - self.jobs.borrow_mut().push_back(job); - } - - fn enqueue_async_job(&self, async_job: NativeAsyncJob, _context: &mut Context) { - self.async_jobs.borrow_mut().push_back(async_job); +impl JobExecutor for Queue { + fn enqueue_job(&self, job: Job, _context: &mut Context) { + match job { + Job::PromiseJob(job) => self.promise_jobs.borrow_mut().push_back(job), + Job::AsyncJob(job) => self.async_jobs.borrow_mut().push_back(job), + _ => panic!("unsupported job type"), + } } // While the sync flavor of `run_jobs` will block the current thread until all the jobs have finished... @@ -81,7 +81,7 @@ impl JobQueue for Queue { { Box::pin(async move { // Early return in case there were no jobs scheduled. - if self.jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() { + if self.promise_jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() { return; } let mut group = FutureGroup::new(); @@ -90,7 +90,7 @@ impl JobQueue for Queue { group.insert(job.call(context)); } - if self.jobs.borrow().is_empty() { + if self.promise_jobs.borrow().is_empty() { let Some(result) = group.next().await else { // Both queues are empty. We can exit. return; @@ -153,21 +153,24 @@ fn interval(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult let delay = args.get_or_undefined(1).to_u32(context)?; let args = args.get(2..).unwrap_or_default().to_vec(); - context.enqueue_async_job(NativeAsyncJob::with_realm( - move |context| { - Box::pin(async move { - let mut timer = smol::Timer::interval(Duration::from_millis(u64::from(delay))); - for _ in 0..10 { - timer.next().await; - if let Err(err) = function.call(&this, &args, &mut context.borrow_mut()) { - eprintln!("Uncaught {err}"); + context.enqueue_job( + NativeAsyncJob::with_realm( + move |context| { + Box::pin(async move { + let mut timer = smol::Timer::interval(Duration::from_millis(u64::from(delay))); + for _ in 0..10 { + timer.next().await; + if let Err(err) = function.call(&this, &args, &mut context.borrow_mut()) { + eprintln!("Uncaught {err}"); + } } - } - Ok(JsValue::undefined()) - }) - }, - context.realm().clone(), - )); + Ok(JsValue::undefined()) + }) + }, + context.realm().clone(), + ) + .into(), + ); Ok(JsValue::undefined()) } @@ -233,7 +236,7 @@ fn internally_async_event_loop() { // Initialize the queue and the context let queue = Queue::new(); let context = &mut ContextBuilder::new() - .job_queue(Rc::new(queue)) + .job_executor(Rc::new(queue)) .build() .unwrap(); @@ -262,7 +265,7 @@ fn externally_async_event_loop() { // Initialize the queue and the context let queue = Queue::new(); let context = &mut ContextBuilder::new() - .job_queue(Rc::new(queue)) + .job_executor(Rc::new(queue)) .build() .unwrap(); diff --git a/examples/src/bin/tokio_event_loop.rs b/examples/src/bin/tokio_event_loop.rs index cc8b33cede3..07b187ce48d 100644 --- a/examples/src/bin/tokio_event_loop.rs +++ b/examples/src/bin/tokio_event_loop.rs @@ -9,7 +9,7 @@ use std::{ use boa_engine::{ context::ContextBuilder, - job::{JobQueue, NativeAsyncJob, NativeJob}, + job::{Job, JobExecutor, NativeAsyncJob, PromiseJob}, js_string, native_function::NativeFunction, property::Attribute, @@ -35,19 +35,19 @@ fn main() { /// An event queue using tokio to drive futures to completion. struct Queue { async_jobs: RefCell>, - jobs: RefCell>, + promise_jobs: RefCell>, } impl Queue { fn new() -> Self { Self { async_jobs: RefCell::default(), - jobs: RefCell::default(), + promise_jobs: RefCell::default(), } } fn drain_jobs(&self, context: &mut Context) { - let jobs = std::mem::take(&mut *self.jobs.borrow_mut()); + let jobs = std::mem::take(&mut *self.promise_jobs.borrow_mut()); for job in jobs { if let Err(e) = job.call(context) { eprintln!("Uncaught {e}"); @@ -56,13 +56,13 @@ impl Queue { } } -impl JobQueue for Queue { - fn enqueue_job(&self, job: NativeJob, _context: &mut Context) { - self.jobs.borrow_mut().push_back(job); - } - - fn enqueue_async_job(&self, async_job: NativeAsyncJob, _context: &mut Context) { - self.async_jobs.borrow_mut().push_back(async_job); +impl JobExecutor for Queue { + fn enqueue_job(&self, job: Job, _context: &mut Context) { + match job { + Job::PromiseJob(job) => self.promise_jobs.borrow_mut().push_back(job), + Job::AsyncJob(job) => self.async_jobs.borrow_mut().push_back(job), + _ => panic!("unsupported job type"), + } } // While the sync flavor of `run_jobs` will block the current thread until all the jobs have finished... @@ -86,7 +86,7 @@ impl JobQueue for Queue { { Box::pin(async move { // Early return in case there were no jobs scheduled. - if self.jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() { + if self.promise_jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() { return; } let mut group = FutureGroup::new(); @@ -95,7 +95,7 @@ impl JobQueue for Queue { group.insert(job.call(context)); } - if self.jobs.borrow().is_empty() { + if self.promise_jobs.borrow().is_empty() { let Some(result) = group.next().await else { // Both queues are empty. We can exit. return; @@ -161,21 +161,24 @@ fn interval(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult let delay = args.get_or_undefined(1).to_u32(context)?; let args = args.get(2..).unwrap_or_default().to_vec(); - context.enqueue_async_job(NativeAsyncJob::with_realm( - move |context| { - Box::pin(async move { - let mut timer = time::interval(Duration::from_millis(u64::from(delay))); - for _ in 0..10 { - timer.tick().await; - if let Err(err) = function.call(&this, &args, &mut context.borrow_mut()) { - eprintln!("Uncaught {err}"); + context.enqueue_job( + NativeAsyncJob::with_realm( + move |context| { + Box::pin(async move { + let mut timer = time::interval(Duration::from_millis(u64::from(delay))); + for _ in 0..10 { + timer.tick().await; + if let Err(err) = function.call(&this, &args, &mut context.borrow_mut()) { + eprintln!("Uncaught {err}"); + } } - } - Ok(JsValue::undefined()) - }) - }, - context.realm().clone(), - )); + Ok(JsValue::undefined()) + }) + }, + context.realm().clone(), + ) + .into(), + ); Ok(JsValue::undefined()) } @@ -241,7 +244,7 @@ fn internally_async_event_loop() { // Initialize the queue and the context let queue = Queue::new(); let context = &mut ContextBuilder::new() - .job_queue(Rc::new(queue)) + .job_executor(Rc::new(queue)) .build() .unwrap(); @@ -268,7 +271,7 @@ async fn externally_async_event_loop() { // Initialize the queue and the context let queue = Queue::new(); let context = &mut ContextBuilder::new() - .job_queue(Rc::new(queue)) + .job_executor(Rc::new(queue)) .build() .unwrap();