diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 1c193d7e4..f350da8b9 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -35,6 +35,19 @@ //! 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. //! +//! ### Suspense +//! ------------ +//! Dioxus implements suspense slightly differently than React. In React, each fiber is manually progressed until it runs +//! into a promise-like value. React will then work on the next "ready" fiber, checking back on the previous fiber once +//! it has finished its new work. In Dioxus, we use a similar approach, but try to completely render the tree before +//! switching sub-fibers. Instead, each future is submitted into a futures-queue and the node is manually loaded later on. +//! +//! We're able to use this approach because we use placeholder nodes - futures that aren't ready still get submitted to +//! DOM, but as a placeholder. +//! +//! Right now, the "suspense" queue is intertwined the hooks. In the future, we should allow any future to drive attributes +//! and contents, without the need for the "use_suspense" hook. For now, this is the quickest way to get suspense working. +//! //! ## Subtree Memoization //! ----------------------- //! We also employ "subtree memoization" which saves us from having to check trees which take no dynamic content. We can @@ -121,6 +134,12 @@ pub enum DiffInstruction<'a> { }, DiffChildren { + old: &'a [VNode<'a>], + new: &'a [VNode<'a>], + }, + + // diff two lists of equally sized children + DiffEqual { progress: usize, old: &'a [VNode<'a>], new: &'a [VNode<'a>], @@ -128,19 +147,29 @@ pub enum DiffInstruction<'a> { Create { node: &'a VNode<'a>, + and: MountType<'a>, }, + CreateChildren { progress: usize, children: &'a [VNode<'a>], + and: MountType<'a>, }, - // todo: merge this into the create instruction? - Append, - InsertAfter, - InsertBefore, - Replace { - with: usize, + Mount { + and: MountType<'a>, }, + + PopScope, +} + +#[derive(Debug, Clone, Copy)] +pub enum MountType<'a> { + Absorb, + Append, + Replace { old: &'a VNode<'a> }, + InsertAfter { other_node: &'a VNode<'a> }, + InsertBefore { other_node: &'a VNode<'a> }, } impl<'bump> DiffMachine<'bump> { @@ -178,63 +207,109 @@ impl<'bump> DiffMachine<'bump> { /// /// This method implements a depth-first iterative tree traversal. /// - /// We do depth-first to maintain high cache locality (nodes were originally generated recursively) and because we - /// only need a stack (not a queue) of lists + /// We do depth-first to maintain high cache locality (nodes were originally generated recursively). pub async fn work(&mut self) -> Result<()> { // todo: don't move the reused instructions around // defer to individual functions so the compiler produces better code // large functions tend to be difficult for the compiler to work with while let Some(instruction) = self.instructions.last_mut() { log::debug!("Handling diff instruction: {:?}", instruction); + + // todo: call this less frequently, there is a bit of overhead involved + yield_now().await; + match instruction { + DiffInstruction::PopScope => { + self.instructions.pop(); + self.scope_stack.pop(); + } + DiffInstruction::DiffNode { old, new, .. } => { let (old, new) = (*old, *new); self.instructions.pop(); + self.diff_node(old, new); } - // this is slightly more complicated, we need to find a way to pause our LIS code - DiffInstruction::DiffChildren { progress, old, new } => { - todo!() - } + DiffInstruction::DiffEqual { progress, old, new } => { + debug_assert_eq!(old.len(), new.len()); - DiffInstruction::Create { node, .. } => { - let node = *node; - self.instructions.pop(); - self.create_node(node); - } - - DiffInstruction::CreateChildren { progress, children } => { - if let Some(child) = children.get(*progress) { + if let (Some(old_child), Some(new_child)) = + (old.get(*progress), new.get(*progress)) + { *progress += 1; - self.create_node(child); + self.diff_node(old_child, new_child); } else { self.instructions.pop(); } } - DiffInstruction::Append => { - let many = self.nodes_created_stack.pop().unwrap(); - self.edit_append_children(many as u32); + // this is slightly more complicated, we need to find a way to pause our LIS code + DiffInstruction::DiffChildren { old, new } => { + let (old, new) = (*old, *new); self.instructions.pop(); + + self.diff_children(old, new); } - DiffInstruction::Replace { with } => { - let with = *with; - let many = self.nodes_created_stack.pop().unwrap(); - self.edit_replace_with(with as u32, many as u32); + DiffInstruction::Create { node, and } => { + let (node, and) = (*node, *and); self.instructions.pop(); + + self.nodes_created_stack.push(0); + self.instructions.push(DiffInstruction::Mount { and }); + + self.create_node(node); } - DiffInstruction::InsertAfter => { - let n = self.nodes_created_stack.pop().unwrap(); - self.edit_insert_after(n as u32); - self.instructions.pop(); + DiffInstruction::CreateChildren { + progress, + children, + and, + } => { + let and = *and; + + if *progress == 0 { + self.nodes_created_stack.push(0); + } + + if let Some(child) = children.get(*progress) { + *progress += 1; + + if *progress == children.len() { + self.instructions.pop(); + self.instructions.push(DiffInstruction::Mount { and }); + } + + self.create_node(child); + } else if children.len() == 0 { + self.instructions.pop(); + } } - DiffInstruction::InsertBefore => { - let n = self.nodes_created_stack.pop().unwrap(); - self.edit_insert_before(n as u32); + DiffInstruction::Mount { and } => { + let nodes_created = self.nodes_created_stack.pop().unwrap(); + match and { + // add the nodes from this virtual list to the parent + // used by fragments and components + MountType::Absorb => { + *self.nodes_created_stack.last_mut().unwrap() += nodes_created; + } + MountType::Append => { + self.edit_append_children(nodes_created as u32); + } + MountType::Replace { old } => { + todo!() + // self.edit_replace_with(with as u32, many as u32); + } + MountType::InsertAfter { other_node } => { + self.edit_insert_after(nodes_created as u32); + } + MountType::InsertBefore { other_node } => { + self.edit_insert_before(nodes_created as u32); + } + } + self.instructions.pop(); } }; @@ -292,7 +367,9 @@ impl<'bump> DiffMachine<'bump> { let real_id = self.vdom.reserve_node(); self.edit_create_element(tag_name, *namespace, real_id); + *self.nodes_created_stack.last_mut().unwrap() += 1; + dom_id.set(Some(real_id)); let cur_scope = self.current_scope().unwrap(); @@ -307,19 +384,20 @@ impl<'bump> DiffMachine<'bump> { self.edit_set_attribute(attr); } - self.instructions.push(DiffInstruction::Append); - - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::CreateChildren { - children, - progress: 0, - }); + if children.len() > 0 { + self.instructions.push(DiffInstruction::CreateChildren { + children, + progress: 0, + and: MountType::Append, + }); + } } fn create_fragment_node(&mut self, frag: &'bump VFragment<'bump>) { self.instructions.push(DiffInstruction::CreateChildren { children: frag.children, progress: 0, + and: MountType::Absorb, }); } @@ -373,23 +451,18 @@ impl<'bump> DiffMachine<'bump> { // Take the node that was just generated from running the component let nextnode = new_component.frames.fin_head(); - // // Push the new scope onto the stack - // self.scope_stack.push(new_idx); + // Push the new scope onto the stack + self.scope_stack.push(new_idx); + self.instructions.push(DiffInstruction::PopScope); - // // Run the creation algorithm with this scope on the stack - self.instructions - .push(DiffInstruction::Create { node: nextnode }); + // Run the creation algorithm with this scope on the stack + // ?? I think we treat components as framgnets?? + self.instructions.push(DiffInstruction::Create { + node: nextnode, + and: MountType::Absorb, + }); - // let meta = self.create_vnode(nextnode); - - // // pop the scope off the stack - // self.scope_stack.pop(); - - // if meta.added_to_stack == 0 { - // panic!("Components should *always* generate nodes - even if they fail"); - // } - - // // Finally, insert this scope as a seen node. + // Finally, insert this scope as a seen node. self.seen_scopes.insert(new_idx); } @@ -641,20 +714,24 @@ impl<'bump> DiffMachine<'bump> { // If old no anchors are provided, then it's assumed that we can freely append to the parent. // // Remember, non-empty lists does not mean that there are real elements, just that there are virtual elements. + // + // Frament nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only + // to an element, and appending makes sense. fn diff_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { const IS_EMPTY: bool = true; const IS_NOT_EMPTY: bool = false; + // Remember, fragments can never be empty (they always have a single child) match (old.is_empty(), new.is_empty()) { (IS_EMPTY, IS_EMPTY) => {} // Completely adding new nodes, removing any placeholder if it exists (IS_EMPTY, IS_NOT_EMPTY) => { - todo!(); - // let meta = todo!(); - // let meta = self.create_children(new); - // let meta = self.create_children(new); - // self.edit_append_children(meta.added_to_stack); + self.instructions.push(DiffInstruction::CreateChildren { + children: new, + progress: 0, + and: MountType::Append, + }); } // Completely removing old nodes and putting an anchor in its place @@ -678,14 +755,11 @@ impl<'bump> DiffMachine<'bump> { // Replace the anchor with whatever new nodes are coming down the pipe (VNode::Anchor(anchor), _) => { - self.edit_push_root(anchor.dom_id.get().unwrap()); - let mut added = 0; - for el in new { - todo!(); - // let meta = self.create_vnode(el); - // added += meta.added_to_stack; - } - self.edit_replace_with(1, added); + self.instructions.push(DiffInstruction::CreateChildren { + children: new, + progress: 0, + and: MountType::Replace { old: first_old }, + }); } // Replace whatever nodes are sitting there with the anchor @@ -888,53 +962,60 @@ impl<'bump> DiffMachine<'bump> { shared_suffix_count: usize, old_shared_suffix_start: usize, ) { + /* + 1. Map the old keys into a numerical ordering based on indicies. + 2. Create a map of old key to its index + 3. Map each new key to the old key, carrying over the old index. + - IE if we have ABCD becomes BACD, our sequence would be 1,0,2,3 + - if we have ABCD to ABDE, our sequence would be 0,1,3,MAX because E doesn't exist + + now, we should have a list of integers that indicates where in the old list the new items map to. + + 4. Compute the LIS of this list + - this indicates the longest list of new children that won't need to be moved. + + 5. Identify which nodes need to be removed + 6. Identify which nodes will need to be diffed + + 7. Going along each item in the new list, create it and insert it before the next closest item in the LIS. + - if the item already existed, just move it to the right place. + + 8. Finally, generate instructions to remove any old children. + 9. Generate instructions to finally diff children that are the same between both + */ + + // 0. Debug sanity checks // Should have already diffed the shared-key prefixes and suffixes. debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); - // // The algorithm below relies upon using `u32::MAX` as a sentinel - // // value, so if we have that many new nodes, it won't work. This - // // check is a bit academic (hence only enabled in debug), since - // // wasm32 doesn't have enough address space to hold that many nodes - // // in memory. - // debug_assert!(new.len() < u32::MAX as usize); - - // Map from each `old` node's key to its index within `old`. + // 1. Map the old keys into a numerical ordering based on indicies. + // 2. Create a map of old key to its index // IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3). - let mut old_key_to_old_index = old + let old_key_to_old_index = old .iter() .enumerate() .map(|(i, o)| (o.key().unwrap(), i)) .collect::>(); - // The set of shared keys between `new` and `old`. let mut shared_keys = FxHashSet::default(); - // let mut to_remove = FxHashSet::default(); let mut to_add = FxHashSet::default(); - // Map from each index in `new` to the index of the node in `old` that - // has the same key. - let mut new_index_to_old_index = new + // 3. Map each new key to the old key, carrying over the old index. + let new_index_to_old_index = new .iter() .map(|n| { let key = n.key().unwrap(); - match old_key_to_old_index.get(&key) { - Some(&index) => { - shared_keys.insert(key); - index - } - None => { - // - to_add.insert(key); - u32::MAX as usize - } + if let Some(&index) = old_key_to_old_index.get(&key) { + shared_keys.insert(key); + index + } else { + to_add.insert(key); + u32::MAX as usize } }) .collect::>(); - dbg!(&shared_keys); - dbg!(&to_add); - // If none of the old keys are reused by the new children, then we // remove all the remaining old children and create the new children // afresh. @@ -943,23 +1024,7 @@ impl<'bump> DiffMachine<'bump> { return; } - // // Remove any old children whose keys were not reused in the new - // // children. Remove from the end first so that we don't mess up indices. - // for old_child in old.iter().rev() { - // if !shared_keys.contains(&old_child.key()) { - // self.remove_child(old_child); - // } - // } - - // let old_keyds = old.iter().map(|f| f.key()).collect::>(); - // let new_keyds = new.iter().map(|f| f.key()).collect::>(); - // dbg!(old_keyds); - // dbg!(new_keyds); - - // // If there aren't any more new children, then we are done! - // if new.is_empty() { - // return; - // } + // 4. Compute the LIS of this list // The longest increasing subsequence within `new_index_to_old_index`. This // is the longest sequence on DOM nodes in `old` that are relatively ordered @@ -981,16 +1046,11 @@ impl<'bump> DiffMachine<'bump> { &mut starts, ); - dbg!(&new_index_is_in_lis); // use the old nodes to navigate the new nodes - let mut lis_in_order = new_index_is_in_lis.into_iter().collect::>(); lis_in_order.sort_unstable(); - dbg!(&lis_in_order); - // we walk front to back, creating the head node - // diff the shared, in-place nodes first // this makes sure we can rely on their first/last nodes being correct later on for id in &lis_in_order { @@ -1016,9 +1076,9 @@ impl<'bump> DiffMachine<'bump> { let new_anchor = &new[*lis_id]; root = Some(new_anchor); - let anchor_el = self.find_first_element(new_anchor); - self.edit_push_root(anchor_el.direct_id()); - // let mut pushed = false; + // let anchor_el = self.find_first_element(new_anchor); + // self.edit_push_root(anchor_el.direct_id()); + // // let mut pushed = false; 'inner: loop { let (next_id, next_new) = new_iter.next().unwrap(); @@ -1044,12 +1104,12 @@ impl<'bump> DiffMachine<'bump> { self.edit_insert_before(1); } } else { - eprintln!("key is not contained {:?}", key); - // new node needs to be created - // insert it before the current milestone - todo!(); - // let meta = self.create_vnode(next_new); - // self.edit_insert_before(meta.added_to_stack); + self.instructions.push(DiffInstruction::Create { + node: next_new, + and: MountType::InsertBefore { + other_node: new_anchor, + }, + }); } } } @@ -1129,11 +1189,7 @@ impl<'bump> DiffMachine<'bump> { // [... parent] // // the change list stack is in the same state when this function returns. - async fn diff_non_keyed_children( - &mut self, - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], - ) { + 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()); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 23b43c210..ca355f986 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -163,9 +163,9 @@ impl VirtualDom { // // We run the component. If it succeeds, then we can diff it and add the changes to the dom. if cur_component.run_scope().is_ok() { - diff_machine.nodes_created_stack.push(0); diff_machine.instructions.push(DiffInstruction::Create { node: cur_component.frames.fin_head(), + and: MountType::Append, }); } else { // todo: should this be a hard error? @@ -178,6 +178,7 @@ impl VirtualDom { Ok(diff_machine.mutations) } + /// Rebuild the dom pub async fn rebuild_async<'s>(&'s mut self) -> Result> { let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); @@ -189,13 +190,11 @@ impl VirtualDom { // // We run the component. If it succeeds, then we can diff it and add the changes to the dom. if cur_component.run_scope().is_ok() { - diff_machine.nodes_created_stack.push(0); diff_machine.instructions.push(DiffInstruction::Create { node: cur_component.frames.fin_head(), + and: MountType::Append, }); diff_machine.work().await.unwrap(); - let many = diff_machine.nodes_created_stack.pop().unwrap() as u32; - diff_machine.edit_append_children(many); } else { // todo: should this be a hard error? log::warn!( diff --git a/packages/core/tests/iterative.rs b/packages/core/tests/iterative.rs index 27fe91e92..573bd4459 100644 --- a/packages/core/tests/iterative.rs +++ b/packages/core/tests/iterative.rs @@ -60,7 +60,7 @@ async fn test_iterative_create() { async fn test_iterative_create_list() { static App: FC<()> = |cx| { cx.render(rsx! { - {(0..10).map(|f| rsx!{ div { + {(0..3).map(|f| rsx!{ div { "hello" }})} }) @@ -73,12 +73,47 @@ async fn test_iterative_create_list() { dbg!(mutations); } -// #[async_std::test] -// async fn websys_loop() { -// ///loop { -// /// let deadline = request_idle_callback().await; -// /// let edits = dom.work(deadline); -// /// request_animation_frame().await; -// /// apply(edits); -// ///} -// } +#[async_std::test] +async fn test_iterative_create_simple() { + static App: FC<()> = |cx| { + cx.render(rsx! { + div {} + div {} + div {} + div {} + }) + }; + + test_logging::set_up_logging(); + + let mut dom = VirtualDom::new(App); + let mutations = dom.rebuild_async().await.unwrap(); + dbg!(mutations); +} + +#[async_std::test] +async fn test_iterative_create_components() { + static App: FC<()> = |cx| { + cx.render(rsx! { + Child { "abc1" } + Child { "abc2" } + Child { "abc3" } + }) + }; + + static Child: FC<()> = |cx| { + cx.render(rsx! { + h1 {} + div { + {cx.children()} + } + p {} + }) + }; + + test_logging::set_up_logging(); + + let mut dom = VirtualDom::new(App); + let mutations = dom.rebuild_async().await.unwrap(); + dbg!(mutations); +} diff --git a/packages/core/tests/test_logging.rs b/packages/core/tests/test_logging.rs index a8cf6926b..8783dd6d2 100644 --- a/packages/core/tests/test_logging.rs +++ b/packages/core/tests/test_logging.rs @@ -44,6 +44,4 @@ pub fn set_up_logging() { .chain(std::io::stdout()) .apply() .unwrap(); - - debug!("finished setting up logging! yay!"); }