Skip to main content

kozan_scheduler/
microtask.rs

1//! Microtask queue — HTML spec "perform a microtask checkpoint".
2//!
3//! # What are microtasks?
4//!
5//! Microtasks run between macrotasks. After each macrotask completes,
6//! the microtask queue is **drained completely** — including any new
7//! microtasks enqueued during the drain. This is the HTML spec behavior.
8//!
9//! # Chrome mapping
10//!
11//! | Chrome / Spec              | Kozan                           |
12//! |----------------------------|---------------------------------|
13//! | `Promise.then()`           | `queue_microtask(callback)`     |
14//! | `queueMicrotask()`         | `queue_microtask(callback)`     |
15//! | `MutationObserver`         | (future: observer microtasks)   |
16//! | Microtask checkpoint       | `drain()`                       |
17//!
18//! # Performance
19//!
20//! - `VecDeque<Microtask>` — O(1) push/pop, cache-friendly.
21//! - Drain is O(n) where n = total microtasks including newly enqueued.
22//! - No allocations during drain (`VecDeque` reuses capacity).
23//!
24//! # Safety: re-entrancy
25//!
26//! `drain()` pops one microtask at a time and runs it. If the microtask
27//! enqueues more microtasks, they are pushed to the same queue and will
28//! be processed in the same drain cycle. This matches the spec exactly.
29//! The drain loop terminates when the queue is empty.
30
31use std::collections::VecDeque;
32
33/// A microtask callback. Boxed `FnOnce()`, consumed on execution.
34///
35/// Microtasks are always highest priority and always drain completely.
36/// There is no priority differentiation within microtasks.
37pub struct Microtask {
38    callback: Box<dyn FnOnce()>,
39}
40
41impl Microtask {
42    /// Create a new microtask.
43    #[inline]
44    pub fn new(callback: impl FnOnce() + 'static) -> Self {
45        Self {
46            callback: Box::new(callback),
47        }
48    }
49
50    /// Execute this microtask, consuming the callback.
51    #[inline]
52    pub fn run(self) {
53        (self.callback)();
54    }
55}
56
57/// The microtask queue — drained completely after each macrotask.
58///
59/// Like Chrome's microtask queue in `V8::MicrotaskQueue` / Blink's
60/// `EventLoop::PerformMicrotaskCheckpoint()`.
61///
62/// # Spec behavior
63///
64/// > "If the microtask queue is not empty:
65/// >   1. Let oldestMicrotask be the result of dequeuing from microtask queue.
66/// >   2. Set event loop's currently running task to oldestMicrotask.
67/// >   3. Run oldestMicrotask.
68/// >   4. Set event loop's currently running task back to null.
69/// >   5. Go to step 1."
70///
71/// Key: microtasks enqueued DURING drain are also drained in the same cycle.
72pub struct MicrotaskQueue {
73    queue: VecDeque<Microtask>,
74}
75
76impl MicrotaskQueue {
77    /// Create an empty microtask queue.
78    #[inline]
79    #[must_use]
80    pub fn new() -> Self {
81        Self {
82            queue: VecDeque::new(),
83        }
84    }
85
86    /// Enqueue a microtask.
87    ///
88    /// If called during [`drain()`](Self::drain), the new microtask
89    /// will be processed in the same drain cycle.
90    #[inline]
91    pub fn enqueue(&mut self, microtask: Microtask) {
92        self.queue.push_back(microtask);
93    }
94
95    /// Convenience: enqueue a closure as a microtask.
96    #[inline]
97    pub fn queue_microtask(&mut self, callback: impl FnOnce() + 'static) {
98        self.enqueue(Microtask::new(callback));
99    }
100
101    /// Drain the microtask queue completely (microtask checkpoint).
102    ///
103    /// Runs all microtasks, including any new ones enqueued during drain.
104    /// Returns the number of microtasks executed.
105    ///
106    /// # Re-entrancy
107    ///
108    /// This takes microtasks one at a time via `pop_front()`. If a
109    /// microtask enqueues more, they appear at the back and will be
110    /// processed before `drain()` returns. To prevent infinite loops
111    /// from buggy user code, a safety limit is enforced.
112    pub fn drain(&mut self) -> usize {
113        /// Maximum microtasks per drain to prevent infinite loops.
114        /// Chrome doesn't have a hard limit, but infinite microtask loops
115        /// are a known footgun. 10,000 is generous for legitimate use.
116        const MAX_DRAIN: usize = 10_000;
117
118        let mut executed = 0;
119        while let Some(microtask) = self.queue.pop_front() {
120            microtask.run();
121            executed += 1;
122
123            if executed >= MAX_DRAIN {
124                // Safety valve. In debug builds, panic to catch the bug.
125                debug_assert!(
126                    false,
127                    "microtask drain exceeded {MAX_DRAIN} — possible infinite loop"
128                );
129                break;
130            }
131        }
132        executed
133    }
134
135    /// Number of pending microtasks.
136    #[inline]
137    #[must_use]
138    pub fn len(&self) -> usize {
139        self.queue.len()
140    }
141
142    /// Whether the microtask queue is empty.
143    #[inline]
144    #[must_use]
145    pub fn is_empty(&self) -> bool {
146        self.queue.is_empty()
147    }
148}
149
150impl Default for MicrotaskQueue {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use std::cell::Cell;
160    use std::rc::Rc;
161
162    #[test]
163    fn empty_drain_returns_zero() {
164        let mut q = MicrotaskQueue::new();
165        assert_eq!(q.drain(), 0);
166        assert!(q.is_empty());
167    }
168
169    #[test]
170    fn single_microtask_executes() {
171        let called = Rc::new(Cell::new(false));
172        let mut q = MicrotaskQueue::new();
173
174        let c = called.clone();
175        q.queue_microtask(move || c.set(true));
176
177        assert_eq!(q.len(), 1);
178        assert_eq!(q.drain(), 1);
179        assert!(called.get());
180        assert!(q.is_empty());
181    }
182
183    #[test]
184    fn drain_order_is_fifo() {
185        let log = Rc::new(std::cell::RefCell::new(Vec::new()));
186        let mut q = MicrotaskQueue::new();
187
188        for i in 0..5 {
189            let l = log.clone();
190            q.queue_microtask(move || l.borrow_mut().push(i));
191        }
192
193        q.drain();
194        assert_eq!(*log.borrow(), vec![0, 1, 2, 3, 4]);
195    }
196
197    #[test]
198    fn drain_includes_newly_enqueued() {
199        // This is the KEY spec behavior:
200        // Microtask A enqueues microtask B → B runs in the same drain.
201        let log = Rc::new(std::cell::RefCell::new(Vec::new()));
202        let mut q = MicrotaskQueue::new();
203
204        // Seed: microtask that enqueues two more.
205        let l1 = log.clone();
206        q.queue_microtask(move || {
207            l1.borrow_mut().push(1);
208        });
209
210        let l2 = log.clone();
211        q.queue_microtask(move || {
212            l2.borrow_mut().push(2);
213        });
214
215        assert_eq!(q.drain(), 2);
216        assert_eq!(*log.borrow(), vec![1, 2]);
217
218        // Test re-entrant enqueue by manually simulating:
219        // First microtask pushes to the queue, then we continue draining.
220        let log2 = Rc::new(std::cell::RefCell::new(Vec::new()));
221        let mut q2 = MicrotaskQueue::new();
222
223        // We can't actually enqueue during drain with &mut self,
224        // but the real scheduler will use a shared reference.
225        // For now, test that sequential enqueue + drain works correctly.
226        let l = log2.clone();
227        q2.queue_microtask(move || l.borrow_mut().push("first"));
228        let l = log2.clone();
229        q2.queue_microtask(move || l.borrow_mut().push("second"));
230
231        assert_eq!(q2.drain(), 2);
232        assert_eq!(*log2.borrow(), vec!["first", "second"]);
233    }
234
235    #[test]
236    fn enqueue_via_struct() {
237        let called = Rc::new(Cell::new(false));
238        let mut q = MicrotaskQueue::new();
239
240        let c = called.clone();
241        q.enqueue(Microtask::new(move || c.set(true)));
242        q.drain();
243
244        assert!(called.get());
245    }
246
247    #[test]
248    fn len_and_is_empty() {
249        let mut q = MicrotaskQueue::new();
250        assert!(q.is_empty());
251        assert_eq!(q.len(), 0);
252
253        q.queue_microtask(|| {});
254        assert!(!q.is_empty());
255        assert_eq!(q.len(), 1);
256
257        q.drain();
258        assert!(q.is_empty());
259    }
260}