Skip to main content
This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/
scheduler.rs

1//! This module contains a scheduler.
2
3use std::cell::RefCell;
4use std::collections::BTreeMap;
5use std::rc::Rc;
6#[cfg(any(test, feature = "test"))]
7mod flush_wakers {
8    use std::cell::RefCell;
9    use std::task::Waker;
10
11    thread_local! {
12        static FLUSH_WAKERS: RefCell<Vec<Waker>> = const { RefCell::new(Vec::new()) };
13    }
14
15    #[cfg(all(
16        target_arch = "wasm32",
17        not(target_os = "wasi"),
18        not(feature = "not_browser_env")
19    ))]
20    pub(super) fn register(waker: Waker) {
21        FLUSH_WAKERS.with(|w| {
22            w.borrow_mut().push(waker);
23        });
24    }
25
26    pub(super) fn wake_all() {
27        FLUSH_WAKERS.with(|w| {
28            for waker in w.borrow_mut().drain(..) {
29                waker.wake();
30            }
31        });
32    }
33}
34
35/// Alias for `Rc<RefCell<T>>`
36pub type Shared<T> = Rc<RefCell<T>>;
37
38/// A routine which could be run.
39pub trait Runnable {
40    /// Runs a routine with a context instance.
41    fn run(self: Box<Self>);
42}
43
44struct QueueEntry {
45    task: Box<dyn Runnable>,
46}
47
48#[derive(Default)]
49struct FifoQueue {
50    inner: Vec<QueueEntry>,
51}
52
53impl FifoQueue {
54    const fn new() -> Self {
55        Self { inner: Vec::new() }
56    }
57
58    fn push(&mut self, task: Box<dyn Runnable>) {
59        self.inner.push(QueueEntry { task });
60    }
61
62    fn drain_into(&mut self, queue: &mut Vec<QueueEntry>) {
63        queue.append(&mut self.inner);
64    }
65}
66
67#[derive(Default)]
68
69struct TopologicalQueue {
70    /// The Binary Tree Map guarantees components with lower id (parent) is rendered first
71    inner: BTreeMap<usize, QueueEntry>,
72}
73
74impl TopologicalQueue {
75    const fn new() -> Self {
76        Self {
77            inner: BTreeMap::new(),
78        }
79    }
80
81    #[cfg(any(feature = "ssr", feature = "csr"))]
82    fn push(&mut self, component_id: usize, task: Box<dyn Runnable>) {
83        self.inner.insert(component_id, QueueEntry { task });
84    }
85
86    /// Take a single entry, preferring parents over children
87    #[inline]
88    fn pop_topmost(&mut self) -> Option<QueueEntry> {
89        self.inner.pop_first().map(|(_, v)| v)
90    }
91
92    /// Drain all entries, such that children are queued before parents
93    fn drain_post_order_into(&mut self, queue: &mut Vec<QueueEntry>) {
94        if self.inner.is_empty() {
95            return;
96        }
97        let rendered = std::mem::take(&mut self.inner);
98        // Children rendered lifecycle happen before parents.
99        queue.extend(rendered.into_values().rev());
100    }
101}
102
103/// This is a global scheduler suitable to schedule and run any tasks.
104#[derive(Default)]
105struct Scheduler {
106    // Main queue
107    main: FifoQueue,
108
109    // Component queues
110    destroy: FifoQueue,
111    create: FifoQueue,
112
113    props_update: FifoQueue,
114    update: FifoQueue,
115
116    render: TopologicalQueue,
117    render_first: TopologicalQueue,
118    render_priority: TopologicalQueue,
119
120    rendered_first: TopologicalQueue,
121    rendered: TopologicalQueue,
122}
123
124impl Scheduler {
125    const fn new() -> Self {
126        Self {
127            main: FifoQueue::new(),
128            destroy: FifoQueue::new(),
129            create: FifoQueue::new(),
130            props_update: FifoQueue::new(),
131            update: FifoQueue::new(),
132            render: TopologicalQueue::new(),
133            render_first: TopologicalQueue::new(),
134            render_priority: TopologicalQueue::new(),
135            rendered_first: TopologicalQueue::new(),
136            rendered: TopologicalQueue::new(),
137        }
138    }
139}
140
141/// Execute closure with a mutable reference to the scheduler
142#[inline]
143fn with<R>(f: impl FnOnce(&mut Scheduler) -> R) -> R {
144    thread_local! {
145        /// This is a global scheduler suitable to schedule and run any tasks.
146        ///
147        /// Exclusivity of mutable access is controlled by only accessing it through a set of public
148        /// functions.
149        static SCHEDULER: RefCell<Scheduler> = const { RefCell::new(Scheduler::new()) };
150    }
151
152    SCHEDULER.with(|s| f(&mut s.borrow_mut()))
153}
154
155/// Push a generic [Runnable] to be executed
156pub fn push(runnable: Box<dyn Runnable>) {
157    with(|s| s.main.push(runnable));
158    // Execute pending immediately. Necessary for runnables added outside the component lifecycle,
159    // which would otherwise be delayed.
160    start();
161}
162
163#[cfg(any(feature = "ssr", feature = "csr"))]
164mod feat_csr_ssr {
165    use super::*;
166    /// Push a component creation, first render and first rendered [Runnable]s to be executed
167    pub(crate) fn push_component_create(
168        component_id: usize,
169        create: Box<dyn Runnable>,
170        first_render: Box<dyn Runnable>,
171    ) {
172        with(|s| {
173            s.create.push(create);
174            s.render_first.push(component_id, first_render);
175        });
176    }
177
178    /// Push a component destruction [Runnable] to be executed
179    pub(crate) fn push_component_destroy(runnable: Box<dyn Runnable>) {
180        with(|s| s.destroy.push(runnable));
181    }
182
183    /// Push a component render [Runnable]s to be executed
184    pub(crate) fn push_component_render(component_id: usize, render: Box<dyn Runnable>) {
185        with(|s| {
186            s.render.push(component_id, render);
187        });
188    }
189
190    /// Push a component update [Runnable] to be executed
191    pub(crate) fn push_component_update(runnable: Box<dyn Runnable>) {
192        with(|s| s.update.push(runnable));
193    }
194}
195
196#[cfg(any(feature = "ssr", feature = "csr"))]
197pub(crate) use feat_csr_ssr::*;
198
199#[cfg(feature = "csr")]
200mod feat_csr {
201    use super::*;
202
203    pub(crate) fn push_component_rendered(
204        component_id: usize,
205        rendered: Box<dyn Runnable>,
206        first_render: bool,
207    ) {
208        with(|s| {
209            if first_render {
210                s.rendered_first.push(component_id, rendered);
211            } else {
212                s.rendered.push(component_id, rendered);
213            }
214        });
215    }
216
217    pub(crate) fn push_component_props_update(props_update: Box<dyn Runnable>) {
218        with(|s| s.props_update.push(props_update));
219    }
220}
221
222#[cfg(feature = "csr")]
223pub(crate) use feat_csr::*;
224
225#[cfg(feature = "hydration")]
226mod feat_hydration {
227    use super::*;
228
229    pub(crate) fn push_component_priority_render(component_id: usize, render: Box<dyn Runnable>) {
230        with(|s| {
231            s.render_priority.push(component_id, render);
232        });
233    }
234}
235
236#[cfg(feature = "hydration")]
237pub(crate) use feat_hydration::*;
238
239/// Execute any pending [Runnable]s
240#[cfg(any(
241    not(target_arch = "wasm32"),
242    target_os = "wasi",
243    feature = "not_browser_env",
244    test,
245    feature = "test"
246))]
247pub(crate) fn start_now() {
248    #[tracing::instrument(level = tracing::Level::DEBUG)]
249    fn scheduler_loop() {
250        let mut queue = vec![];
251        loop {
252            with(|s| s.fill_queue(&mut queue));
253            if queue.is_empty() {
254                break;
255            }
256            for r in queue.drain(..) {
257                r.task.run();
258            }
259        }
260    }
261
262    thread_local! {
263        // The lock is used to prevent recursion. If the lock cannot be acquired, it is because the
264        // `start()` method is being called recursively as part of a `runnable.run()`.
265        static LOCK: RefCell<()> = const { RefCell::new(()) };
266    }
267
268    LOCK.with(|l| {
269        if let Ok(_lock) = l.try_borrow_mut() {
270            scheduler_loop();
271            #[cfg(any(test, feature = "test"))]
272            flush_wakers::wake_all();
273        }
274    });
275}
276
277#[cfg(all(
278    target_arch = "wasm32",
279    not(target_os = "wasi"),
280    not(feature = "not_browser_env")
281))]
282mod arch {
283    use std::sync::atomic::{AtomicBool, Ordering};
284
285    use wasm_bindgen::prelude::*;
286
287    use crate::platform::spawn_local;
288
289    // Really only used as a `Cell<bool>` that is also `Sync`
290    static IS_SCHEDULED: AtomicBool = AtomicBool::new(false);
291    fn check_scheduled() -> bool {
292        // Since we can tolerate starting too many times, and we don't need to "see" any stores
293        // done in the scheduler, Relaxed ordering is fine
294        IS_SCHEDULED.load(Ordering::Relaxed)
295    }
296    fn set_scheduled(is: bool) {
297        // See comment in check_scheduled why Relaxed ordering is fine
298        IS_SCHEDULED.store(is, Ordering::Relaxed)
299    }
300
301    #[cfg(any(test, feature = "test"))]
302    pub(super) fn is_scheduled() -> bool {
303        check_scheduled()
304    }
305
306    const YIELD_DEADLINE_MS: f64 = 16.0;
307
308    #[wasm_bindgen]
309    unsafe extern "C" {
310        #[wasm_bindgen(js_name = setTimeout)]
311        fn set_timeout(handler: &js_sys::Function, timeout: i32) -> i32;
312    }
313
314    fn run_scheduler(mut queue: Vec<super::QueueEntry>) {
315        let deadline = js_sys::Date::now() + YIELD_DEADLINE_MS;
316
317        loop {
318            super::with(|s| s.fill_queue(&mut queue));
319            if queue.is_empty() {
320                break;
321            }
322            for r in queue.drain(..) {
323                r.task.run();
324            }
325            if js_sys::Date::now() >= deadline {
326                // Only yield when no DOM-mutating work is pending, so event
327                // handlers that fire during the yield see a consistent DOM.
328                let can_yield = super::with(|s| s.can_yield());
329                if can_yield {
330                    let cb = Closure::once_into_js(move || run_scheduler(queue));
331                    set_timeout(cb.unchecked_ref(), 0);
332                    return;
333                }
334            }
335        }
336
337        set_scheduled(false);
338        #[cfg(any(test, feature = "test"))]
339        super::flush_wakers::wake_all();
340    }
341
342    /// We delay the start of the scheduler to the end of the micro task queue.
343    /// So any messages that needs to be queued can be queued.
344    /// Once running, we yield to the browser every ~16ms, but only at points
345    /// where the DOM is in a consistent state (no pending renders/destroys).
346    pub(crate) fn start() {
347        if check_scheduled() {
348            return;
349        }
350        set_scheduled(true);
351        spawn_local(async {
352            run_scheduler(vec![]);
353        });
354    }
355}
356
357#[cfg(any(
358    not(target_arch = "wasm32"),
359    target_os = "wasi",
360    feature = "not_browser_env"
361))]
362mod arch {
363    // Delayed rendering is not very useful in the context of server-side rendering.
364    // There are no event listeners or other high priority events that need to be
365    // processed and we risk of having a future un-finished.
366    // Until scheduler is future-capable which means we can join inside a future,
367    // it can remain synchronous.
368    pub(crate) fn start() {
369        super::start_now();
370    }
371}
372
373pub(crate) use arch::*;
374
375/// Flush all pending scheduler work, ensuring all rendering and lifecycle callbacks complete.
376///
377/// On browser WebAssembly targets, the scheduler defers its work to the microtask queue.
378/// This function registers a waker that is notified when `start_now()` finishes draining all
379/// queues, providing proper event-driven render-complete notification without arbitrary sleeps.
380///
381/// On non-browser targets, the scheduler runs synchronously so this simply drains pending work.
382///
383/// Use this in tests after mounting or updating a component to ensure all rendering has
384/// completed before making assertions.
385#[cfg(all(
386    any(test, feature = "test"),
387    target_arch = "wasm32",
388    not(target_os = "wasi"),
389    not(feature = "not_browser_env")
390))]
391pub async fn flush() {
392    std::future::poll_fn(|cx| {
393        start_now();
394
395        if arch::is_scheduled() {
396            flush_wakers::register(cx.waker().clone());
397            std::task::Poll::Pending
398        } else {
399            std::task::Poll::Ready(())
400        }
401    })
402    .await
403}
404
405/// Flush all pending scheduler work, ensuring all rendering and lifecycle callbacks complete.
406///
407/// On non-browser targets, the scheduler runs synchronously so this simply drains pending work.
408#[cfg(all(
409    any(test, feature = "test"),
410    not(all(
411        target_arch = "wasm32",
412        not(target_os = "wasi"),
413        not(feature = "not_browser_env")
414    ))
415))]
416pub async fn flush() {
417    start_now();
418}
419
420impl Scheduler {
421    /// Returns true when no DOM-mutating work is pending, meaning it's safe to
422    /// yield to the browser without leaving the DOM in an inconsistent state.
423    #[cfg(all(
424        target_arch = "wasm32",
425        not(target_os = "wasi"),
426        not(feature = "not_browser_env")
427    ))]
428    fn can_yield(&self) -> bool {
429        self.destroy.inner.is_empty()
430            && self.create.inner.is_empty()
431            && self.render_first.inner.is_empty()
432            && self.render.inner.is_empty()
433            && self.render_priority.inner.is_empty()
434    }
435
436    /// Fill vector with tasks to be executed according to Runnable type execution priority
437    ///
438    /// This method is optimized for typical usage, where possible, but does not break on
439    /// non-typical usage (like scheduling renders in [crate::Component::create()] or
440    /// [crate::Component::rendered()] calls).
441    fn fill_queue(&mut self, to_run: &mut Vec<QueueEntry>) {
442        // Placed first to avoid as much needless work as possible, handling all the other events.
443        // Drained completely, because they are the highest priority events anyway.
444        self.destroy.drain_into(to_run);
445
446        // Create events can be batched, as they are typically just for object creation
447        self.create.drain_into(to_run);
448
449        // These typically do nothing and don't spawn any other events - can be batched.
450        // Should be run only after all first renders have finished.
451        if !to_run.is_empty() {
452            return;
453        }
454
455        // First render must never be skipped and takes priority over main, because it may need
456        // to init `NodeRef`s
457        //
458        // Should be processed one at time, because they can spawn more create and rendered events
459        // for their children.
460        if let Some(r) = self.render_first.pop_topmost() {
461            to_run.push(r);
462            return;
463        }
464
465        self.props_update.drain_into(to_run);
466
467        // Priority rendering
468        //
469        // This is needed for hydration subsequent render to fix node refs.
470        if let Some(r) = self.render_priority.pop_topmost() {
471            to_run.push(r);
472            return;
473        }
474
475        // Children rendered lifecycle happen before parents.
476        self.rendered_first.drain_post_order_into(to_run);
477
478        // Updates are after the first render to ensure we always have the entire child tree
479        // rendered, once an update is processed.
480        //
481        // Can be batched, as they can cause only non-first renders.
482        self.update.drain_into(to_run);
483
484        // Likely to cause duplicate renders via component updates, so placed before them
485        self.main.drain_into(to_run);
486
487        // Run after all possible updates to avoid duplicate renders.
488        //
489        // Should be processed one at time, because they can spawn more create and first render
490        // events for their children.
491        if !to_run.is_empty() {
492            return;
493        }
494
495        // Should be processed one at time, because they can spawn more create and rendered events
496        // for their children.
497        if let Some(r) = self.render.pop_topmost() {
498            to_run.push(r);
499            return;
500        }
501        // These typically do nothing and don't spawn any other events - can be batched.
502        // Should be run only after all renders have finished.
503        // Children rendered lifecycle happen before parents.
504        self.rendered.drain_post_order_into(to_run);
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    #[test]
513    fn push_executes_runnables_immediately() {
514        use std::cell::Cell;
515
516        thread_local! {
517            static FLAG: Cell<bool> = const { Cell::new(false) };
518        }
519
520        struct Test;
521        impl Runnable for Test {
522            fn run(self: Box<Self>) {
523                FLAG.with(|v| v.set(true));
524            }
525        }
526
527        push(Box::new(Test));
528        FLAG.with(|v| assert!(v.get()));
529    }
530}