Skip to main content

script/
microtask.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::cell::Cell;
10use std::mem;
11use std::rc::Rc;
12
13use js::context::JSContext;
14use js::rust::wrappers2::JobQueueMayNotBeEmpty;
15use malloc_size_of::MallocSizeOf;
16use script_bindings::cell::DomRefCell;
17use script_bindings::root::Dom;
18
19use crate::JSTraceable;
20use crate::dom::bindings::callback::ExceptionHandling;
21use crate::dom::bindings::codegen::Bindings::PromiseBinding::PromiseJobCallback;
22use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
23use crate::dom::bindings::root::DomRoot;
24use crate::dom::globalscope::GlobalScope;
25use crate::realms::enter_auto_realm;
26use crate::script_runtime::notify_about_rejected_promises;
27use crate::script_thread::ScriptThread;
28
29/// A collection of microtasks in FIFO order.
30#[derive(Default, JSTraceable, MallocSizeOf)]
31pub(crate) struct MicrotaskQueue {
32    /// The list of enqueued microtasks that will be invoked at the next microtask checkpoint.
33    microtask_queue: DomRefCell<Vec<Box<dyn MicrotaskRunnable>>>,
34    /// <https://html.spec.whatwg.org/multipage/#performing-a-microtask-checkpoint>
35    performing_a_microtask_checkpoint: Cell<bool>,
36}
37
38#[derive(JSTraceable, MallocSizeOf)]
39pub struct NotifyMutationObserversMicrotask;
40
41impl NotifyMutationObserversMicrotask {
42    pub(crate) fn new() -> Self {
43        Self
44    }
45}
46
47impl MicrotaskRunnable for NotifyMutationObserversMicrotask {
48    fn handler(&self, cx: &mut JSContext) {
49        ScriptThread::mutation_observers().notify_mutation_observers(cx);
50    }
51}
52
53#[derive(JSTraceable, MallocSizeOf)]
54pub struct CustomElementReactionMicrotask;
55
56impl CustomElementReactionMicrotask {
57    pub(crate) fn new() -> Self {
58        Self
59    }
60}
61
62impl MicrotaskRunnable for CustomElementReactionMicrotask {
63    fn handler(&self, cx: &mut JSContext) {
64        ScriptThread::invoke_backup_element_queue(cx);
65    }
66}
67
68pub(crate) trait MicrotaskRunnable: JSTraceable + MallocSizeOf {
69    // must also take care of entering the realm
70    fn handler(&self, _cx: &mut JSContext) {}
71}
72
73/// A promise callback scheduled to run during the next microtask checkpoint (#4283).
74#[derive(JSTraceable, MallocSizeOf)]
75#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
76pub(crate) struct EnqueuedPromiseCallback {
77    #[conditional_malloc_size_of]
78    pub(crate) callback: Rc<PromiseJobCallback>,
79    pub(crate) global: Dom<GlobalScope>,
80    pub(crate) is_user_interacting: bool,
81}
82
83impl MicrotaskRunnable for EnqueuedPromiseCallback {
84    fn handler(&self, cx: &mut JSContext) {
85        let _guard = ScriptThread::user_interacting_guard();
86        let mut realm = enter_auto_realm(cx, &*self.global);
87        let cx = &mut realm;
88        let _ = self
89            .callback
90            .Call_(cx, &*self.global, ExceptionHandling::Report);
91    }
92}
93
94/// A microtask that comes from a queueMicrotask() Javascript call,
95/// identical to EnqueuedPromiseCallback once it's on the queue
96#[derive(JSTraceable, MallocSizeOf)]
97#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
98pub(crate) struct UserMicrotask {
99    #[conditional_malloc_size_of]
100    pub(crate) callback: Rc<VoidFunction>,
101    pub(crate) global: Dom<GlobalScope>,
102}
103
104impl MicrotaskRunnable for UserMicrotask {
105    fn handler(&self, cx: &mut JSContext) {
106        let mut realm = enter_auto_realm(cx, &*self.global);
107        let cx = &mut realm;
108        let _ = self
109            .callback
110            .Call_(cx, &*self.global, ExceptionHandling::Report);
111    }
112}
113
114impl MicrotaskQueue {
115    /// Add a new microtask to this queue. It will be invoked as part of the next
116    /// microtask checkpoint.
117    #[expect(unsafe_code)]
118    pub(crate) fn enqueue(&self, cx: &JSContext, task: Box<dyn MicrotaskRunnable>) {
119        self.microtask_queue.borrow_mut().push(task);
120        unsafe { JobQueueMayNotBeEmpty(cx) };
121    }
122
123    /// <https://html.spec.whatwg.org/multipage/#perform-a-microtask-checkpoint>
124    /// Perform a microtask checkpoint, executing all queued microtasks until the queue is empty.
125    #[expect(unsafe_code)]
126    pub(crate) fn checkpoint(&self, cx: &mut JSContext, globalscopes: Vec<DomRoot<GlobalScope>>) {
127        // Step 1. If the event loop's performing a microtask checkpoint is true, then return.
128        if self.performing_a_microtask_checkpoint.get() {
129            return;
130        }
131
132        // Step 2. Set the event loop's performing a microtask checkpoint to true.
133        self.performing_a_microtask_checkpoint.set(true);
134
135        debug!("Now performing a microtask checkpoint");
136
137        // Step 3. While the event loop's microtask queue is not empty:
138        while !self.microtask_queue.borrow().is_empty() {
139            rooted_vec!(let mut pending_queue);
140            mem::swap(&mut *pending_queue, &mut *self.microtask_queue.borrow_mut());
141
142            for (idx, job) in pending_queue.iter().enumerate() {
143                if idx == pending_queue.len() - 1 && self.microtask_queue.borrow().is_empty() {
144                    unsafe { js::rust::wrappers2::JobQueueIsEmpty(cx) };
145                }
146
147                job.handler(cx);
148            }
149        }
150
151        // Step 4. For each environment settings object settingsObject whose responsible
152        // event loop is this event loop, notify about rejected promises given
153        // settingsObject's global object.
154        for global in globalscopes.clone().into_iter() {
155            notify_about_rejected_promises(cx, &global);
156        }
157
158        // https://html.spec.whatwg.org/multipage/#perform-a-microtask-checkpoint
159        // Step 5. Cleanup Indexed Database transactions.
160        // https://w3c.github.io/IndexedDB/#cleanup-indexed-database-transactions
161        // “These steps are invoked by [HTML]. They ensure that transactions created by a script call
162        // to transaction() are deactivated once the task that invoked the script has completed.”
163        for global in globalscopes.iter() {
164            if let Some(factory) = global.indexeddb_factory() {
165                let _ = factory.cleanup_indexeddb_transactions(cx);
166            }
167        }
168
169        // TODO: Step 6. Perform ClearKeptObjects().
170
171        // Step 7. Set the event loop's performing a microtask checkpoint to false.
172        self.performing_a_microtask_checkpoint.set(false);
173        // TODO: Step 8. Record timing info for microtask checkpoint.
174    }
175
176    pub(crate) fn empty(&self) -> bool {
177        self.microtask_queue.borrow().is_empty()
178    }
179
180    pub(crate) fn clear(&self) {
181        self.microtask_queue.borrow_mut().clear();
182    }
183}