diff --git a/examples/ssr.rs b/examples/ssr.rs index c5bc8d744..9a74031d4 100644 --- a/examples/ssr.rs +++ b/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 +} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 12c16c2f4..1c10f2113 100644 --- a/packages/core/src/diff.rs +++ b/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, - // will be used later for garbage collection - // we check every seen node and then schedule its eventual deletion pub seen_scopes: FxHashSet, - - _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 { + 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; } } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index e46de477d..fa2de0c8c 100644 --- a/packages/core/src/lib.rs +++ b/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>; pub type FC

= fn(Context

) -> DomTree; @@ -73,3 +74,4 @@ pub mod scope; pub mod signals; pub mod util; pub mod virtual_dom; +pub mod yield_now; diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 364f76609..95a8b8918 100644 --- a/packages/core/src/scheduler.rs +++ b/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, + + 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>>, - 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, +/* +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>, + edits: Vec>, - 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, - dirty_scopes: IndexSet, + invalidate_scopes: HashSet, - wip_edits: Vec>, - - current_batch_scopes: HashSet, + priority_level: EventPriority, } -impl PriorityFiber<'_> { - fn new() -> Self { +pub struct PriortySystem { + pub pending_scopes: Vec, + pub dirty_scopes: IndexSet, +} + +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 } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 0dafc2ea2..78b4cbde3 100644 --- a/packages/core/src/virtual_dom.rs +++ b/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> { - 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, - ) -> Result> { - 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, - is_ready: &mut impl FnMut() -> bool, ) -> Result> { 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); diff --git a/packages/core/src/yield_now.rs b/packages/core/src/yield_now.rs new file mode 100644 index 000000000..2a985483e --- /dev/null +++ b/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 { + if !self.0 { + self.0 = true; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(()) + } + } +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 8527ea82d..9ff8906e9 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -108,9 +108,9 @@ pub async fn run_with_props( 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,