Skip to main content

script/runtime/
job_queue.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Implementation of [microtasks](https://html.spec.whatwg.org/multipage/#microtask) and
6//! microtask queues. It is up to implementations of event loops to store a queue and
7//! perform checkpoints at appropriate times, as well as enqueue microtasks as required.
8
9use std::ffi::c_void;
10use std::ptr::NonNull;
11use std::rc::Rc;
12
13use js::context::JSContext;
14use js::glue::{CreateJobQueue, DeleteJobQueue, JobQueueTraps, RustJobQueue};
15use js::jsapi::{
16    GetExecutionGlobalFromJSMicroTask, GetPromiseUserInputEventHandlingState, IsJSMicroTask,
17    JSContext as RawJSContext, JSTracer, MaybeGetPromiseFromJSMicroTask, MutableHandleObject,
18    PromiseUserInputEventHandlingState, ToMaybeWrappedJSMicroTask,
19};
20use js::jsval::{JSVal, PrivateValue};
21use js::panic::wrap_panic;
22use js::realm::AutoRealm;
23use js::rust::wrappers2::{
24    EnqueueMicroTask, GetJobQueue, HasAnyMicroTasks, JS_DequeueNextMicroTask, JobQueueIsEmpty,
25    JobQueueMayNotBeEmpty, MaybeGetHostDefinedDataFromJSMicroTask, RunJSMicroTask, SetJobQueue,
26};
27use malloc_size_of::MallocSizeOf;
28use script_bindings::reflector::DomObject as _;
29use script_bindings::root::Dom;
30use script_bindings::settings_stack::{run_a_callback, run_a_script};
31
32use crate::dom::bindings::callback::ExceptionHandling;
33use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
34use crate::dom::bindings::root::DomRoot;
35use crate::dom::globalscope::GlobalScope;
36use crate::event_loop::script_thread::ScriptThread;
37use crate::realms::enter_auto_realm;
38use crate::runtime::script_runtime::notify_about_rejected_promises;
39use crate::{DomTypeHolder, JSTraceable};
40
41pub(crate) static JOB_QUEUE_TRAPS: JobQueueTraps = JobQueueTraps {
42    getHostDefinedData: Some(get_host_defined_data),
43    getHostDefinedGlobal: Some(get_host_defined_global),
44    runJobs: Some(run_jobs),
45    traceNonGCThingMicroTask: Some(trace_non_gc_things_micro_task),
46};
47
48pub(crate) struct JobQueue(*mut RustJobQueue);
49
50#[expect(unsafe_code)]
51unsafe impl JSTraceable for JobQueue {
52    unsafe fn trace(&self, _trc: *mut JSTracer) {
53        // RustJobQueue does not contain any GC things.
54    }
55}
56
57impl MallocSizeOf for JobQueue {
58    fn size_of(&self, _ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
59        // TODO: measure size of all enqueued microtasks
60        0
61    }
62}
63
64impl JobQueue {
65    #[expect(unsafe_code)]
66    pub(crate) fn new() -> Self {
67        JobQueue(unsafe { CreateJobQueue(&JOB_QUEUE_TRAPS) })
68    }
69
70    #[expect(unsafe_code)]
71    pub(crate) fn set_on_context(&self, cx: &JSContext) {
72        unsafe { SetJobQueue(cx, self.0 as *mut _) };
73    }
74}
75
76impl Drop for JobQueue {
77    #[expect(unsafe_code)]
78    fn drop(&mut self) {
79        unsafe {
80            DeleteJobQueue(self.0);
81        }
82    }
83}
84
85/// <https://searchfox.org/firefox-main/rev/446c6e609dbd7c355c2fb27209dfe4833211991f/xpcom/base/CycleCollectedJSContext.cpp#229>
86#[expect(unsafe_code)]
87unsafe extern "C" fn get_host_defined_data(
88    cx: *mut RawJSContext,
89    incumbent_global: MutableHandleObject,
90    data: MutableHandleObject,
91) -> bool {
92    incumbent_global.set(std::ptr::null_mut());
93    data.set(std::ptr::null_mut());
94    if !unsafe { get_host_defined_global(cx, incumbent_global) } {
95        return false;
96    }
97
98    if incumbent_global.is_null() {
99        return true;
100    }
101
102    // we have no schedulingState
103
104    true
105}
106
107#[allow(unsafe_code)]
108/// <https://searchfox.org/firefox-main/rev/446c6e609dbd7c355c2fb27209dfe4833211991f/xpcom/base/CycleCollectedJSContext.cpp#199>
109unsafe extern "C" fn get_host_defined_global(
110    _cx: *mut RawJSContext,
111    out: MutableHandleObject,
112) -> bool {
113    wrap_panic(&mut || {
114        let Some(incumbent_global) = GlobalScope::incumbent() else {
115            return;
116        };
117
118        out.set(incumbent_global.reflector().get_jsobject().get());
119    });
120
121    true
122}
123
124#[expect(unsafe_code)]
125unsafe extern "C" fn run_jobs(cx: *mut RawJSContext) {
126    let mut cx = unsafe {
127        // SAFETY: We are in SM hook
128        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
129    };
130    wrap_panic(&mut || {
131        // TODO: run Promise- and User-variant Microtasks, and do #notify-about-rejected-promises.
132        // Those will require real `globalscopes` values.
133        job_queue_microtask_checkpoint(&mut cx, vec![]);
134    });
135}
136
137#[derive(JSTraceable, MallocSizeOf)]
138pub struct NotifyMutationObserversMicrotask;
139
140impl NotifyMutationObserversMicrotask {
141    pub(crate) fn new() -> Self {
142        Self
143    }
144}
145
146impl MicrotaskRunnable for NotifyMutationObserversMicrotask {
147    fn handler(&self, cx: &mut JSContext) {
148        ScriptThread::mutation_observers().notify_mutation_observers(cx);
149    }
150}
151
152#[derive(JSTraceable, MallocSizeOf)]
153pub struct CustomElementReactionMicrotask;
154
155impl CustomElementReactionMicrotask {
156    pub(crate) fn new() -> Self {
157        Self
158    }
159}
160
161impl MicrotaskRunnable for CustomElementReactionMicrotask {
162    fn handler(&self, cx: &mut JSContext) {
163        ScriptThread::invoke_backup_element_queue(cx);
164    }
165}
166
167pub(crate) trait MicrotaskRunnable: JSTraceable + MallocSizeOf {
168    // must also take care of entering the realm
169    fn handler(&self, _cx: &mut JSContext) {}
170}
171
172/// A microtask that comes from a queueMicrotask() Javascript call
173#[derive(JSTraceable, MallocSizeOf)]
174#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
175pub(crate) struct UserMicrotask {
176    #[conditional_malloc_size_of]
177    pub(crate) callback: Rc<VoidFunction>,
178    pub(crate) global: Dom<GlobalScope>,
179}
180
181impl MicrotaskRunnable for UserMicrotask {
182    fn handler(&self, cx: &mut JSContext) {
183        let mut realm = enter_auto_realm(cx, &*self.global);
184        let cx = &mut realm;
185        let _ = self
186            .callback
187            .Call_(cx, &*self.global, ExceptionHandling::Report);
188    }
189}
190
191fn microtask_from_jsval(val: JSVal) -> *mut Box<dyn MicrotaskRunnable> {
192    val.to_private() as *const Box<dyn MicrotaskRunnable> as *mut Box<dyn MicrotaskRunnable>
193}
194
195/// Add a new microtask to this queue. It will be invoked as part of the next
196/// microtask checkpoint.
197#[expect(unsafe_code)]
198pub(crate) fn enqueue(cx: &JSContext, task: Box<dyn MicrotaskRunnable>) {
199    let task = Box::new(task);
200    let raw = Box::into_raw(task);
201    unsafe { JobQueueMayNotBeEmpty(cx) };
202    assert!(unsafe { EnqueueMicroTask(cx, &PrivateValue(raw as *const c_void)) });
203}
204
205/// <https://html.spec.whatwg.org/multipage/#perform-a-microtask-checkpoint>
206/// Perform a microtask checkpoint, executing all queued microtasks until the queue is empty.
207#[expect(unsafe_code)]
208pub(crate) fn job_queue_microtask_checkpoint(
209    cx: &mut JSContext,
210    globalscopes: Vec<DomRoot<GlobalScope>>,
211) {
212    let job_queue: *mut RustJobQueue = unsafe { GetJobQueue(cx) } as _;
213    // Step 1. If the event loop's performing a microtask checkpoint is true, then return.
214    if unsafe { (*job_queue).draining } {
215        return;
216    }
217
218    // Step 2. Set the event loop's performing a microtask checkpoint to true.
219    unsafe {
220        (*job_queue).draining = true;
221    }
222
223    debug!("Now performing a microtask checkpoint");
224
225    rooted!(&in(cx) let mut generic_task: js::jsapi::GenericMicroTask);
226    rooted!(&in(cx) let mut js_micro_task: *mut js::jsapi::JSMicroTask);
227    rooted!(&in(cx) let mut execution_global: *mut js::jsapi::JSObject);
228    rooted!(&in(cx) let mut incumbent_global: *mut js::jsapi::JSObject);
229    rooted!(&in(cx) let mut data: *mut js::jsapi::JSObject);
230
231    // Step 3. While the event loop's microtask queue is not empty:
232    // based on https://spidermonkey.dev/blog/2026/01/15/job-responsibility.html#running-micro-tasks
233    // and https://searchfox.org/firefox-main/rev/7ae92e67d094086cd3e09918ec94b6278a948535/xpcom/base/CycleCollectedJSContext.cpp#1176
234    // and its helper functions
235    while unsafe { HasAnyMicroTasks(cx) } {
236        unsafe { JS_DequeueNextMicroTask(cx, generic_task.handle_mut()) };
237
238        // Notify the JS engine if the queue is now empty, enabling optimizations
239        // like skipping await microtask creation for resolved promises.
240        if !unsafe { HasAnyMicroTasks(cx) } {
241            unsafe { JobQueueIsEmpty(cx) };
242        }
243
244        // https://searchfox.org/firefox-main/rev/50691777d300fffc7d1f7844b59769109bc76f3e/xpcom/base/CycleCollectedJSContext.cpp#916
245        if !unsafe { IsJSMicroTask(generic_task.as_ptr()) } {
246            rooted!(&in(cx) let task = unsafe {
247                Box::from_raw(
248                    microtask_from_jsval(*generic_task),
249                )
250            });
251            task.handler(cx);
252            continue;
253        }
254
255        js_micro_task.set(unsafe { ToMaybeWrappedJSMicroTask(generic_task.as_ptr()) });
256        execution_global.set(unsafe { GetExecutionGlobalFromJSMicroTask(js_micro_task.get()) });
257        if execution_global.get().is_null() {
258            continue;
259        }
260        if !unsafe {
261            MaybeGetHostDefinedDataFromJSMicroTask(
262                js_micro_task.get(),
263                incumbent_global.handle_mut(),
264                data.handle_mut(),
265            )
266        } {
267            continue;
268        }
269
270        let interaction = if let Some(promise) =
271            NonNull::new(unsafe { MaybeGetPromiseFromJSMicroTask(js_micro_task.get()) })
272        {
273            unsafe { GetPromiseUserInputEventHandlingState(promise.as_ptr()) }
274        } else {
275            PromiseUserInputEventHandlingState::DontCare
276        };
277        let _maybe_user_interacting_guard =
278            if interaction == PromiseUserInputEventHandlingState::HadUserInteractionAtCreation {
279                Some(ScriptThread::user_interacting_guard())
280            } else {
281                None
282            };
283        let global_scope = unsafe { GlobalScope::from_object(execution_global.get()) };
284        run_a_script::<DomTypeHolder, _, _>(cx, &global_scope, |cx| {
285            let mut r = || {
286                let mut realm = AutoRealm::new_from_handle(cx, execution_global.handle());
287                let _ = unsafe { RunJSMicroTask(&mut realm, js_micro_task.handle()) };
288            };
289            if incumbent_global.get().is_null() {
290                r();
291            } else {
292                let global_scope = unsafe { GlobalScope::from_object(incumbent_global.get()) };
293                run_a_callback::<DomTypeHolder, _>(&global_scope, r);
294            }
295        });
296    }
297
298    // Step 4. For each environment settings object settingsObject whose responsible
299    // event loop is this event loop, notify about rejected promises given
300    // settingsObject's global object.
301    for global in globalscopes.clone().into_iter() {
302        notify_about_rejected_promises(cx, &global);
303    }
304
305    // https://html.spec.whatwg.org/multipage/#perform-a-microtask-checkpoint
306    // Step 5. Cleanup Indexed Database transactions.
307    // https://w3c.github.io/IndexedDB/#cleanup-indexed-database-transactions
308    // “These steps are invoked by [HTML]. They ensure that transactions created by a script call
309    // to transaction() are deactivated once the task that invoked the script has completed.”
310    for global in globalscopes.iter() {
311        if let Some(factory) = global.indexeddb_factory() {
312            let _ = factory.cleanup_indexeddb_transactions(cx);
313        }
314    }
315
316    // TODO: Step 6. Perform ClearKeptObjects().
317
318    // Step 7. Set the event loop's performing a microtask checkpoint to false.
319    unsafe {
320        (*job_queue).draining = false;
321    }
322    // TODO: Step 8. Record timing info for microtask checkpoint.
323}
324
325#[expect(unsafe_code)]
326pub(crate) fn job_queue_clear(cx: &JSContext) {
327    rooted!(&in(cx) let mut generic_task: js::jsapi::GenericMicroTask);
328    while unsafe { HasAnyMicroTasks(cx) } {
329        unsafe { JS_DequeueNextMicroTask(cx, generic_task.handle_mut()) };
330        if !unsafe { IsJSMicroTask(generic_task.as_ptr()) } {
331            let task = unsafe { Box::from_raw(microtask_from_jsval(*generic_task)) };
332            drop(task);
333        }
334    }
335}
336
337#[expect(unsafe_code)]
338unsafe extern "C" fn trace_non_gc_things_micro_task(trc: *mut JSTracer, val: *mut JSVal) {
339    wrap_panic(&mut || {
340        let task = microtask_from_jsval(unsafe { *val });
341        unsafe { (**task).trace(trc) };
342    })
343}