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;
11
12use js::context::JSContext;
13use js::glue::{CreateJobQueue, DeleteJobQueue, JobQueueTraps, RustJobQueue};
14use js::jsapi::{
15    GetExecutionGlobalFromJSMicroTask, GetPromiseUserInputEventHandlingState, IsJSMicroTask,
16    JSContext as RawJSContext, JSTracer, MaybeGetPromiseFromJSMicroTask, MutableHandleObject,
17    PromiseUserInputEventHandlingState, ToMaybeWrappedJSMicroTask,
18};
19use js::jsval::{JSVal, PrivateValue};
20use js::panic::wrap_panic;
21use js::realm::AutoRealm;
22use js::rust::wrappers2::{
23    EnqueueMicroTask, GetJobQueue, HasAnyMicroTasks, JS_DequeueNextMicroTask, JobQueueIsEmpty,
24    JobQueueMayNotBeEmpty, MaybeGetHostDefinedDataFromJSMicroTask, RunJSMicroTask, SetJobQueue,
25};
26use malloc_size_of::MallocSizeOf;
27use script_bindings::callback::TracedCallback;
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    pub(crate) callback: TracedCallback<VoidFunction>,
177    pub(crate) global: Dom<GlobalScope>,
178}
179
180impl MicrotaskRunnable for UserMicrotask {
181    fn handler(&self, cx: &mut JSContext) {
182        let mut realm = enter_auto_realm(cx, &*self.global);
183        let cx = &mut realm;
184        let _ = self
185            .callback
186            .Call_(cx, &*self.global, ExceptionHandling::Report);
187    }
188}
189
190fn microtask_from_jsval(val: JSVal) -> *mut Box<dyn MicrotaskRunnable> {
191    val.to_private() as *const Box<dyn MicrotaskRunnable> as *mut Box<dyn MicrotaskRunnable>
192}
193
194/// Add a new microtask to this queue. It will be invoked as part of the next
195/// microtask checkpoint.
196#[expect(unsafe_code)]
197pub(crate) fn enqueue(cx: &JSContext, task: Box<dyn MicrotaskRunnable>) {
198    let task = Box::new(task);
199    let raw = Box::into_raw(task);
200    unsafe { JobQueueMayNotBeEmpty(cx) };
201    assert!(unsafe { EnqueueMicroTask(cx, &PrivateValue(raw as *const c_void)) });
202}
203
204/// <https://html.spec.whatwg.org/multipage/#perform-a-microtask-checkpoint>
205/// Perform a microtask checkpoint, executing all queued microtasks until the queue is empty.
206#[expect(unsafe_code)]
207pub(crate) fn job_queue_microtask_checkpoint(
208    cx: &mut JSContext,
209    globalscopes: Vec<DomRoot<GlobalScope>>,
210) {
211    let job_queue: *mut RustJobQueue = unsafe { GetJobQueue(cx) } as _;
212    // Step 1. If the event loop's performing a microtask checkpoint is true, then return.
213    if unsafe { (*job_queue).draining } {
214        return;
215    }
216
217    // Step 2. Set the event loop's performing a microtask checkpoint to true.
218    unsafe {
219        (*job_queue).draining = true;
220    }
221
222    debug!("Now performing a microtask checkpoint");
223
224    rooted!(&in(cx) let mut generic_task: js::jsapi::GenericMicroTask);
225    rooted!(&in(cx) let mut js_micro_task: *mut js::jsapi::JSMicroTask);
226    rooted!(&in(cx) let mut execution_global: *mut js::jsapi::JSObject);
227    rooted!(&in(cx) let mut incumbent_global: *mut js::jsapi::JSObject);
228    rooted!(&in(cx) let mut data: *mut js::jsapi::JSObject);
229
230    // Step 3. While the event loop's microtask queue is not empty:
231    // based on https://spidermonkey.dev/blog/2026/01/15/job-responsibility.html#running-micro-tasks
232    // and https://searchfox.org/firefox-main/rev/7ae92e67d094086cd3e09918ec94b6278a948535/xpcom/base/CycleCollectedJSContext.cpp#1176
233    // and its helper functions
234    while unsafe { HasAnyMicroTasks(cx) } {
235        unsafe { JS_DequeueNextMicroTask(cx, generic_task.handle_mut()) };
236
237        // Notify the JS engine if the queue is now empty, enabling optimizations
238        // like skipping await microtask creation for resolved promises.
239        if !unsafe { HasAnyMicroTasks(cx) } {
240            unsafe { JobQueueIsEmpty(cx) };
241        }
242
243        // https://searchfox.org/firefox-main/rev/50691777d300fffc7d1f7844b59769109bc76f3e/xpcom/base/CycleCollectedJSContext.cpp#916
244        if !unsafe { IsJSMicroTask(generic_task.as_ptr()) } {
245            rooted!(&in(cx) let task = unsafe {
246                Box::from_raw(
247                    microtask_from_jsval(*generic_task),
248                )
249            });
250            task.handler(cx);
251            continue;
252        }
253
254        js_micro_task.set(unsafe { ToMaybeWrappedJSMicroTask(generic_task.as_ptr()) });
255        execution_global.set(unsafe { GetExecutionGlobalFromJSMicroTask(js_micro_task.get()) });
256        if execution_global.get().is_null() {
257            continue;
258        }
259        if !unsafe {
260            MaybeGetHostDefinedDataFromJSMicroTask(
261                js_micro_task.get(),
262                incumbent_global.handle_mut(),
263                data.handle_mut(),
264            )
265        } {
266            continue;
267        }
268
269        let interaction = if let Some(promise) =
270            NonNull::new(unsafe { MaybeGetPromiseFromJSMicroTask(js_micro_task.get()) })
271        {
272            unsafe { GetPromiseUserInputEventHandlingState(promise.as_ptr()) }
273        } else {
274            PromiseUserInputEventHandlingState::DontCare
275        };
276        let _maybe_user_interacting_guard =
277            if interaction == PromiseUserInputEventHandlingState::HadUserInteractionAtCreation {
278                Some(ScriptThread::user_interacting_guard())
279            } else {
280                None
281            };
282        let global_scope = unsafe { GlobalScope::from_object(execution_global.get()) };
283        run_a_script::<DomTypeHolder, _, _>(cx, &global_scope, |cx| {
284            let mut r = || {
285                let mut realm = AutoRealm::new_from_handle(cx, execution_global.handle());
286                let _ = unsafe { RunJSMicroTask(&mut realm, js_micro_task.handle()) };
287            };
288            if incumbent_global.get().is_null() {
289                r();
290            } else {
291                let global_scope = unsafe { GlobalScope::from_object(incumbent_global.get()) };
292                run_a_callback::<DomTypeHolder, _>(&global_scope, r);
293            }
294        });
295    }
296
297    // Step 4. For each environment settings object settingsObject whose responsible
298    // event loop is this event loop, notify about rejected promises given
299    // settingsObject's global object.
300    for global in globalscopes.clone().into_iter() {
301        notify_about_rejected_promises(cx, &global);
302    }
303
304    // https://html.spec.whatwg.org/multipage/#perform-a-microtask-checkpoint
305    // Step 5. Cleanup Indexed Database transactions.
306    // https://w3c.github.io/IndexedDB/#cleanup-indexed-database-transactions
307    // “These steps are invoked by [HTML]. They ensure that transactions created by a script call
308    // to transaction() are deactivated once the task that invoked the script has completed.”
309    for global in globalscopes.iter() {
310        if let Some(factory) = global.indexeddb_factory() {
311            let _ = factory.cleanup_indexeddb_transactions(cx);
312        }
313    }
314
315    // TODO: Step 6. Perform ClearKeptObjects().
316
317    // Step 7. Set the event loop's performing a microtask checkpoint to false.
318    unsafe {
319        (*job_queue).draining = false;
320    }
321    // TODO: Step 8. Record timing info for microtask checkpoint.
322}
323
324#[expect(unsafe_code)]
325pub(crate) fn job_queue_clear(cx: &JSContext) {
326    rooted!(&in(cx) let mut generic_task: js::jsapi::GenericMicroTask);
327    while unsafe { HasAnyMicroTasks(cx) } {
328        unsafe { JS_DequeueNextMicroTask(cx, generic_task.handle_mut()) };
329        if !unsafe { IsJSMicroTask(generic_task.as_ptr()) } {
330            let task = unsafe { Box::from_raw(microtask_from_jsval(*generic_task)) };
331            drop(task);
332        }
333    }
334}
335
336#[expect(unsafe_code)]
337unsafe extern "C" fn trace_non_gc_things_micro_task(trc: *mut JSTracer, val: *mut JSVal) {
338    wrap_panic(&mut || {
339        let task = microtask_from_jsval(unsafe { *val });
340        unsafe { (**task).trace(trc) };
341    })
342}