Procházet zdrojové kódy

wip: working on async diff

Jonathan Kelley před 3 roky
rodič
revize
f41cff5

+ 8 - 1
examples/ssr.rs

@@ -5,7 +5,7 @@ use dioxus::ssr;
 
 fn main() {
     let mut vdom = VirtualDom::new(App);
-    vdom.rebuild_in_place().expect("Rebuilding failed");
+    // vdom.rebuild_in_place().expect("Rebuilding failed");
     println!("{}", ssr::render_vdom(&vdom, |c| c));
 }
 
@@ -17,3 +17,10 @@ static App: FC<()> = |cx| {
         }
     ))
 };
+
+struct MyProps<'a> {
+    text: &'a str,
+}
+fn App2<'a>(cx: Context<'a, MyProps>) -> DomTree<'a> {
+    None
+}

+ 51 - 35
packages/core/src/diff.rs

@@ -1,9 +1,12 @@
-//! This module contains the stateful DiffMachine and all methods to diff VNodes, their properties, and their children.
-//! The DiffMachine calculates the diffs between the old and new frames, updates the new nodes, and modifies the real dom.
+//! This module contains the stateful PriorityFiber and all methods to diff VNodes, their properties, and their children.
+//!
+//! The [`PriorityFiber`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set
+//! of mutations for the RealDom to apply.
 //!
 //! ## Notice:
 //! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support
-//! Components, Fragments, Suspense, SubTree memoization, and additional batching operations.
+//! Components, Fragments, Suspense, SubTree memoization, incremental diffing, cancelation, NodeRefs, and additional
+//! batching operations.
 //!
 //! ## Implementation Details:
 //!
@@ -11,12 +14,13 @@
 //! --------------------
 //! All nodes are addressed by their IDs. The RealDom provides an imperative interface for making changes to these nodes.
 //! We don't necessarily require that DOM changes happen instnatly during the diffing process, so the implementor may choose
-//! to batch nodes if it is more performant for their application. The expectation is that renderers use a Slotmap for nodes
-//! whose keys can be converted to u64 on FFI boundaries.
+//! to batch nodes if it is more performant for their application. The element IDs are indicies into the internal element
+//! array. The expectation is that implemenetors will use the ID as an index into a Vec of real nodes, allowing for passive
+//! garbage collection as the VirtualDOM replaces old nodes.
 //!
-//! When new nodes are created through `render`, they won't know which real node they correspond to. During diffing, we
-//! always make sure to copy over the ID. If we don't do this properly, the ElementId will be populated incorrectly and
-//! brick the user's page.
+//! When new vnodes are created through `cx.render`, they won't know which real node they correspond to. During diffing,
+//! we always make sure to copy over the ID. If we don't do this properly, the ElementId will be populated incorrectly
+//! and brick the user's page.
 //!
 //! ### Fragment Support
 //!
@@ -26,6 +30,9 @@
 //! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. This is
 //! slightly inefficient, but represents a such an uncommon use case that it is not worth optimizing.
 //!
+//! Other implementations either don't support fragments or use a "child + sibling" pattern to represent them. Our code is
+//! vastly simpler and more performant when we can just create a placeholder element while the fragment has no children.
+//!
 //! ## Subtree Memoization
 //! -----------------------
 //! We also employ "subtree memoization" which saves us from having to check trees which take no dynamic content. We can
@@ -35,13 +42,15 @@
 //! rsx!( div { class: "hello world", "this node is entirely static" } )
 //! ```
 //! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so its up to the reconciler to
-//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP
+//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP and depends on comp-time
+//! hashing of the subtree from the rsx! macro. We do a very limited form of static analysis via static string pointers as
+//! a way of short-circuiting the most expensive checks.
 //!
 //! ## Bloom Filter and Heuristics
 //! ------------------------------
 //! For all components, we employ some basic heuristics to speed up allocations and pre-size bump arenas. The heuristics are
-//! currently very rough, but will get better as time goes on. For FFI, we recommend using a bloom filter to cache strings.
-//!
+//! currently very rough, but will get better as time goes on. The information currently tracked includes the size of a
+//! bump arena after first render, the number of hooks, and the number of nodes in the tree.
 //!
 //! ## Garbage Collection
 //! ---------------------
@@ -53,11 +62,6 @@
 //! so the client only needs to maintain a simple list of nodes. By default, Dioxus will not manually clean up old nodes
 //! for the client. As new nodes are created, old nodes will be over-written.
 //!
-//! HEADS-UP:
-//!     For now, deferred garabge collection is disabled. The code-paths are almost wired up, but it's quite complex to
-//!     get working safely and efficiently. For now, garabge is collected immediately during diffing. This adds extra
-//!     overhead, but is faster to implement in the short term.
-//!
 //! Further Reading and Thoughts
 //! ----------------------------
 //! There are more ways of increasing diff performance here that are currently not implemented.
@@ -67,13 +71,16 @@
 use crate::{arena::SharedResources, innerlude::*};
 use futures_util::Future;
 use fxhash::{FxBuildHasher, FxHashMap, FxHashSet};
+use indexmap::IndexSet;
 use smallvec::{smallvec, SmallVec};
 
-use std::{any::Any, cell::Cell, cmp::Ordering, marker::PhantomData, pin::Pin};
+use std::{
+    any::Any, cell::Cell, cmp::Ordering, collections::HashSet, marker::PhantomData, pin::Pin,
+};
 use DomEdit::*;
 
-pub struct DiffMachine<'r, 'bump> {
-    pub vdom: &'bump SharedResources,
+pub struct DiffMachine<'bump> {
+    vdom: &'bump SharedResources,
 
     pub mutations: Mutations<'bump>,
 
@@ -81,14 +88,10 @@ pub struct DiffMachine<'r, 'bump> {
 
     pub diffed: FxHashSet<ScopeId>,
 
-    // will be used later for garbage collection
-    // we check every seen node and then schedule its eventual deletion
     pub seen_scopes: FxHashSet<ScopeId>,
-
-    _r: PhantomData<&'r ()>,
 }
 
-impl<'r, 'bump> DiffMachine<'r, 'bump> {
+impl<'bump> DiffMachine<'bump> {
     pub(crate) fn new(
         edits: Mutations<'bump>,
         cur_scope: ScopeId,
@@ -100,7 +103,6 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
             vdom: shared,
             diffed: FxHashSet::default(),
             seen_scopes: FxHashSet::default(),
-            _r: PhantomData,
         }
     }
 
@@ -115,12 +117,17 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
             vdom: shared,
             diffed: FxHashSet::default(),
             seen_scopes: FxHashSet::default(),
-            _r: PhantomData,
         }
     }
 
+    // make incremental progress on the current task
+    pub fn work(&mut self, is_ready: impl FnMut() -> bool) -> Result<FiberResult> {
+        todo!()
+        // Ok(FiberResult::D)
+    }
+
     //
-    pub fn diff_scope(&mut self, id: ScopeId) -> Result<()> {
+    pub async fn diff_scope(&mut self, id: ScopeId) -> Result<()> {
         let component = self.get_scope_mut(&id).ok_or_else(|| Error::NotMounted)?;
         let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
         self.diff_node(old, new);
@@ -133,7 +140,11 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
     // the real stack should be what it is coming in and out of this function (ideally empty)
     //
     // each function call assumes the stack is fresh (empty).
-    pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) {
+    pub async fn diff_node(
+        &mut self,
+        old_node: &'bump VNode<'bump>,
+        new_node: &'bump VNode<'bump>,
+    ) {
         match (&old_node.kind, &new_node.kind) {
             // Handle the "sane" cases first.
             // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment".
@@ -264,7 +275,8 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
                         false => {
                             // the props are different...
                             scope.run_scope().unwrap();
-                            self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
+                            self.diff_node(scope.frames.wip_head(), scope.frames.fin_head())
+                                .await;
                         }
                     }
 
@@ -296,7 +308,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
                 // This is the case where options or direct vnodes might be used.
                 // In this case, it's faster to just skip ahead to their diff
                 if old.children.len() == 1 && new.children.len() == 1 {
-                    self.diff_node(&old.children[0], &new.children[0]);
+                    self.diff_node(&old.children[0], &new.children[0]).await;
                     return;
                 }
 
@@ -1059,7 +1071,11 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
     //     [... parent]
     //
     // the change list stack is in the same state when this function returns.
-    fn diff_non_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) {
+    async fn diff_non_keyed_children(
+        &mut self,
+        old: &'bump [VNode<'bump>],
+        new: &'bump [VNode<'bump>],
+    ) {
         // Handled these cases in `diff_children` before calling this function.
         //
         debug_assert!(!new.is_empty());
@@ -1099,15 +1115,15 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
                 self.edit_pop();
 
                 // diff the rest
-                new.iter()
-                    .zip(old.iter())
-                    .for_each(|(new_child, old_child)| self.diff_node(old_child, new_child));
+                for (new_child, old_child) in new.iter().zip(old.iter()) {
+                    self.diff_node(old_child, new_child).await
+                }
             }
 
             // old.len == new.len -> no nodes added/removed, but perhaps changed
             Ordering::Equal => {
                 for (new_child, old_child) in new.iter().zip(old.iter()) {
-                    self.diff_node(old_child, new_child);
+                    self.diff_node(old_child, new_child).await;
                 }
             }
         }

+ 2 - 0
packages/core/src/lib.rs

@@ -44,6 +44,7 @@ pub(crate) mod innerlude {
     pub use crate::scope::*;
     pub use crate::util::*;
     pub use crate::virtual_dom::*;
+    pub use crate::yield_now::*;
 
     pub type DomTree<'a> = Option<VNode<'a>>;
     pub type FC<P> = fn(Context<P>) -> DomTree;
@@ -73,3 +74,4 @@ pub mod scope;
 pub mod signals;
 pub mod util;
 pub mod virtual_dom;
+pub mod yield_now;

+ 74 - 44
packages/core/src/scheduler.rs

@@ -1,3 +1,26 @@
+//! Provides resumable task scheduling for Dioxus.
+//!
+//!
+//! ## Design
+//!
+//! The recent React fiber architecture rewrite enabled pauseable and resumable diffing through the development of
+//! something called a "Fiber." Fibers were created to provide a way of "saving a stack frame", making it possible to
+//! resume said stack frame at a later time, or to drop it altogether. This made it possible to
+//!
+//!
+//!
+//!
+//!
+//!
+//!
+//!
+//!
+//!
+//!
+//!
+//!
+//!
+
 use std::any::Any;
 
 use std::any::TypeId;
@@ -11,6 +34,7 @@ use futures_util::Future;
 use futures_util::FutureExt;
 use futures_util::StreamExt;
 use indexmap::IndexSet;
+use smallvec::SmallVec;
 
 use crate::innerlude::*;
 
@@ -74,13 +98,15 @@ pub struct Scheduler {
 
     shared: SharedResources,
 
-    high_priorty: PriorityFiber<'static>,
-    medium_priority: PriorityFiber<'static>,
-    low_priority: PriorityFiber<'static>,
+    waypoints: VecDeque<Waypoint>,
+
+    high_priorty: PriortySystem,
+    medium_priority: PriortySystem,
+    low_priority: PriortySystem,
 }
 
 pub enum FiberResult<'a> {
-    Done(Mutations<'a>),
+    Done(&'a mut Mutations<'a>),
     Interrupted,
 }
 
@@ -97,10 +123,11 @@ impl Scheduler {
             garbage_scopes: HashSet::new(),
 
             current_priority: EventPriority::Low,
+            waypoints: VecDeque::new(),
 
-            high_priorty: PriorityFiber::new(),
-            medium_priority: PriorityFiber::new(),
-            low_priority: PriorityFiber::new(),
+            high_priorty: PriortySystem::new(),
+            medium_priority: PriortySystem::new(),
+            low_priority: PriortySystem::new(),
         }
     }
 
@@ -223,21 +250,26 @@ impl Scheduler {
     }
 
     pub fn has_work(&self) -> bool {
-        let has_work = self.high_priorty.has_work()
-            || self.medium_priority.has_work()
-            || self.low_priority.has_work();
-        !has_work
+        self.waypoints.len() > 0
     }
 
     pub fn has_pending_garbage(&self) -> bool {
         !self.garbage_scopes.is_empty()
     }
 
+    fn get_current_fiber<'a>(&'a mut self) -> &mut DiffMachine<'a> {
+        let fib = match self.current_priority {
+            EventPriority::High => &mut self.high_priorty,
+            EventPriority::Medium => &mut self.medium_priority,
+            EventPriority::Low => &mut self.low_priority,
+        };
+        unsafe { std::mem::transmute(fib) }
+    }
+
     /// If a the fiber finishes its works (IE needs to be committed) the scheduler will drop the dirty scope
     pub fn work_with_deadline(
         &mut self,
         mut deadline: &mut Pin<Box<impl FusedFuture<Output = ()>>>,
-        is_deadline_reached: &mut impl FnMut() -> bool,
     ) -> FiberResult {
         // check if we need to elevate priority
         self.current_priority = match (
@@ -250,13 +282,10 @@ impl Scheduler {
             (false, false, _) => EventPriority::Low,
         };
 
-        let mut current_fiber = match self.current_priority {
-            EventPriority::High => &mut self.high_priorty,
-            EventPriority::Medium => &mut self.medium_priority,
-            EventPriority::Low => &mut self.low_priority,
-        };
+        let mut is_ready = || -> bool { (&mut deadline).now_or_never().is_some() };
 
-        todo!()
+        // TODO: remove this unwrap - proprogate errors out
+        self.get_current_fiber().work(is_ready).unwrap()
     }
 
     // waits for a trigger, canceling early if the deadline is reached
@@ -374,45 +403,46 @@ pub struct DirtyScope {
     start_tick: u32,
 }
 
-// fibers in dioxus aren't exactly the same as React's. Our fibers are more like a "saved state" of the diffing algorithm.
-pub struct PriorityFiber<'a> {
-    // scopes that haven't been updated yet
-    pending_scopes: Vec<ScopeId>,
+/*
+A "waypoint" represents a frozen unit in time for the DiffingMachine to resume from. Whenever the deadline runs out
+while diffing, the diffing algorithm generates a Waypoint in order to easily resume from where it left off. Waypoints are
+fairly expensive to create, especially for big trees, so it's a good idea to pre-allocate them.
 
-    pending_nodes: Vec<*const VNode<'a>>,
+Waypoints are created pessimisticly, and are only generated when an "Error" state is bubbled out of the diffing machine.
+This saves us from wasting cycles book-keeping waypoints for 99% of edits where the deadline is not reached.
+*/
+pub struct Waypoint {
+    // the progenitor of this waypoint
+    root: ScopeId,
 
-    // WIP edits
-    edits: Vec<DomEdit<'a>>,
+    edits: Vec<DomEdit<'static>>,
 
-    started: bool,
+    // a saved position in the tree
+    // these indicies continue to map through the tree into children nodes.
+    // A sequence of usizes is all that is needed to represent the path to a node.
+    tree_position: SmallVec<[usize; 10]>,
 
-    // a fiber is finished when no more scopes or nodes are pending
-    completed: bool,
+    seen_scopes: HashSet<ScopeId>,
 
-    dirty_scopes: IndexSet<ScopeId>,
+    invalidate_scopes: HashSet<ScopeId>,
 
-    wip_edits: Vec<DomEdit<'a>>,
+    priority_level: EventPriority,
+}
 
-    current_batch_scopes: HashSet<ScopeId>,
+pub struct PriortySystem {
+    pub pending_scopes: Vec<ScopeId>,
+    pub dirty_scopes: IndexSet<ScopeId>,
 }
 
-impl PriorityFiber<'_> {
-    fn new() -> Self {
+impl PriortySystem {
+    pub fn new() -> Self {
         Self {
-            pending_scopes: Vec::new(),
-            pending_nodes: Vec::new(),
-            edits: Vec::new(),
-            started: false,
-            completed: false,
-            dirty_scopes: IndexSet::new(),
-            wip_edits: Vec::new(),
-            current_batch_scopes: HashSet::new(),
+            pending_scopes: Default::default(),
+            dirty_scopes: Default::default(),
         }
     }
 
     fn has_work(&self) -> bool {
-        self.dirty_scopes.is_empty()
-            && self.wip_edits.is_empty()
-            && self.current_batch_scopes.is_empty()
+        self.pending_scopes.len() > 0 || self.dirty_scopes.len() > 0
     }
 }

+ 7 - 25
packages/core/src/virtual_dom.rs

@@ -178,11 +178,12 @@ impl VirtualDom {
     ///
     /// This method will not wait for any suspended nodes to complete.
     pub fn run_immediate<'s>(&'s mut self) -> Result<Mutations<'s>> {
-        use futures_util::FutureExt;
-        let mut is_ready = || false;
-        self.run_with_deadline_and_is_ready(futures_util::future::ready(()), &mut is_ready)
-            .now_or_never()
-            .expect("this future will always resolve immediately")
+        todo!()
+        // use futures_util::FutureExt;
+        // let mut is_ready = || false;
+        // self.run_with_deadline(futures_util::future::ready(()), &mut is_ready)
+        //     .now_or_never()
+        //     .expect("this future will always resolve immediately")
     }
 
     /// Runs the virtualdom with no time limit.
@@ -240,25 +241,6 @@ impl VirtualDom {
     pub async fn run_with_deadline<'s>(
         &'s mut self,
         deadline: impl Future<Output = ()>,
-    ) -> Result<Mutations<'s>> {
-        use futures_util::FutureExt;
-
-        let deadline_future = deadline.shared();
-        let mut is_ready_deadline = deadline_future.clone();
-        let mut is_ready = || -> bool { (&mut is_ready_deadline).now_or_never().is_some() };
-
-        self.run_with_deadline_and_is_ready(deadline_future, &mut is_ready)
-            .await
-    }
-
-    /// Runs the virtualdom with a deadline and a custom "check" function.
-    ///
-    /// Designed this way so "run_immediate" can re-use all the same rendering logic as "run_with_deadline" but the work
-    /// queue is completely drained;
-    async fn run_with_deadline_and_is_ready<'s>(
-        &'s mut self,
-        deadline: impl Future<Output = ()>,
-        is_ready: &mut impl FnMut() -> bool,
     ) -> Result<Mutations<'s>> {
         let mut committed_mutations = Mutations::new();
         let mut deadline = Box::pin(deadline.fuse());
@@ -286,7 +268,7 @@ impl VirtualDom {
 
             // Work through the current subtree, and commit the results when it finishes
             // When the deadline expires, give back the work
-            match self.scheduler.work_with_deadline(&mut deadline, is_ready) {
+            match self.scheduler.work_with_deadline(&mut deadline) {
                 FiberResult::Done(mut mutations) => {
                     committed_mutations.extend(&mut mutations);
 

+ 52 - 0
packages/core/src/yield_now.rs

@@ -0,0 +1,52 @@
+use std::future::Future;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+// use crate::task::{Context, Poll};
+
+/// Cooperatively gives up a timeslice to the task scheduler.
+///
+/// Calling this function will move the currently executing future to the back
+/// of the execution queue, making room for other futures to execute. This is
+/// especially useful after running CPU-intensive operations inside a future.
+///
+/// See also [`task::spawn_blocking`].
+///
+/// [`task::spawn_blocking`]: fn.spawn_blocking.html
+///
+/// # Examples
+///
+/// Basic usage:
+///
+/// ```
+/// # async_std::task::block_on(async {
+/// #
+/// use async_std::task;
+///
+/// task::yield_now().await;
+/// #
+/// # })
+/// ```
+#[inline]
+pub async fn yield_now() {
+    YieldNow(false).await
+}
+
+struct YieldNow(bool);
+
+impl Future for YieldNow {
+    type Output = ();
+
+    // The futures executor is implemented as a FIFO queue, so all this future
+    // does is re-schedule the future back to the end of the queue, giving room
+    // for other futures to progress.
+    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        if !self.0 {
+            self.0 = true;
+            cx.waker().wake_by_ref();
+            Poll::Pending
+        } else {
+            Poll::Ready(())
+        }
+    }
+}

+ 3 - 3
packages/web/src/lib.rs

@@ -108,9 +108,9 @@ pub async fn run_with_props<T: Properties + 'static>(
     let tasks = dom.get_event_sender();
 
     // initialize the virtualdom first
-    if cfg.hydrate {
-        dom.rebuild_in_place()?;
-    }
+    // if cfg.hydrate {
+    //     dom.rebuild_in_place()?;
+    // }
 
     let mut websys_dom = dom::WebsysDom::new(
         root_el,