|
@@ -1,3 +1,73 @@
|
|
|
+/*
|
|
|
+Welcome to Dioxus's cooperative, priority-based scheduler.
|
|
|
+
|
|
|
+I hope you enjoy your stay.
|
|
|
+
|
|
|
+Some essential reading:
|
|
|
+- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200
|
|
|
+- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440
|
|
|
+- https://github.com/WICG/is-input-pending
|
|
|
+- https://web.dev/rail/
|
|
|
+- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react
|
|
|
+
|
|
|
+# What's going on?
|
|
|
+
|
|
|
+Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI
|
|
|
+snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no
|
|
|
+point if being "fast" if you can't also be "responsive."
|
|
|
+
|
|
|
+As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned
|
|
|
+priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms).
|
|
|
+The controller responsible for this priority management is called the "scheduler" and is responsible for juggle many
|
|
|
+different types of work simultaneously.
|
|
|
+
|
|
|
+# How does it work?
|
|
|
+
|
|
|
+Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked.
|
|
|
+React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of
|
|
|
+React, and the UI still stays snappy!
|
|
|
+
|
|
|
+While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do
|
|
|
+this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser
|
|
|
+has time to paint and run animations. When the browser is finished, we can step in and continue our work.
|
|
|
+
|
|
|
+React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not
|
|
|
+necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially
|
|
|
+"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause
|
|
|
+our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields
|
|
|
+so Rust's async machinery can take over, allowing us to customize when exactly to pause it.
|
|
|
+
|
|
|
+React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For
|
|
|
+now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes
|
|
|
+before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes
|
|
|
+are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine
|
|
|
+future is paused and saved (self-referentially).
|
|
|
+
|
|
|
+# Priorty System
|
|
|
+
|
|
|
+So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that
|
|
|
+doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where
|
|
|
+priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as
|
|
|
+a "lower" priority. That way, we can interrupt long-running low-prioty work with short-running high-priority work.
|
|
|
+
|
|
|
+React's priority system is quite complex.
|
|
|
+
|
|
|
+There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only
|
|
|
+uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on.
|
|
|
+
|
|
|
+For Dioxus, we're going with a 4 tier priorty system:
|
|
|
+- Sync: Things that need to be done by the next frame, like TextInput on controlled elements
|
|
|
+- High: for events that block all others - clicks, keyboard, and hovers
|
|
|
+- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events)
|
|
|
+- Low: set_state called asynchronously, and anything generated by suspense
|
|
|
+
|
|
|
+In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed
|
|
|
+work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave
|
|
|
+as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to
|
|
|
+do anything too arduous from onInput.
|
|
|
+
|
|
|
+For the rest, we defer to the rIC period and work down each queue from high to low.
|
|
|
+*/
|
|
|
use std::cell::{Cell, RefCell, RefMut};
|
|
|
use std::fmt::Display;
|
|
|
use std::{cell::UnsafeCell, rc::Rc};
|
|
@@ -7,6 +77,7 @@ use crate::innerlude::*;
|
|
|
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
|
|
use futures_util::stream::FuturesUnordered;
|
|
|
use fxhash::{FxHashMap, FxHashSet};
|
|
|
+use indexmap::IndexSet;
|
|
|
use slab::Slab;
|
|
|
use smallvec::SmallVec;
|
|
|
|
|
@@ -376,14 +447,42 @@ impl Scheduler {
|
|
|
// unsafe { std::mem::transmute(fib) }
|
|
|
}
|
|
|
|
|
|
- /// If a the fiber finishes its works (IE needs to be committed) the scheduler will drop the dirty scope
|
|
|
+ /// The primary workhorse of the VirtualDOM.
|
|
|
///
|
|
|
+ /// Uses some fairly complex logic to schedule what work should be produced.
|
|
|
+ ///
|
|
|
+ /// Returns a list of successful mutations.
|
|
|
///
|
|
|
///
|
|
|
pub async fn work_with_deadline<'a>(
|
|
|
&'a mut self,
|
|
|
deadline: &mut Pin<Box<impl FusedFuture<Output = ()>>>,
|
|
|
) -> Vec<Mutations<'a>> {
|
|
|
+ /*
|
|
|
+ Strategy:
|
|
|
+ - When called, check for any UI events that might've been received since the last frame.
|
|
|
+ - Dump all UI events into a "pending discrete" queue and a "pending continuous" queue.
|
|
|
+
|
|
|
+ - If there are any pending discrete events, then elevate our priorty level. If our priority level is already "high,"
|
|
|
+ then we need to finish the high priority work first. If the current work is "low" then analyze what scopes
|
|
|
+ will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need
|
|
|
+ to bump the other work out of the way, or choose to process it so we don't have any conflicts.
|
|
|
+ 'static components have a leg up here since their work can be re-used among multiple scopes.
|
|
|
+ "High priority" is only for blocking! Should only be used on "clicks"
|
|
|
+
|
|
|
+ - If there are no pending discrete events, then check for continuous events. These can be completely batched
|
|
|
+
|
|
|
+
|
|
|
+ Open questions:
|
|
|
+ - what if we get two clicks from the component during the same slice?
|
|
|
+ - should we batch?
|
|
|
+ - react says no - they are continuous
|
|
|
+ - but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ */
|
|
|
let mut committed_mutations = Vec::new();
|
|
|
|
|
|
// TODO:
|
|
@@ -509,6 +608,28 @@ impl Scheduler {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+pub struct PriortySystem {
|
|
|
+ pub dirty_scopes: IndexSet<ScopeId>,
|
|
|
+ pub machine: DiffMachine<'static>,
|
|
|
+}
|
|
|
+
|
|
|
+impl PriortySystem {
|
|
|
+ pub fn new() -> Self {
|
|
|
+ Self {
|
|
|
+ machine: DiffMachine::new(edits, shared),
|
|
|
+ dirty_scopes: Default::default(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fn has_work(&self) -> bool {
|
|
|
+ todo!()
|
|
|
+ }
|
|
|
+
|
|
|
+ fn work(&mut self) {
|
|
|
+ let scope = self.dirty_scopes.pop();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
pub struct TaskHandle {
|
|
|
pub sender: UnboundedSender<SchedulerMsg>,
|
|
|
pub our_id: u64,
|
|
@@ -530,30 +651,6 @@ impl TaskHandle {
|
|
|
pub fn restart(&self) {}
|
|
|
}
|
|
|
|
|
|
-#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)]
|
|
|
-pub struct DirtyScope {
|
|
|
- height: u32,
|
|
|
- start_tick: u32,
|
|
|
-}
|
|
|
-
|
|
|
-pub struct PriortySystem {
|
|
|
- pub pending_scopes: Vec<ScopeId>,
|
|
|
- pub dirty_scopes: HashSet<ScopeId>,
|
|
|
-}
|
|
|
-
|
|
|
-impl PriortySystem {
|
|
|
- pub fn new() -> Self {
|
|
|
- Self {
|
|
|
- pending_scopes: Default::default(),
|
|
|
- dirty_scopes: Default::default(),
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- fn has_work(&self) -> bool {
|
|
|
- self.pending_scopes.len() > 0 || self.dirty_scopes.len() > 0
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
|
|
pub struct ScopeId(pub usize);
|
|
|
|
|
@@ -571,28 +668,6 @@ impl ElementId {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent"
|
|
|
-// //
|
|
|
-// // Async events don't necessarily propagate into a scope being ran. It's up to the event itself
|
|
|
-// // to force an update for itself.
|
|
|
-// //
|
|
|
-// // Most async events should have a low priority.
|
|
|
-// //
|
|
|
-// // This type exists for the task/concurrency system to signal that a task is ready.
|
|
|
-// // However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its
|
|
|
-// // own re-run.
|
|
|
-// AsyncEvent {
|
|
|
-// should_rerender: bool,
|
|
|
-// },
|
|
|
-
|
|
|
-// // Suspense events are a type of async event generated when suspended nodes are ready to be processed.
|
|
|
-// //
|
|
|
-// // they have the lowest priority
|
|
|
-// SuspenseEvent {
|
|
|
-// hook_idx: usize,
|
|
|
-// domnode: Rc<Cell<Option<ElementId>>>,
|
|
|
-// },
|
|
|
-
|
|
|
/// Priority of Event Triggers.
|
|
|
///
|
|
|
/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus
|
|
@@ -616,11 +691,16 @@ impl ElementId {
|
|
|
/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
|
|
|
pub enum EventPriority {
|
|
|
+ /// Work that must be completed during the EventHandler phase
|
|
|
+ ///
|
|
|
+ ///
|
|
|
+ Immediate = 3,
|
|
|
+
|
|
|
/// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.
|
|
|
///
|
|
|
/// This is typically reserved for things like user interaction.
|
|
|
///
|
|
|
- /// React calls these "discrete" events, but with an extra category of "user-blocking".
|
|
|
+ /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate).
|
|
|
High = 2,
|
|
|
|
|
|
/// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
|