From 3bb5c8142cf63489ffd0ff0c316cc168852d5c4f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 30 Jan 2022 17:47:58 -0500 Subject: [PATCH] fix: switch to future based diffing this commit removes the old manual fiber implementation in favor of a traditional recursion based approach. This should make the diffing algorithm easier to work on and eliminate various stack-based bugs in. --- packages/core/.rustfmt.toml | 1 + packages/core/src/diff.rs | 21 +- packages/core/src/diff_async.rs | 1102 ++++++++++++++++++++++++++++++ packages/core/src/lib.rs | 6 +- packages/core/src/mutations.rs | 8 + packages/core/src/nodes.rs | 59 +- packages/core/src/scopes.rs | 32 +- packages/core/src/virtual_dom.rs | 104 +-- packages/core/tests/diffing.rs | 257 ++----- 9 files changed, 1268 insertions(+), 322 deletions(-) create mode 100644 packages/core/.rustfmt.toml create mode 100644 packages/core/src/diff_async.rs diff --git a/packages/core/.rustfmt.toml b/packages/core/.rustfmt.toml new file mode 100644 index 000000000..70a9483a8 --- /dev/null +++ b/packages/core/.rustfmt.toml @@ -0,0 +1 @@ +struct_lit_width = 80 diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index c8e2b29a7..3b9c7692c 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -222,7 +222,7 @@ impl<'bump> DiffStack<'bump> { self.nodes_created_stack.pop().unwrap() } - fn current_scope(&self) -> Option { + pub fn current_scope(&self) -> Option { self.scope_stack.last().copied() } @@ -314,9 +314,10 @@ impl<'bump> DiffState<'bump> { } MountType::Append => { - self.mutations.edits.push(AppendChildren { - many: nodes_created as u32, - }); + self.mutations.append_children(nodes_created as u32); + // self.mutations.edits.push(AppendChildren { + // many: nodes_created as u32, + // }); } MountType::InsertAfter { other_node } => { @@ -470,7 +471,8 @@ impl<'bump> DiffState<'bump> { self.stack.create_component(new_idx, nextnode); // Finally, insert this scope as a seen node. - self.mutations.dirty_scopes.insert(new_idx); + self.mutations.mark_dirty_scope(new_idx); + // self.mutations.dirty_scopes.insert(new_idx); } // ================================= @@ -639,9 +641,10 @@ impl<'bump> DiffState<'bump> { } if old.children.is_empty() && !new.children.is_empty() { - self.mutations.edits.push(PushRoot { - root: root.as_u64(), - }); + self.mutations.push_root(root); + // self.mutations.edits.push(PushRoot { + // root: root.as_u64(), + // }); self.stack.element_stack.push(root); self.stack.instructions.push(DiffInstruction::PopElement); self.stack.create_children(new.children, MountType::Append); @@ -771,7 +774,7 @@ impl<'bump> DiffState<'bump> { // this should auto drop the previous props self.scopes.run_scope(scope_addr); - self.mutations.dirty_scopes.insert(scope_addr); + self.mutations.mark_dirty_scope(scope_addr); self.diff_node( self.scopes.wip_head(scope_addr), diff --git a/packages/core/src/diff_async.rs b/packages/core/src/diff_async.rs new file mode 100644 index 000000000..e5d63ffb5 --- /dev/null +++ b/packages/core/src/diff_async.rs @@ -0,0 +1,1102 @@ +use crate::innerlude::*; +use fxhash::{FxHashMap, FxHashSet}; +use smallvec::{smallvec, SmallVec}; + +pub(crate) struct AsyncDiffState<'bump> { + pub(crate) scopes: &'bump ScopeArena, + pub(crate) mutations: Mutations<'bump>, + pub(crate) force_diff: bool, + pub(crate) element_stack: SmallVec<[ElementId; 10]>, + pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, +} + +impl<'b> AsyncDiffState<'b> { + pub fn new(scopes: &'b ScopeArena) -> Self { + Self { + scopes, + mutations: Mutations::new(), + force_diff: false, + element_stack: smallvec![], + scope_stack: smallvec![], + } + } + + pub fn diff_scope(&mut self, scopeid: ScopeId) { + let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); + self.scope_stack.push(scopeid); + let scope = self.scopes.get_scope(scopeid).unwrap(); + self.element_stack.push(scope.container); + self.diff_node(old, new); + } + + pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) { + use VNode::*; + match (old_node, new_node) { + // Check the most common cases first + // these are *actual* elements, not wrappers around lists + (Text(old), Text(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - text are the sames"); + return; + } + + if let Some(root) = old.id.get() { + if old.text != new.text { + self.mutations.set_text(new.text, root.as_u64()); + } + self.scopes.update_node(new_node, root); + + new.id.set(Some(root)); + } + } + + (Placeholder(old), Placeholder(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - placeholder are the sames"); + return; + } + + if let Some(root) = old.id.get() { + self.scopes.update_node(new_node, root); + new.id.set(Some(root)) + } + } + + (Element(old), Element(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - element are the sames"); + return; + } + self.diff_element_nodes(old, new, old_node, new_node) + } + + // These two sets are pointers to nodes but are not actually nodes themselves + (Component(old), Component(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - placeholder are the sames"); + return; + } + self.diff_component_nodes(old_node, new_node, *old, *new) + } + + (Fragment(old), Fragment(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - fragment are the sames"); + return; + } + self.diff_fragment_nodes(old, new) + } + + // The normal pathway still works, but generates slightly weird instructions + // This pathway ensures uses the ReplaceAll, not the InsertAfter and remove + (Placeholder(_), Fragment(_)) => { + log::debug!("replacing placeholder with fragment {:?}", new_node); + self.replace_node(old_node, new_node); + } + + // Anything else is just a basic replace and create + ( + Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), + Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), + ) => self.replace_node(old_node, new_node), + } + } + + pub fn create_node(&mut self, node: &'b VNode<'b>) -> usize { + match node { + VNode::Text(vtext) => self.create_text_node(vtext, node), + VNode::Placeholder(anchor) => self.create_anchor_node(anchor, node), + VNode::Element(element) => self.create_element_node(element, node), + VNode::Fragment(frag) => self.create_fragment_node(frag), + VNode::Component(component) => self.create_component_node(*component), + } + } + + fn create_text_node(&mut self, vtext: &'b VText<'b>, node: &'b VNode<'b>) -> usize { + let real_id = self.scopes.reserve_node(node); + self.mutations.create_text_node(vtext.text, real_id); + vtext.id.set(Some(real_id)); + + 1 + } + + fn create_anchor_node(&mut self, anchor: &'b VPlaceholder, node: &'b VNode<'b>) -> usize { + let real_id = self.scopes.reserve_node(node); + self.mutations.create_placeholder(real_id); + anchor.id.set(Some(real_id)); + + 1 + } + + fn create_element_node(&mut self, element: &'b VElement<'b>, node: &'b VNode<'b>) -> usize { + let VElement { + tag: tag_name, + listeners, + attributes, + children, + namespace, + id: dom_id, + parent: parent_id, + .. + } = element; + + // set the parent ID for event bubbling + // self.stack.instructions.push(DiffInstruction::PopElement); + + let parent = self.element_stack.last().unwrap(); + parent_id.set(Some(*parent)); + + // set the id of the element + let real_id = self.scopes.reserve_node(node); + self.element_stack.push(real_id); + dom_id.set(Some(real_id)); + + self.mutations.create_element(tag_name, *namespace, real_id); + + if let Some(cur_scope_id) = self.current_scope() { + for listener in *listeners { + listener.mounted_node.set(Some(real_id)); + self.mutations.new_event_listener(listener, cur_scope_id); + } + } else { + log::warn!("create element called with no scope on the stack - this is an error for a live dom"); + } + + for attr in *attributes { + self.mutations.set_attribute(attr, real_id.as_u64()); + } + + if !children.is_empty() { + self.create_and_append_children(children); + } + + 1 + } + + fn create_fragment_node(&mut self, frag: &'b VFragment<'b>) -> usize { + self.create_children(frag.children) + } + + fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { + let parent_idx = self.current_scope().unwrap(); + + // the component might already exist - if it does, we need to reuse it + // this makes figure out when to drop the component more complicated + let new_idx = if let Some(idx) = vcomponent.scope.get() { + assert!(self.scopes.get_scope(idx).is_some()); + idx + } else { + // Insert a new scope into our component list + let props: Box = vcomponent.props.borrow_mut().take().unwrap(); + let props: Box = unsafe { std::mem::transmute(props) }; + let new_idx = self.scopes.new_with_key( + vcomponent.user_fc, + props, + Some(parent_idx), + self.element_stack.last().copied().unwrap(), + 0, + ); + + new_idx + }; + + log::info!( + "created component {:?} with parent {:?} and originator {:?}", + new_idx, + parent_idx, + vcomponent.originator + ); + + // Actually initialize the caller's slot with the right address + vcomponent.scope.set(Some(new_idx)); + + match vcomponent.can_memoize { + true => { + // todo: implement promotion logic. save us from boxing props that we don't need + } + false => { + // track this component internally so we know the right drop order + } + } + + // Run the scope for one iteration to initialize it + self.scopes.run_scope(new_idx); + + self.mutations.mark_dirty_scope(new_idx); + + // self.stack.create_component(new_idx, nextnode); + // Finally, insert this scope as a seen node. + + // Take the node that was just generated from running the component + let nextnode = self.scopes.fin_head(new_idx); + self.create_node(nextnode) + } + + fn diff_element_nodes( + &mut self, + old: &'b VElement<'b>, + new: &'b VElement<'b>, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + ) { + let root = old.id.get().unwrap(); + + // If the element type is completely different, the element needs to be re-rendered completely + // This is an optimization React makes due to how users structure their code + // + // This case is rather rare (typically only in non-keyed lists) + if new.tag != old.tag || new.namespace != old.namespace { + self.replace_node(old_node, new_node); + return; + } + + self.scopes.update_node(new_node, root); + + new.id.set(Some(root)); + new.parent.set(old.parent.get()); + + // todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the + // element to modify its attributes. + // it would result in fewer instructions if we just set the id directly. + // it would also clean up this code some, but that's not very important anyways + + // Diff Attributes + // + // It's extraordinarily rare to have the number/order of attributes change + // In these cases, we just completely erase the old set and make a new set + // + // TODO: take a more efficient path than this + if old.attributes.len() == new.attributes.len() { + for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { + if old_attr.value != new_attr.value || new_attr.is_volatile { + self.mutations.set_attribute(new_attr, root.as_u64()); + } + } + } else { + for attribute in old.attributes { + self.mutations.remove_attribute(attribute, root.as_u64()); + } + for attribute in new.attributes { + self.mutations.set_attribute(attribute, root.as_u64()) + } + } + + // Diff listeners + // + // It's extraordinarily rare to have the number/order of listeners change + // In the cases where the listeners change, we completely wipe the data attributes and add new ones + // + // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) + // + // TODO: take a more efficient path than this + if let Some(cur_scope_id) = self.current_scope() { + if old.listeners.len() == new.listeners.len() { + for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { + if old_l.event != new_l.event { + self.mutations + .remove_event_listener(old_l.event, root.as_u64()); + self.mutations.new_event_listener(new_l, cur_scope_id); + } + new_l.mounted_node.set(old_l.mounted_node.get()); + } + } else { + for listener in old.listeners { + self.mutations + .remove_event_listener(listener.event, root.as_u64()); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.mutations.new_event_listener(listener, cur_scope_id); + } + } + } + + match (old.children.len(), new.children.len()) { + (0, 0) => {} + (0, _) => { + let created = self.create_children(new.children); + self.mutations.append_children(created as u32); + } + (_, _) => { + self.diff_children(old.children, new.children); + } + }; + } + + fn diff_component_nodes( + &mut self, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + old: &'b VComponent<'b>, + new: &'b VComponent<'b>, + ) { + let scope_addr = old.scope.get().unwrap(); + log::trace!( + "diff_component_nodes. old: {:#?} new: {:#?}", + old_node, + new_node + ); + + if std::ptr::eq(old, new) { + log::trace!("skipping component diff - component is the sames"); + return; + } + + // Make sure we're dealing with the same component (by function pointer) + if old.user_fc == new.user_fc { + self.enter_scope(scope_addr); + + // Make sure the new component vnode is referencing the right scope id + new.scope.set(Some(scope_addr)); + + // make sure the component's caller function is up to date + let scope = self + .scopes + .get_scope(scope_addr) + .unwrap_or_else(|| panic!("could not find {:?}", scope_addr)); + + // take the new props out regardless + // when memoizing, push to the existing scope if memoization happens + let new_props = new.props.borrow_mut().take().unwrap(); + + let should_run = { + if old.can_memoize { + let props_are_the_same = unsafe { + scope + .props + .borrow() + .as_ref() + .unwrap() + .memoize(new_props.as_ref()) + }; + !props_are_the_same || self.force_diff + } else { + true + } + }; + + if should_run { + let _old_props = scope + .props + .replace(unsafe { std::mem::transmute(Some(new_props)) }); + + // this should auto drop the previous props + self.scopes.run_scope(scope_addr); + self.mutations.mark_dirty_scope(scope_addr); + + self.diff_node( + self.scopes.wip_head(scope_addr), + self.scopes.fin_head(scope_addr), + ); + } else { + log::trace!("memoized"); + // memoization has taken place + drop(new_props); + }; + + self.leave_scope(); + } else { + log::debug!("scope stack is {:#?}", self.scope_stack); + self.replace_node(old_node, new_node); + } + } + + fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { + // 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]); + return; + } + + debug_assert!(!old.children.is_empty()); + debug_assert!(!new.children.is_empty()); + + self.diff_children(old.children, new.children); + } + + // ============================================= + // Utilities for creating new diff instructions + // ============================================= + + // Diff the given set of old and new children. + // + // The parent must be on top of the change list stack when this function is + // entered: + // + // [... parent] + // + // the change list stack is in the same state when this function returns. + // + // 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. + // + // Fragment 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: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + // Remember, fragments can never be empty (they always have a single child) + match (old, new) { + ([], []) => {} + ([], _) => self.create_and_append_children(new), + (_, []) => self.remove_nodes(old, true), + _ => { + let new_is_keyed = new[0].key().is_some(); + let old_is_keyed = old[0].key().is_some(); + + debug_assert!( + new.iter().all(|n| n.key().is_some() == new_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + debug_assert!( + old.iter().all(|o| o.key().is_some() == old_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + + if new_is_keyed && old_is_keyed { + self.diff_keyed_children(old, new); + } else { + self.diff_non_keyed_children(old, new); + } + } + } + } + + // Diff children that are not keyed. + // + // The parent must be on the top of the change list stack when entering this + // function: + // + // [... parent] + // + // the change list stack is in the same state when this function returns. + fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + // Handled these cases in `diff_children` before calling this function. + debug_assert!(!new.is_empty()); + debug_assert!(!old.is_empty()); + + use std::cmp::Ordering; + match old.len().cmp(&new.len()) { + Ordering::Greater => self.remove_nodes(&old[new.len()..], true), + Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), + Ordering::Equal => {} + } + + for (new, old) in new.iter().zip(old.iter()) { + self.diff_node(old, new); + } + } + + // Diffing "keyed" children. + // + // With keyed children, we care about whether we delete, move, or create nodes + // versus mutate existing nodes in place. Presumably there is some sort of CSS + // transition animation that makes the virtual DOM diffing algorithm + // observable. By specifying keys for nodes, we know which virtual DOM nodes + // must reuse (or not reuse) the same physical DOM nodes. + // + // This is loosely based on Inferno's keyed patching implementation. However, we + // have to modify the algorithm since we are compiling the diff down into change + // list instructions that will be executed later, rather than applying the + // changes to the DOM directly as we compare virtual DOMs. + // + // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 + // + // The stack is empty upon entry. + fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + if cfg!(debug_assertions) { + let mut keys = fxhash::FxHashSet::default(); + let mut assert_unique_keys = |children: &'b [VNode<'b>]| { + keys.clear(); + for child in children { + let key = child.key(); + debug_assert!( + key.is_some(), + "if any sibling is keyed, all siblings must be keyed" + ); + keys.insert(key); + } + debug_assert_eq!( + children.len(), + keys.len(), + "keyed siblings must each have a unique key" + ); + }; + assert_unique_keys(old); + assert_unique_keys(new); + } + + // First up, we diff all the nodes with the same key at the beginning of the + // children. + // + // `shared_prefix_count` is the count of how many nodes at the start of + // `new` and `old` share the same keys. + let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { + Some(count) => count, + None => return, + }; + + // Ok, we now hopefully have a smaller range of children in the middle + // within which to re-order nodes with the same keys, remove old nodes with + // now-unused keys, and create new nodes with fresh keys. + + let old_middle = &old[left_offset..(old.len() - right_offset)]; + let new_middle = &new[left_offset..(new.len() - right_offset)]; + + debug_assert!( + !((old_middle.len() == new_middle.len()) && old_middle.is_empty()), + "keyed children must have the same number of children" + ); + + if new_middle.is_empty() { + // remove the old elements + self.remove_nodes(old_middle, true); + } else if old_middle.is_empty() { + // there were no old elements, so just create the new elements + // we need to find the right "foothold" though - we shouldn't use the "append" at all + if left_offset == 0 { + // insert at the beginning of the old list + let foothold = &old[old.len() - right_offset]; + self.create_and_insert_before(new_middle, foothold); + } else if right_offset == 0 { + // insert at the end the old list + let foothold = old.last().unwrap(); + self.create_and_insert_after(new_middle, foothold); + } else { + // inserting in the middle + let foothold = &old[left_offset - 1]; + self.create_and_insert_after(new_middle, foothold); + } + } else { + self.diff_keyed_middle(old_middle, new_middle); + } + } + + /// Diff both ends of the children that share keys. + /// + /// Returns a left offset and right offset of that indicates a smaller section to pass onto the middle diffing. + /// + /// If there is no offset, then this function returns None and the diffing is complete. + fn diff_keyed_ends( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + ) -> Option<(usize, usize)> { + let mut left_offset = 0; + + for (old, new) in old.iter().zip(new.iter()) { + // abort early if we finally run into nodes with different keys + if old.key() != new.key() { + break; + } + self.diff_node(old, new); + left_offset += 1; + } + + // If that was all of the old children, then create and append the remaining + // new children and we're finished. + if left_offset == old.len() { + self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); + return None; + } + + // And if that was all of the new children, then remove all of the remaining + // old children and we're finished. + if left_offset == new.len() { + self.remove_nodes(&old[left_offset..], true); + return None; + } + + // if the shared prefix is less than either length, then we need to walk backwards + let mut right_offset = 0; + for (old, new) in old.iter().rev().zip(new.iter().rev()) { + // abort early if we finally run into nodes with different keys + if old.key() != new.key() { + break; + } + self.diff_node(old, new); + right_offset += 1; + } + + Some((left_offset, right_offset)) + } + + // The most-general, expensive code path for keyed children diffing. + // + // We find the longest subsequence within `old` of children that are relatively + // ordered the same way in `new` (via finding a longest-increasing-subsequence + // of the old child's index within `new`). The children that are elements of + // this subsequence will remain in place, minimizing the number of DOM moves we + // will have to do. + // + // Upon entry to this function, the change list stack must be empty. + // + // This function will load the appropriate nodes onto the stack and do diffing in place. + // + // Upon exit from this function, it will be restored to that same self. + fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + /* + 1. Map the old keys into a numerical ordering based on indices. + 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())); + + // 1. Map the old keys into a numerical ordering based on indices. + // 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 old_key_to_old_index = old + .iter() + .enumerate() + .map(|(i, o)| (o.key().unwrap(), i)) + .collect::>(); + + let mut shared_keys = FxHashSet::default(); + + // 3. Map each new key to the old key, carrying over the old index. + let new_index_to_old_index = new + .iter() + .map(|node| { + let key = node.key().unwrap(); + if let Some(&index) = old_key_to_old_index.get(&key) { + shared_keys.insert(key); + index + } else { + u32::MAX as usize + } + }) + .collect::>(); + + // 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. + if shared_keys.is_empty() { + if let Some(first_old) = old.get(0) { + self.remove_nodes(&old[1..], true); + let nodes_created = self.create_children(new); + self.replace_inner(first_old, nodes_created); + } else { + // I think this is wrong - why are we appending? + // only valid of the if there are no trailing elements + self.create_and_append_children(new); + } + return; + } + + // 4. Compute the LIS of this list + let mut lis_sequence = Vec::default(); + lis_sequence.reserve(new_index_to_old_index.len()); + + let mut predecessors = vec![0; new_index_to_old_index.len()]; + let mut starts = vec![0; new_index_to_old_index.len()]; + + longest_increasing_subsequence::lis_with( + &new_index_to_old_index, + &mut lis_sequence, + |a, b| a < b, + &mut predecessors, + &mut starts, + ); + + // the lis comes out backwards, I think. can't quite tell. + lis_sequence.sort_unstable(); + + // if a new node gets u32 max and is at the end, then it might be part of our LIS (because u32 max is a valid LIS) + if lis_sequence.last().map(|f| new_index_to_old_index[*f]) == Some(u32::MAX as usize) { + lis_sequence.pop(); + } + + for idx in lis_sequence.iter() { + self.diff_node(&old[new_index_to_old_index[*idx]], &new[*idx]); + } + + let mut nodes_created = 0; + + // add mount instruction for the first items not covered by the lis + let last = *lis_sequence.last().unwrap(); + if last < (new.len() - 1) { + for (idx, new_node) in new[(last + 1)..].iter().enumerate() { + let new_idx = idx + last + 1; + let old_index = new_index_to_old_index[new_idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } + } + + self.mutations.insert_after( + self.find_last_element(&new[last]).unwrap(), + nodes_created as u32, + ); + nodes_created = 0; + } + + // for each spacing, generate a mount instruction + let mut lis_iter = lis_sequence.iter().rev(); + let mut last = *lis_iter.next().unwrap(); + for next in lis_iter { + if last - next > 1 { + for (idx, new_node) in new[(next + 1)..last].iter().enumerate() { + let new_idx = idx + next + 1; + + let old_index = new_index_to_old_index[new_idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } + } + + self.mutations.insert_before( + self.find_first_element(&new[last]).unwrap(), + nodes_created as u32, + ); + + nodes_created = 0; + } + last = *next; + } + + // add mount instruction for the last items not covered by the lis + let first_lis = *lis_sequence.first().unwrap(); + if first_lis > 0 { + for (idx, new_node) in new[..first_lis].iter().enumerate() { + let old_index = new_index_to_old_index[idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } + } + + self.mutations.insert_before( + self.find_first_element(&new[first_lis]).unwrap(), + nodes_created as u32, + ); + } + } + + fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { + // fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { + log::debug!("Replacing node {:?}", old); + let nodes_created = self.create_node(new); + self.replace_inner(old, nodes_created); + } + + fn replace_inner(&mut self, old: &'b VNode<'b>, nodes_created: usize) { + match old { + VNode::Element(el) => { + let id = old + .try_mounted_id() + .unwrap_or_else(|| panic!("broke on {:?}", old)); + + self.mutations.replace_with(id, nodes_created as u32); + self.remove_nodes(el.children, false); + self.scopes.collect_garbage(id); + } + + VNode::Text(_) | VNode::Placeholder(_) => { + let id = old + .try_mounted_id() + .unwrap_or_else(|| panic!("broke on {:?}", old)); + + self.mutations.replace_with(id, nodes_created as u32); + self.scopes.collect_garbage(id); + } + + VNode::Fragment(f) => { + self.replace_inner(&f.children[0], nodes_created); + self.remove_nodes(f.children.iter().skip(1), true); + } + + VNode::Component(c) => { + let node = self.scopes.fin_head(c.scope.get().unwrap()); + self.replace_node(node, node); + + let scope_id = c.scope.get().unwrap(); + + // we can only remove components if they are actively being diffed + if self.scope_stack.contains(&c.originator) { + self.scopes.try_remove(scope_id).unwrap(); + } + } + } + } + + pub fn remove_nodes(&mut self, nodes: impl IntoIterator>, gen_muts: bool) { + for node in nodes { + match node { + VNode::Text(t) => { + // this check exists because our null node will be removed but does not have an ID + if let Some(id) = t.id.get() { + self.scopes.collect_garbage(id); + + if gen_muts { + self.mutations.remove(id.as_u64()); + } + } + } + VNode::Placeholder(a) => { + let id = a.id.get().unwrap(); + self.scopes.collect_garbage(id); + + if gen_muts { + self.mutations.remove(id.as_u64()); + } + } + VNode::Element(e) => { + let id = e.id.get().unwrap(); + + if gen_muts { + self.mutations.remove(id.as_u64()); + } + + self.scopes.collect_garbage(id); + + self.remove_nodes(e.children, false); + } + + VNode::Fragment(f) => { + self.remove_nodes(f.children, gen_muts); + } + + VNode::Component(c) => { + let scope_id = c.scope.get().unwrap(); + let root = self.scopes.root_node(scope_id); + self.remove_nodes([root], gen_muts); + + // we can only remove this node if the originator is actively + if self.scope_stack.contains(&c.originator) { + self.scopes.try_remove(scope_id).unwrap(); + } + } + } + } + } + + fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { + let mut created = 0; + for node in nodes { + created += self.create_node(node); + } + created + } + + fn create_and_append_children(&mut self, nodes: &'b [VNode<'b>]) { + let created = self.create_children(nodes); + self.mutations.append_children(created as u32); + } + + fn create_and_insert_after(&mut self, nodes: &'b [VNode<'b>], after: &'b VNode<'b>) { + let created = self.create_children(nodes); + let last = self.find_last_element(after).unwrap(); + self.mutations.insert_after(last, created as u32); + } + + fn create_and_insert_before(&mut self, nodes: &'b [VNode<'b>], before: &'b VNode<'b>) { + let created = self.create_children(nodes); + let first = self.find_first_element(before).unwrap(); + self.mutations.insert_before(first, created as u32); + } + + fn current_scope(&self) -> Option { + self.scope_stack.last().copied() + } + + fn enter_scope(&mut self, scope: ScopeId) { + self.scope_stack.push(scope); + } + + fn leave_scope(&mut self) { + self.scope_stack.pop(); + } + + fn find_last_element(&self, vnode: &'b VNode<'b>) -> Option { + let mut search_node = Some(vnode); + + loop { + match &search_node.take().unwrap() { + VNode::Text(t) => break t.id.get(), + VNode::Element(t) => break t.id.get(), + VNode::Placeholder(t) => break t.id.get(), + VNode::Fragment(frag) => { + search_node = frag.children.last(); + } + VNode::Component(el) => { + let scope_id = el.scope.get().unwrap(); + search_node = Some(self.scopes.root_node(scope_id)); + } + } + } + } + + fn find_first_element(&self, vnode: &'b VNode<'b>) -> Option { + let mut search_node = Some(vnode); + + loop { + match &search_node.take().unwrap() { + // the ones that have a direct id + VNode::Fragment(frag) => { + search_node = Some(&frag.children[0]); + } + VNode::Component(el) => { + let scope_id = el.scope.get().unwrap(); + search_node = Some(self.scopes.root_node(scope_id)); + } + VNode::Text(t) => break t.id.get(), + VNode::Element(t) => break t.id.get(), + VNode::Placeholder(t) => break t.id.get(), + } + } + } + + // fn replace_node(&mut self, old: &'b VNode<'b>, nodes_created: usize) { + // log::debug!("Replacing node {:?}", old); + // match old { + // VNode::Element(el) => { + // let id = old + // .try_mounted_id() + // .unwrap_or_else(|| panic!("broke on {:?}", old)); + + // self.mutations.replace_with(id, nodes_created as u32); + // self.remove_nodes(el.children, false); + // } + + // VNode::Text(_) | VNode::Placeholder(_) => { + // let id = old + // .try_mounted_id() + // .unwrap_or_else(|| panic!("broke on {:?}", old)); + + // self.mutations.replace_with(id, nodes_created as u32); + // } + + // VNode::Fragment(f) => { + // self.replace_node(&f.children[0], nodes_created); + // self.remove_nodes(f.children.iter().skip(1), true); + // } + + // VNode::Component(c) => { + // let node = self.scopes.fin_head(c.scope.get().unwrap()); + // self.replace_node(node, nodes_created); + + // let scope_id = c.scope.get().unwrap(); + + // // we can only remove components if they are actively being diffed + // if self.stack.scope_stack.contains(&c.originator) { + // self.scopes.try_remove(scope_id).unwrap(); + // } + // } + // } + // } + + // /// schedules nodes for garbage collection and pushes "remove" to the mutation stack + // /// remove can happen whenever + // pub(crate) fn remove_nodes( + // &mut self, + // nodes: impl IntoIterator>, + // gen_muts: bool, + // ) { + // // or cache the vec on the diff machine + // for node in nodes { + // log::debug!("removing {:?}", node); + // match node { + // VNode::Text(t) => { + // // this check exists because our null node will be removed but does not have an ID + // if let Some(id) = t.id.get() { + // // self.scopes.collect_garbage(id); + + // if gen_muts { + // self.mutations.remove(id.as_u64()); + // } + // } + // } + // VNode::Placeholder(a) => { + // let id = a.id.get().unwrap(); + // // self.scopes.collect_garbage(id); + + // if gen_muts { + // self.mutations.remove(id.as_u64()); + // } + // } + // VNode::Element(e) => { + // let id = e.id.get().unwrap(); + + // if gen_muts { + // self.mutations.remove(id.as_u64()); + // } + + // self.remove_nodes(e.children, false); + + // // self.scopes.collect_garbage(id); + // } + + // VNode::Fragment(f) => { + // self.remove_nodes(f.children, gen_muts); + // } + + // VNode::Component(c) => { + // let scope_id = c.scope.get().unwrap(); + // let root = self.scopes.root_node(scope_id); + // self.remove_nodes(Some(root), gen_muts); + + // // we can only remove this node if the originator is actively + // if self.stack.scope_stack.contains(&c.originator) { + // self.scopes.try_remove(scope_id).unwrap(); + // } + // } + // } + // } + // } + + // recursively push all the nodes of a tree onto the stack and return how many are there + fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize { + match node { + VNode::Text(_) | VNode::Placeholder(_) => { + self.mutations.push_root(node.mounted_id()); + 1 + } + + VNode::Fragment(_) | VNode::Component(_) => { + // + let mut added = 0; + for child in node.children() { + added += self.push_all_nodes(child); + } + added + } + + VNode::Element(el) => { + let mut num_on_stack = 0; + for child in el.children.iter() { + num_on_stack += self.push_all_nodes(child); + } + self.mutations.push_root(el.id.get().unwrap()); + + num_on_stack + 1 + } + } + } +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dba4effbb..48a8a3505 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,7 +1,8 @@ #![allow(non_snake_case)] #![doc = include_str!("../README.md")] -pub(crate) mod diff; +// pub(crate) mod diff; +pub(crate) mod diff_async; pub(crate) mod events; pub(crate) mod lazynodes; pub(crate) mod mutations; @@ -12,7 +13,8 @@ pub(crate) mod util; pub(crate) mod virtual_dom; pub(crate) mod innerlude { - pub(crate) use crate::diff::*; + pub(crate) use crate::diff_async::*; + // pub(crate) use crate::diff::*; pub use crate::events::*; pub use crate::lazynodes::*; pub use crate::mutations::*; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index fa96edd1b..bd94d0b71 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -143,6 +143,10 @@ impl<'a> Mutations<'a> { self.edits.push(InsertBefore { n, root }); } + pub(crate) fn append_children(&mut self, n: u32) { + self.edits.push(AppendChildren { many: n }); + } + // Remove Nodes from the dom pub(crate) fn remove(&mut self, id: u64) { self.edits.push(Remove { root: id }); @@ -217,6 +221,10 @@ impl<'a> Mutations<'a> { let name = attribute.name; self.edits.push(RemoveAttribute { name, root }); } + + pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) { + self.dirty_scopes.insert(scope); + } } // refs are only assigned once diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 64da46279..74234a84c 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -175,6 +175,7 @@ impl Debug for VNode<'_> { .field("key", &el.key) .field("attrs", &el.attributes) .field("children", &el.children) + .field("id", &el.id) .finish(), VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text), VNode::Placeholder(t) => write!(s, "VNode::VPlaceholder {{ id: {:?} }}", t.id), @@ -365,9 +366,7 @@ pub struct EventHandler<'bump, T = ()> { impl<'a, T> Default for EventHandler<'a, T> { fn default() -> Self { - Self { - callback: RefCell::new(None), - } + Self { callback: RefCell::new(None) } } } @@ -441,10 +440,7 @@ pub struct NodeFactory<'a> { impl<'a> NodeFactory<'a> { pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> { - NodeFactory { - scope, - bump: &scope.wip_frame().bump, - } + NodeFactory { scope, bump: &scope.wip_frame().bump } } #[inline] @@ -454,11 +450,10 @@ impl<'a> NodeFactory<'a> { /// Directly pass in text blocks without the need to use the format_args macro. pub fn static_text(&self, text: &'static str) -> VNode<'a> { - VNode::Text(self.bump.alloc(VText { - id: empty_cell(), - text, - is_static: true, - })) + VNode::Text( + self.bump + .alloc(VText { id: empty_cell(), text, is_static: true }), + ) } /// Parses a lazy text Arguments and returns a string and a flag indicating if the text is 'static @@ -481,11 +476,7 @@ impl<'a> NodeFactory<'a> { pub fn text(&self, args: Arguments) -> VNode<'a> { let (text, is_static) = self.raw_text(args); - VNode::Text(self.bump.alloc(VText { - text, - is_static, - id: empty_cell(), - })) + VNode::Text(self.bump.alloc(VText { text, is_static, id: empty_cell() })) } pub fn element( @@ -543,13 +534,7 @@ impl<'a> NodeFactory<'a> { is_volatile: bool, ) -> Attribute<'a> { let (value, is_static) = self.raw_text(val); - Attribute { - name, - value, - is_static, - namespace, - is_volatile, - } + Attribute { name, value, is_static, namespace, is_volatile } } pub fn component

( @@ -591,11 +576,7 @@ impl<'a> NodeFactory<'a> { } pub fn listener(self, event: &'static str, callback: InternalHandler<'a>) -> Listener<'a> { - Listener { - event, - mounted_node: Cell::new(None), - callback, - } + Listener { event, mounted_node: Cell::new(None), callback } } pub fn fragment_root<'b, 'c>( @@ -611,10 +592,10 @@ impl<'a> NodeFactory<'a> { if nodes.is_empty() { VNode::Placeholder(self.bump.alloc(VPlaceholder { id: empty_cell() })) } else { - VNode::Fragment(self.bump.alloc(VFragment { - children: nodes.into_bump_slice(), - key: None, - })) + VNode::Fragment( + self.bump + .alloc(VFragment { children: nodes.into_bump_slice(), key: None }), + ) } } @@ -650,10 +631,7 @@ impl<'a> NodeFactory<'a> { ); } - VNode::Fragment(self.bump.alloc(VFragment { - children, - key: None, - })) + VNode::Fragment(self.bump.alloc(VFragment { children, key: None })) } } @@ -676,10 +654,9 @@ impl<'a> NodeFactory<'a> { } else { let children = nodes.into_bump_slice(); - Some(VNode::Fragment(self.bump.alloc(VFragment { - children, - key: None, - }))) + Some(VNode::Fragment( + self.bump.alloc(VFragment { children, key: None }), + )) } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index d8cfd6505..542afc673 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -203,7 +203,8 @@ impl ScopeArena { } pub fn collect_garbage(&self, id: ElementId) { - log::debug!("collecting garbage for {:?}", id); + // println!("collecting garbage for {:?}", id); + // log::debug!("collecting garbage for {:?}", id); self.nodes.borrow_mut().remove(id.0); } @@ -304,11 +305,9 @@ impl ScopeArena { frame.node.set(unsafe { extend_vnode(node) }); } else { let frame = scope.wip_frame(); - let node = frame - .bump - .alloc(VNode::Placeholder(frame.bump.alloc(VPlaceholder { - id: Default::default(), - }))); + let node = frame.bump.alloc(VNode::Placeholder( + frame.bump.alloc(VPlaceholder { id: Default::default() }), + )); frame.node.set(unsafe { extend_vnode(node) }); } @@ -418,10 +417,7 @@ pub struct Scope<'a, P = ()> { impl

Copy for Scope<'_, P> {} impl

Clone for Scope<'_, P> { fn clone(&self) -> Self { - Self { - scope: self.scope, - props: self.props, - } + Self { scope: self.scope, props: self.props } } } @@ -784,10 +780,7 @@ impl ScopeState { /// } ///``` pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option> { - Some(rsx.call(NodeFactory { - scope: self, - bump: &self.wip_frame().bump, - })) + Some(rsx.call(NodeFactory { scope: self, bump: &self.wip_frame().bump })) } /// Store a value between renders @@ -894,10 +887,7 @@ impl ScopeState { self.shared_contexts.get_mut().clear(); // next: reset the node data - let SelfReferentialItems { - borrowed_props, - listeners, - } = self.items.get_mut(); + let SelfReferentialItems { borrowed_props, listeners } = self.items.get_mut(); borrowed_props.clear(); listeners.clear(); self.frames[0].reset(); @@ -955,11 +945,7 @@ pub(crate) struct TaskQueue { pub(crate) type InnerTask = Pin>>; impl TaskQueue { fn new(sender: UnboundedSender) -> Rc { - Rc::new(Self { - tasks: RefCell::new(FxHashMap::default()), - gen: Cell::new(0), - sender, - }) + Rc::new(Self { tasks: RefCell::new(FxHashMap::default()), gen: Cell::new(0), sender }) } fn push_fut(&self, task: impl Future + 'static) -> TaskId { let pinned = Box::pin(task); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1f443db86..d879e0340 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,6 +2,7 @@ //! //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. +use crate::diff_async::AsyncDiffState as DiffState; use crate::innerlude::*; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::{future::poll_fn, StreamExt}; @@ -453,7 +454,7 @@ impl VirtualDom { while !self.dirty_scopes.is_empty() { let scopes = &self.scopes; - let mut diff_state = DiffState::new(scopes); + let mut diff_state = AsyncDiffState::new(scopes); let mut ran_scopes = FxHashSet::default(); @@ -473,32 +474,33 @@ impl VirtualDom { self.scopes.run_scope(scopeid); - let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); - diff_state.stack.push(DiffInstruction::Diff { new, old }); + diff_state.diff_scope(scopeid); - log::debug!("pushing scope {:?} onto scope stack", scopeid); - diff_state.stack.scope_stack.push(scopeid); + let AsyncDiffState { mutations, .. } = diff_state; - let scope = scopes.get_scope(scopeid).unwrap(); - diff_state.stack.element_stack.push(scope.container); + for scope in &mutations.dirty_scopes { + self.dirty_scopes.remove(scope); + } + + committed_mutations.push(mutations); + + // if diff_state.work(&mut deadline) { + // let DiffState { mutations, .. } = diff_state; + + // for scope in &mutations.dirty_scopes { + // self.dirty_scopes.remove(scope); + // } + + // committed_mutations.push(mutations); + // } else { + // // leave the work in an incomplete state + // // + // // todo: we should store the edits and re-apply them later + // // for now, we just dump the work completely (threadsafe) + // return committed_mutations; + // } } } - - if diff_state.work(&mut deadline) { - let DiffState { mutations, .. } = diff_state; - - for scope in &mutations.dirty_scopes { - self.dirty_scopes.remove(scope); - } - - committed_mutations.push(mutations); - } else { - // leave the work in an incomplete state - // - // todo: we should store the edits and re-apply them later - // for now, we just dump the work completely (threadsafe) - return committed_mutations; - } } committed_mutations @@ -526,13 +528,15 @@ impl VirtualDom { let mut diff_state = DiffState::new(&self.scopes); self.scopes.run_scope(scope_id); - diff_state - .stack - .create_node(self.scopes.fin_head(scope_id), MountType::Append); - diff_state.stack.element_stack.push(ElementId(0)); - diff_state.stack.scope_stack.push(scope_id); - diff_state.work(|| false); + diff_state.element_stack.push(ElementId(0)); + diff_state.scope_stack.push(scope_id); + + let node = self.scopes.fin_head(scope_id); + let created = diff_state.create_node(node); + + diff_state.mutations.append_children(created as u32); + self.dirty_scopes.clear(); assert!(self.dirty_scopes.is_empty()); @@ -579,12 +583,11 @@ impl VirtualDom { ); diff_machine.force_diff = true; - diff_machine.stack.push(DiffInstruction::Diff { old, new }); - diff_machine.stack.scope_stack.push(scope_id); - + diff_machine.scope_stack.push(scope_id); let scope = diff_machine.scopes.get_scope(scope_id).unwrap(); - diff_machine.stack.element_stack.push(scope.container); - diff_machine.work(|| false); + diff_machine.element_stack.push(scope.container); + + diff_machine.diff_node(old, new); diff_machine.mutations } @@ -623,10 +626,10 @@ impl VirtualDom { /// ``` pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); - machine.stack.push(DiffInstruction::Diff { new, old }); - machine.stack.element_stack.push(ElementId(0)); - machine.stack.scope_stack.push(ScopeId(0)); - machine.work(|| false); + machine.element_stack.push(ElementId(0)); + machine.scope_stack.push(ScopeId(0)); + machine.diff_node(old, new); + machine.mutations } @@ -645,11 +648,11 @@ impl VirtualDom { /// ``` pub fn create_vnodes<'a>(&'a self, nodes: LazyNodes<'a, '_>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); - machine.stack.element_stack.push(ElementId(0)); - machine - .stack - .create_node(self.render_vnodes(nodes), MountType::Append); - machine.work(|| false); + machine.scope_stack.push(ScopeId(0)); + machine.element_stack.push(ElementId(0)); + let node = self.render_vnodes(nodes); + let created = machine.create_node(node); + machine.mutations.append_children(created as u32); machine.mutations } @@ -674,16 +677,15 @@ impl VirtualDom { let (old, new) = (self.render_vnodes(left), self.render_vnodes(right)); let mut create = DiffState::new(&self.scopes); - create.stack.scope_stack.push(ScopeId(0)); - create.stack.element_stack.push(ElementId(0)); - create.stack.create_node(old, MountType::Append); - create.work(|| false); + create.scope_stack.push(ScopeId(0)); + create.element_stack.push(ElementId(0)); + let created = create.create_node(old); + create.mutations.append_children(created as u32); let mut edit = DiffState::new(&self.scopes); - edit.stack.scope_stack.push(ScopeId(0)); - edit.stack.element_stack.push(ElementId(0)); - edit.stack.push(DiffInstruction::Diff { old, new }); - edit.work(|| false); + edit.scope_stack.push(ScopeId(0)); + edit.element_stack.push(ElementId(0)); + edit.diff_node(old, new); (create.mutations, edit.mutations) } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 5b98ea80e..c407359c9 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -1,4 +1,5 @@ #![allow(unused, non_upper_case_globals)] + //! Diffing Tests //! //! These tests only verify that the diffing algorithm works properly for single components. @@ -31,26 +32,14 @@ fn html_and_rsx_generate_the_same_output() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello world" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "Hello world" }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, ] ); - assert_eq!( - change.edits, - [SetText { - text: "Goodbye world", - root: 2 - },] - ); + assert_eq!(change.edits, [SetText { text: "Goodbye world", root: 2 },]); } /// Should result in 3 elements on the stack @@ -67,32 +56,14 @@ fn fragments_create_properly() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello a" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "Hello a" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - root: 4, - text: "Hello b" - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { root: 4, text: "Hello b" }, AppendChildren { many: 1 }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "Hello c" - }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "Hello c" }, AppendChildren { many: 1 }, AppendChildren { many: 3 }, ] @@ -116,10 +87,7 @@ fn empty_fragments_create_anchors() { assert_eq!( change.edits, [ - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 2, tag: "div" }, ReplaceWith { m: 1, root: 1 } ] ); @@ -138,29 +106,15 @@ fn empty_fragments_create_many_anchors() { create.edits, [CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }] ); + assert_eq!( change.edits, [ - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateElement { root: 6, tag: "div" }, ReplaceWith { m: 5, root: 1 } ] ); @@ -188,32 +142,14 @@ fn empty_fragments_create_anchors_with_many_children() { assert_eq!( change.edits, [ - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "hello: 0", - root: 3 - }, + CreateElement { tag: "div", root: 2 }, + CreateTextNode { text: "hello: 0", root: 3 }, AppendChildren { many: 1 }, - CreateElement { - tag: "div", - root: 4, - }, - CreateTextNode { - text: "hello: 1", - root: 5 - }, + CreateElement { tag: "div", root: 4 }, + CreateTextNode { text: "hello: 1", root: 5 }, AppendChildren { many: 1 }, - CreateElement { - tag: "div", - root: 6, - }, - CreateTextNode { - text: "hello: 2", - root: 7 - }, + CreateElement { tag: "div", root: 6 }, + CreateTextNode { text: "hello: 2", root: 7 }, AppendChildren { many: 1 }, ReplaceWith { root: 1, m: 3 } ] @@ -236,23 +172,11 @@ fn many_items_become_fragment() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 2 - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { text: "hello", root: 2 }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 4 - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { text: "hello", root: 4 }, AppendChildren { many: 1 }, AppendChildren { many: 2 }, ] @@ -315,7 +239,7 @@ fn two_fragments_with_differrent_elements_are_differet() { // replace the divs with new h1s CreateElement { tag: "h1", root: 7 }, ReplaceWith { root: 1, m: 1 }, - CreateElement { tag: "h1", root: 8 }, + CreateElement { tag: "h1", root: 1 }, // notice how 1 gets re-used ReplaceWith { root: 2, m: 1 }, ] ); @@ -339,39 +263,27 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, CreateElement { root: 6, tag: "p" }, AppendChildren { many: 6 }, ] ); + + // note: key reuse is always the last node that got used + // slab maintains a linked list, essentially assert_eq!( change.edits, [ Remove { root: 3 }, Remove { root: 4 }, Remove { root: 5 }, - CreateElement { root: 7, tag: "h1" }, - ReplaceWith { root: 1, m: 1 }, - CreateElement { root: 8, tag: "h1" }, + CreateElement { root: 5, tag: "h1" }, // 3 gets reused + ReplaceWith { root: 1, m: 1 }, // 1 gets deleted + CreateElement { root: 1, tag: "h1" }, // 1 gets reused ReplaceWith { root: 2, m: 1 }, ] ); @@ -395,14 +307,8 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, CreateElement { root: 3, tag: "p" }, AppendChildren { many: 3 }, ] @@ -410,18 +316,9 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( change.edits, [ - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateElement { root: 6, tag: "div" }, InsertAfter { root: 2, n: 3 }, ] ); @@ -628,14 +525,8 @@ fn keyed_diffing_additions() { assert_eq!( change.edits, [ - CreateElement { - root: 6, - tag: "div" - }, - CreateElement { - root: 7, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, + CreateElement { root: 7, tag: "div" }, InsertAfter { n: 2, root: 5 } ] ); @@ -663,14 +554,8 @@ fn keyed_diffing_additions_and_moves_on_ends() { change.edits, [ // create 11, 12 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, + CreateElement { tag: "div", root: 5 }, + CreateElement { tag: "div", root: 6 }, InsertAfter { root: 3, n: 2 }, // move 7 to the front PushRoot { root: 4 }, @@ -684,43 +569,32 @@ fn keyed_diffing_additions_and_moves_in_middle() { let dom = new_dom(); let left = rsx!({ - [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { + [/**/ 1, 2, 3, 4 /**/].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); let right = rsx!({ - [/**/ 7, 4, 13, 17, 5, 11, 12, 6 /**/].iter().map(|f| { + [/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); + // LIS: 4, 5, 6 let (_, change) = dom.diff_lazynodes(left, right); log::debug!("{:#?}", change); assert_eq!( change.edits, [ - // create 13, 17 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, - InsertBefore { root: 2, n: 2 }, - // create 11, 12 - CreateElement { - tag: "div", - root: 7 - }, - CreateElement { - tag: "div", - root: 8 - }, + // create 5, 6 + CreateElement { tag: "div", root: 5 }, + CreateElement { tag: "div", root: 6 }, InsertBefore { root: 3, n: 2 }, + // create 7, 8 + CreateElement { tag: "div", root: 7 }, + CreateElement { tag: "div", root: 8 }, + InsertBefore { root: 2, n: 2 }, // move 7 PushRoot { root: 4 }, InsertBefore { root: 1, n: 1 } @@ -756,16 +630,10 @@ fn controlled_keyed_diffing_out_of_order() { // remove 7 // create 9 and insert before 6 - CreateElement { - root: 5, - tag: "div" - }, + CreateElement { root: 5, tag: "div" }, InsertBefore { n: 1, root: 3 }, // create 0 and insert before 5 - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 2 }, ] ); @@ -792,10 +660,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { assert_eq!( changes.edits, [ - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 3 }, PushRoot { root: 4 }, InsertBefore { n: 1, root: 1 },