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