From a44e9fcffa4da6dd01966a62575ff4f4832e5fcc Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 9 Aug 2021 13:17:19 -0400 Subject: [PATCH 01/53] wip: more work on jank free --- packages/core/src/diff.rs | 48 ++--- packages/core/src/virtual_dom.rs | 307 ++++++++++++++++++------------- 2 files changed, 203 insertions(+), 152 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 09c4ea92e..f92a76e26 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -75,7 +75,7 @@ use DomEdit::*; pub struct DiffMachine<'r, 'bump> { pub vdom: &'bump SharedResources, - pub edits: Mutations<'bump>, + pub mutations: Mutations<'bump>, pub scope_stack: SmallVec<[ScopeId; 5]>, @@ -95,7 +95,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { shared: &'bump SharedResources, ) -> Self { Self { - edits, + mutations: edits, scope_stack: smallvec![cur_scope], vdom: shared, diffed: FxHashSet::default(), @@ -110,7 +110,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { /// This will PANIC if any component elements are passed in. pub fn new_headless(shared: &'bump SharedResources) -> Self { Self { - edits: Mutations { edits: Vec::new() }, + mutations: Mutations { edits: Vec::new() }, scope_stack: smallvec![ScopeId(0)], vdom: shared, diffed: FxHashSet::default(), @@ -182,13 +182,13 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { 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 { - please_commit(&mut self.edits.edits); + please_commit(&mut self.mutations.edits); self.edit_set_attribute(new_attr); } } } else { // TODO: provide some sort of report on how "good" the diffing was - please_commit(&mut self.edits.edits); + please_commit(&mut self.mutations.edits); for attribute in old.attributes { self.edit_remove_attribute(attribute); } @@ -209,7 +209,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { 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 { - please_commit(&mut self.edits.edits); + please_commit(&mut self.mutations.edits); self.edit_remove_event_listener(old_l.event); self.edit_new_event_listener(new_l, cur_scope); } @@ -217,7 +217,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { self.fix_listener(new_l); } } else { - please_commit(&mut self.edits.edits); + please_commit(&mut self.mutations.edits); for listener in old.listeners { self.edit_remove_event_listener(listener.event); } @@ -1343,42 +1343,42 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // Navigation pub(crate) fn edit_push_root(&mut self, root: ElementId) { let id = root.as_u64(); - self.edits.edits.push(PushRoot { id }); + self.mutations.edits.push(PushRoot { id }); } pub(crate) fn edit_pop(&mut self) { - self.edits.edits.push(PopRoot {}); + self.mutations.edits.push(PopRoot {}); } // Add Nodes to the dom // add m nodes from the stack pub(crate) fn edit_append_children(&mut self, many: u32) { - self.edits.edits.push(AppendChildren { many }); + self.mutations.edits.push(AppendChildren { many }); } // replace the n-m node on the stack with the m nodes // ends with the last element of the chain on the top of the stack pub(crate) fn edit_replace_with(&mut self, n: u32, m: u32) { - self.edits.edits.push(ReplaceWith { n, m }); + self.mutations.edits.push(ReplaceWith { n, m }); } pub(crate) fn edit_insert_after(&mut self, n: u32) { - self.edits.edits.push(InsertAfter { n }); + self.mutations.edits.push(InsertAfter { n }); } pub(crate) fn edit_insert_before(&mut self, n: u32) { - self.edits.edits.push(InsertBefore { n }); + self.mutations.edits.push(InsertBefore { n }); } // Remove Nodesfrom the dom pub(crate) fn edit_remove(&mut self) { - self.edits.edits.push(Remove); + self.mutations.edits.push(Remove); } // Create pub(crate) fn edit_create_text_node(&mut self, text: &'bump str, id: ElementId) { let id = id.as_u64(); - self.edits.edits.push(CreateTextNode { text, id }); + self.mutations.edits.push(CreateTextNode { text, id }); } pub(crate) fn edit_create_element( @@ -1389,15 +1389,15 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { ) { let id = id.as_u64(); match ns { - Some(ns) => self.edits.edits.push(CreateElementNs { id, ns, tag }), - None => self.edits.edits.push(CreateElement { id, tag }), + Some(ns) => self.mutations.edits.push(CreateElementNs { id, ns, tag }), + None => self.mutations.edits.push(CreateElement { id, tag }), } } // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom pub(crate) fn edit_create_placeholder(&mut self, id: ElementId) { let id = id.as_u64(); - self.edits.edits.push(CreatePlaceholder { id }); + self.mutations.edits.push(CreatePlaceholder { id }); } // events @@ -1410,7 +1410,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { let element_id = mounted_node.get().unwrap().as_u64(); - self.edits.edits.push(NewEventListener { + self.mutations.edits.push(NewEventListener { scope, event_name: event, mounted_node_id: element_id, @@ -1418,12 +1418,12 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { } pub(crate) fn edit_remove_event_listener(&mut self, event: &'static str) { - self.edits.edits.push(RemoveEventListener { event }); + self.mutations.edits.push(RemoveEventListener { event }); } // modify pub(crate) fn edit_set_text(&mut self, text: &'bump str) { - self.edits.edits.push(SetText { text }); + self.mutations.edits.push(SetText { text }); } pub(crate) fn edit_set_attribute(&mut self, attribute: &'bump Attribute) { @@ -1437,7 +1437,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // field: &'static str, // value: &'bump str, // ns: Option<&'static str>, - self.edits.edits.push(SetAttribute { + self.mutations.edits.push(SetAttribute { field: name, value, ns: *namespace, @@ -1460,7 +1460,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // field: &'static str, // value: &'bump str, // ns: Option<&'static str>, - self.edits.edits.push(SetAttribute { + self.mutations.edits.push(SetAttribute { field: name, value, ns: Some(namespace), @@ -1469,7 +1469,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { let name = attribute.name; - self.edits.edits.push(RemoveAttribute { name }); + self.mutations.edits.push(RemoveAttribute { name }); } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index e6f27c6c5..d880b110b 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -19,7 +19,7 @@ //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. #![allow(unreachable_code)] -use futures_util::StreamExt; +use futures_util::{Future, StreamExt}; use fxhash::FxHashMap; use crate::hooks::{SuspendedContext, SuspenseHook}; @@ -271,7 +271,7 @@ impl VirtualDom { /// that has suspended nodes or suspended tasks. Be warned - any async tasks running forever will prevent this method /// from completing. Consider using `run` and specifing a deadline. pub async fn run_unbounded<'s>(&'s mut self) -> Result> { - self.run_with_deadline(|| false).await + self.run_with_deadline(async {}).await } /// Run the virtualdom with a time limit. @@ -283,181 +283,232 @@ impl VirtualDom { /// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent /// "jank". It will try to finish whatever work it has by the deadline to free up time for other work. /// - /// Due to platform differences in how time is handled, this method accepts a closure that must return true when the - /// deadline is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room - /// into the deadline closure manually. + /// Due to platform differences in how time is handled, this method accepts a future that resolves when the deadline + /// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the + /// deadline closure manually. /// /// The deadline is checked before starting to diff components. This strikes a balance between the overhead of checking /// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then /// the screen will "jank" up. In debug, this will trigger an alert. /// + /// If there are no in-flight fibers when this method is called, it will await any possible tasks, aborting early if + /// the provided deadline future resolves. + /// + /// For use in the web, it is expected that this method will be called to be executed during "idle times" and the + /// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft + /// entirely jank-free applications that perform a ton of work. + /// /// # Example /// /// ```no_run - /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!( div {"hello"} ))); + /// static App: FC<()> = |cx| rsx!(in cx, div {"hello"} ); + /// let mut dom = VirtualDom::new(App); /// loop { - /// let started = std::time::Instant::now(); - /// let deadline = move || std::time::Instant::now() - started > std::time::Duration::from_millis(16); - /// + /// let deadline = TimeoutFuture::from_ms(16); /// let mutations = dom.run_with_deadline(deadline).await; /// apply_mutations(mutations); /// } /// ``` pub async fn run_with_deadline<'s>( &'s mut self, - mut deadline_exceeded: impl FnMut() -> bool, + mut deadline: impl Future, ) -> Result> { - let cur_component = self.base_scope; + // Configure our deadline + use futures_util::FutureExt; + let mut deadline_future = deadline.boxed_local(); - let mut diff_machine = - DiffMachine::new(Mutations { edits: Vec::new() }, cur_component, &self.shared); + let is_ready = || -> bool { (&mut deadline_future).now_or_never().is_some() }; + + let mut diff_machine = DiffMachine::new( + Mutations { edits: Vec::new() }, + self.base_scope, + &self.shared, + ); /* Strategy: 1. Check if there are any events in the receiver. 2. If there are, process them and create a new fiber. 3. If there are no events, then choose a fiber to work on. - 4. If there are no fibers, then wait for the next event from the receiver. + 4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached. 5. While processing a fiber, periodically check if we're out of time - 6. If we are almost out of time, then commit our edits to the realdom + 6. If our deadling is reached, then commit our edits to the realdom 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) + + We slice fibers based on time. Each batch of events between frames is its own fiber. This is the simplest way + to conceptualize what *is* or *isn't* a fiber. IE if a bunch of events occur during a time slice, they all + get batched together as a single operation of "dirty" scopes. + + This approach is designed around the "diff during rIC and commit during rAF" + + We need to make sure to not call multiple events while the diff machine is borrowing the same scope. Because props + and listeners hold references to hook data, it is wrong to run a scope that is already being diffed. */ // 1. Consume any pending events and create new fibers let mut receiver = self.shared.task_receiver.borrow_mut(); - while let Ok(Some(trigger)) = receiver.try_next() { - // todo: cache the fibers - let mut fiber = Fiber::new(); - match &trigger.event { - // If any input event is received, then we need to create a new fiber - VirtualEvent::ClipboardEvent(_) - | VirtualEvent::CompositionEvent(_) - | VirtualEvent::KeyboardEvent(_) - | VirtualEvent::FocusEvent(_) - | VirtualEvent::FormEvent(_) - | VirtualEvent::SelectionEvent(_) - | VirtualEvent::TouchEvent(_) - | VirtualEvent::UIEvent(_) - | VirtualEvent::WheelEvent(_) - | VirtualEvent::MediaEvent(_) - | VirtualEvent::AnimationEvent(_) - | VirtualEvent::TransitionEvent(_) - | VirtualEvent::ToggleEvent(_) - | VirtualEvent::MouseEvent(_) - | VirtualEvent::PointerEvent(_) => { - if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { - scope.call_listener(trigger)?; + // On the primary event queue, there is no batching. + let mut trigger = { + match receiver.try_next() { + Ok(Some(trigger)) => trigger, + _ => { + // Continuously poll the future pool and the event receiver for work + let mut tasks = self.shared.async_tasks.borrow_mut(); + let tasks_tasks = tasks.next(); + + let mut receiver = self.shared.task_receiver.borrow_mut(); + let reciv_task = receiver.next(); + + futures_util::pin_mut!(tasks_tasks); + futures_util::pin_mut!(reciv_task); + + // Poll the event receiver and the future pool for work + // Abort early if our deadline has ran out + use futures_util::select; + let mut deadline = (&mut deadline_future).fuse(); + + let trig = select! { + trigger = tasks_tasks => trigger, + trigger = reciv_task => trigger, + _ = deadline => { return Ok(diff_machine.mutations); } + }; + + trig.unwrap() + } + } + }; + + // since the last time we were ran with a deadline, we've accumulated many updates + // IE a button was clicked twice, or a scroll trigger was fired twice. + // We consider the button a event to be a function of the current state, which means we can batch many updates + // together. + + match &trigger.event { + // If any input event is received, then we need to create a new fiber + VirtualEvent::ClipboardEvent(_) + | VirtualEvent::CompositionEvent(_) + | VirtualEvent::KeyboardEvent(_) + | VirtualEvent::FocusEvent(_) + | VirtualEvent::FormEvent(_) + | VirtualEvent::SelectionEvent(_) + | VirtualEvent::TouchEvent(_) + | VirtualEvent::UIEvent(_) + | VirtualEvent::WheelEvent(_) + | VirtualEvent::MediaEvent(_) + | VirtualEvent::AnimationEvent(_) + | VirtualEvent::TransitionEvent(_) + | VirtualEvent::ToggleEvent(_) + | VirtualEvent::MouseEvent(_) + | VirtualEvent::PointerEvent(_) => { + if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { + scope.call_listener(trigger)?; + } + } + + VirtualEvent::AsyncEvent { .. } => while let Ok(Some(event)) = receiver.try_next() {}, + + // These shouldn't normally be received, but if they are, it's done because some task set state manually + // Instead of processing it serially, + // We will batch all the scheduled updates together in one go. + VirtualEvent::ScheduledUpdate { height: u32 } => {} + + // Suspense Events! A component's suspended node is updated + VirtualEvent::SuspenseEvent { hook_idx, domnode } => { + // Safety: this handler is the only thing that can mutate shared items at this moment in tim + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + + // safety: we are sure that there are no other references to the inner content of suspense hooks + let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); + + let cx = Context { scope, props: &() }; + let scx = SuspendedContext { inner: cx }; + + // generate the new node! + let nodes: Option = (&hook.callback)(scx); + match nodes { + None => { + log::warn!( + "Suspense event came through, but there were no generated nodes >:(." + ); + } + Some(nodes) => { + // allocate inside the finished frame - not the WIP frame + let nodes = scope.frames.finished_frame().bump.alloc(nodes); + + // push the old node's root onto the stack + let real_id = domnode.get().ok_or(Error::NotMounted)?; + diff_machine.edit_push_root(real_id); + + // push these new nodes onto the diff machines stack + let meta = diff_machine.create_vnode(&*nodes); + + // replace the placeholder with the new nodes we just pushed on the stack + diff_machine.edit_replace_with(1, meta.added_to_stack); } } + } - VirtualEvent::AsyncEvent { .. } => { - while let Ok(Some(event)) = receiver.try_next() { - fiber.pending_scopes.push(event.originator); - } - } + // Collecting garabge is not currently interruptible. + // + // In the future, it could be though + VirtualEvent::GarbageCollection => { + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - // These shouldn't normally be received, but if they are, it's done because some task set state manually - // Instead of batching the results, - VirtualEvent::ScheduledUpdate { height: u32 } => {} + let mut garbage_list = scope.consume_garbage(); - // Suspense Events! A component's suspended node is updated - VirtualEvent::SuspenseEvent { hook_idx, domnode } => { - // Safety: this handler is the only thing that can mutate shared items at this moment in tim - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - // safety: we are sure that there are no other references to the inner content of suspense hooks - let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - - let cx = Context { scope, props: &() }; - let scx = SuspendedContext { inner: cx }; - - // generate the new node! - let nodes: Option = (&hook.callback)(scx); - match nodes { - None => { - log::warn!( - "Suspense event came through, but there were no generated nodes >:(." - ); + let mut scopes_to_kill = Vec::new(); + while let Some(node) = garbage_list.pop() { + match &node.kind { + VNodeKind::Text(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Anchor(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Suspended(_) => { + self.shared.collect_garbage(node.direct_id()); } - Some(nodes) => { - // allocate inside the finished frame - not the WIP frame - let nodes = scope.frames.finished_frame().bump.alloc(nodes); - // push the old node's root onto the stack - let real_id = domnode.get().ok_or(Error::NotMounted)?; - diff_machine.edit_push_root(real_id); + VNodeKind::Element(el) => { + self.shared.collect_garbage(node.direct_id()); + for child in el.children { + garbage_list.push(child); + } + } - // push these new nodes onto the diff machines stack - let meta = diff_machine.create_vnode(&*nodes); + VNodeKind::Fragment(frag) => { + for child in frag.children { + garbage_list.push(child); + } + } - // replace the placeholder with the new nodes we just pushed on the stack - diff_machine.edit_replace_with(1, meta.added_to_stack); + VNodeKind::Component(comp) => { + // TODO: run the hook destructors and then even delete the scope + + let scope_id = comp.ass_scope.get().unwrap(); + let scope = self.get_scope(scope_id).unwrap(); + let root = scope.root(); + garbage_list.push(root); + scopes_to_kill.push(scope_id); } } } - // Collecting garabge is not currently interruptible. - // - // In the future, it could be though - VirtualEvent::GarbageCollection => { - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - let mut garbage_list = scope.consume_garbage(); - - let mut scopes_to_kill = Vec::new(); - while let Some(node) = garbage_list.pop() { - match &node.kind { - VNodeKind::Text(_) => { - self.shared.collect_garbage(node.direct_id()); - } - VNodeKind::Anchor(_) => { - self.shared.collect_garbage(node.direct_id()); - } - VNodeKind::Suspended(_) => { - self.shared.collect_garbage(node.direct_id()); - } - - VNodeKind::Element(el) => { - self.shared.collect_garbage(node.direct_id()); - for child in el.children { - garbage_list.push(child); - } - } - - VNodeKind::Fragment(frag) => { - for child in frag.children { - garbage_list.push(child); - } - } - - VNodeKind::Component(comp) => { - // TODO: run the hook destructors and then even delete the scope - - let scope_id = comp.ass_scope.get().unwrap(); - let scope = self.get_scope(scope_id).unwrap(); - let root = scope.root(); - garbage_list.push(root); - scopes_to_kill.push(scope_id); - } - } - } - - for scope in scopes_to_kill { - // oy kill em - log::debug!("should be removing scope {:#?}", scope); - } + for scope in scopes_to_kill { + // oy kill em + log::debug!("should be removing scope {:#?}", scope); } } } - while !deadline_exceeded() { - let mut receiver = self.shared.task_receiver.borrow_mut(); + // while !deadline() { + // let mut receiver = self.shared.task_receiver.borrow_mut(); - // no messages to receive, just work on the fiber - } + // // no messages to receive, just work on the fiber + // } - Ok(diff_machine.edits) + Ok(diff_machine.mutations) } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { From c1afeba1efb1a063705466a14648beee08cacb86 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 9 Aug 2021 17:09:33 -0400 Subject: [PATCH 02/53] wip: groundwork for noderefs --- .vscode/spellright.dict | 1 + Cargo.toml | 7 ++- README.md | 59 ++++++++++---------- examples/reference/noderefs.rs | 2 + examples/reference/task.rs | 2 +- packages/core-macro/src/rsx/ambiguous.rs | 12 ++--- packages/core-macro/src/rsx/element.rs | 8 ++- packages/core/Cargo.toml | 3 ++ packages/core/src/diff.rs | 2 +- packages/core/src/events.rs | 3 ++ packages/core/src/hooks.rs | 28 ++++++++++ packages/core/src/noderef.rs | 0 packages/core/src/virtual_dom.rs | 69 +++++++++++++++++++----- packages/core/tests/diffing.rs | 8 +-- packages/core/tests/hooks.rs | 28 ++++++++++ packages/desktop/src/dom.rs | 8 +-- packages/mobile/src/dom.rs | 8 +-- packages/mobile/src/lib.rs | 8 +-- packages/web/Cargo.toml | 3 +- packages/web/src/dom.rs | 12 ----- packages/web/src/lib.rs | 16 +++--- 21 files changed, 184 insertions(+), 103 deletions(-) create mode 100644 packages/core/src/noderef.rs create mode 100644 packages/core/tests/hooks.rs diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index c15bc778c..348655db4 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -65,3 +65,4 @@ asynchronicity constified SegVec contentful +Jank diff --git a/Cargo.toml b/Cargo.toml index 1286072f5..19f2fae71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,11 +51,14 @@ argh = "0.1.5" env_logger = "*" async-std = { version = "1.9.0", features = ["attributes"] } rand = { version = "0.8.4", features = ["small_rng"] } -surf = {version = "2.2.0", git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"} +surf = { version = "2.2.0", git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm" } +gloo-timers = "0.2.1" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] gloo-timers = "0.2.1" -surf = {version = "2.2.0", default-features = false, features = ["wasm-client"], git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"} +surf = { version = "2.2.0", default-features = false, features = [ + "wasm-client", +], git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm" } [dependencies.getrandom] diff --git a/README.md b/README.md index 271942169..85edb1c5f 100644 --- a/README.md +++ b/README.md @@ -147,35 +147,36 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an ### Phase 1: The Basics -| Feature | Dioxus | React | Notes for Dioxus | -| ------------------------- | ------ | ----- | ----------------------------------------------------------- | -| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | -| Map, Iterator | ✅ | ✅ | map/filter/reduce to produce rsx! | -| Keyed Components | ✅ | ✅ | advanced diffing with keys | -| Web | ✅ | ✅ | renderer for web browser | -| Desktop (webview) | ✅ | ✅ | renderer for desktop | -| Shared State (Context) | ✅ | ✅ | share state through the tree | -| Hooks | ✅ | ✅ | memory cells in components | -| SSR | ✅ | ✅ | render directly to string | -| Component Children | ✅ | ✅ | cx.children() as a list of nodes | -| Headless components | ✅ | ✅ | components that don't return real elements | -| Fragments | ✅ | ✅ | multiple elements without a real root | -| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | -| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | -| CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups | -| Custom elements | ✅ | ✅ | Define new element primitives | -| Suspense | ✅ | ✅ | schedule future render from future/promise | -| Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax | -| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint | -| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events | -| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | -| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context | -| Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees | -| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts | -| Heuristic Engine | 🛠 | ❓ | track component memory usage to minimize future allocations | -| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | -| Effects | 🛠 | ✅ | Run effects after a component has been committed to render | -| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | +| Feature | Dioxus | React | Notes for Dioxus | +| ------------------------- | ------ | ----- | -------------------------------------------------------------------- | +| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | +| Map, Iterator | ✅ | ✅ | map/filter/reduce to produce rsx! | +| Keyed Components | ✅ | ✅ | advanced diffing with keys | +| Web | ✅ | ✅ | renderer for web browser | +| Desktop (webview) | ✅ | ✅ | renderer for desktop | +| Shared State (Context) | ✅ | ✅ | share state through the tree | +| Hooks | ✅ | ✅ | memory cells in components | +| SSR | ✅ | ✅ | render directly to string | +| Component Children | ✅ | ✅ | cx.children() as a list of nodes | +| Headless components | ✅ | ✅ | components that don't return real elements | +| Fragments | ✅ | ✅ | multiple elements without a real root | +| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | +| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | +| CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups | +| Custom elements | ✅ | ✅ | Define new element primitives | +| Suspense | ✅ | ✅ | schedule future render from future/promise | +| Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax | +| NodeRef | ✅ | ✅ | gain direct access to nodes [1] | +| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint | +| Jank-Free Rendering | ✅ | ✅ | Large diffs are segmented across frames for silky-smooth transitions | +| Cooperative Scheduling | ✅ | ✅ | Prioritize important events over non-important events | +| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | +| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context | +| Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees | +| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts | +| Heuristic Engine | ✅ | ❓ | track component memory usage to minimize future allocations | +| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | +| Effects | 🛠 | ✅ | Run effects after a component has been committed to render | - [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API. diff --git a/examples/reference/noderefs.rs b/examples/reference/noderefs.rs index fdab747b6..195210cfd 100644 --- a/examples/reference/noderefs.rs +++ b/examples/reference/noderefs.rs @@ -2,6 +2,8 @@ use dioxus::prelude::*; fn main() {} pub static Example: FC<()> = |cx| { + let p = 10; + cx.render(rsx! { div { diff --git a/examples/reference/task.rs b/examples/reference/task.rs index e455b6ff8..7e28786ec 100644 --- a/examples/reference/task.rs +++ b/examples/reference/task.rs @@ -30,7 +30,7 @@ pub static Example: FC<()> = |cx| { // Tasks are 'static, so we need to copy relevant items in let (async_count, dir) = (count.for_async(), *direction); - + let (task, result) = use_task(cx, move || async move { loop { gloo_timers::future::TimeoutFuture::new(250).await; diff --git a/packages/core-macro/src/rsx/ambiguous.rs b/packages/core-macro/src/rsx/ambiguous.rs index d75b07fc5..bfcfbc374 100644 --- a/packages/core-macro/src/rsx/ambiguous.rs +++ b/packages/core-macro/src/rsx/ambiguous.rs @@ -39,7 +39,8 @@ impl Parse for AmbiguousElement { } } - if let Ok(name) = input.fork().parse::() { + use syn::ext::IdentExt; + if let Ok(name) = input.fork().call(Ident::parse_any) { let name_str = name.to_string(); let first_char = name_str.chars().next().unwrap(); @@ -53,9 +54,6 @@ impl Parse for AmbiguousElement { .map(|c| AmbiguousElement::Element(c)) } } else { - if input.peek(LitStr) { - panic!("it's actually a litstr"); - } Err(Error::new(input.span(), "Not a valid Html tag")) } } @@ -80,7 +78,8 @@ impl Parse for AmbiguousElement { } } - if let Ok(name) = input.fork().parse::() { + use syn::ext::IdentExt; + if let Ok(name) = input.fork().call(Ident::parse_any) { let name_str = name.to_string(); let first_char = name_str.chars().next().unwrap(); @@ -94,9 +93,6 @@ impl Parse for AmbiguousElement { .map(|c| AmbiguousElement::Element(c)) } } else { - if input.peek(LitStr) { - panic!("it's actually a litstr"); - } Err(Error::new(input.span(), "Not a valid Html tag")) } } diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 0a6b89d17..4d9a2f09e 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -32,6 +32,7 @@ impl Parse for Element { let mut listeners: Vec> = vec![]; let mut children: Vec> = vec![]; let mut key = None; + let mut el_ref = None; 'parsing: loop { // [1] Break if empty @@ -45,6 +46,7 @@ impl Parse for Element { &mut attributes, &mut listeners, &mut key, + &mut el_ref, name.clone(), )?; } else { @@ -237,6 +239,7 @@ fn parse_rsx_element_field( attrs: &mut Vec>, listeners: &mut Vec>, key: &mut Option, + el_ref: &mut Option, element_name: Ident, ) -> Result<()> { let name = Ident::parse_any(stream)?; @@ -311,8 +314,9 @@ fn parse_rsx_element_field( "namespace" => { todo!("custom namespace not supported") } - "ref" => { - todo!("NodeRefs are currently not supported! This is currently a reserved keyword.") + "node_ref" => { + *el_ref = Some(stream.parse::()?); + return Ok(()); } // Fall through diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 00038baca..2ad42bccf 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -41,6 +41,9 @@ slab = "0.4.3" futures-channel = "0.3.16" +# used for noderefs +once_cell = "1.8.0" + [dev-dependencies] anyhow = "1.0.42" diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f92a76e26..12c16c2f4 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -110,7 +110,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { /// This will PANIC if any component elements are passed in. pub fn new_headless(shared: &'bump SharedResources) -> Self { Self { - mutations: Mutations { edits: Vec::new() }, + mutations: Mutations::new(), scope_stack: smallvec![ScopeId(0)], vdom: shared, diffed: FxHashSet::default(), diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 3fe04f28f..01fac846d 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -447,7 +447,10 @@ pub mod on { onmousemove /// onmouseout onmouseout + /// onmouseover + /// + /// Triggered when the users's mouse hovers over an element. onmouseover /// onmouseup onmouseup diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 538e2ae18..74fe330a6 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -256,3 +256,31 @@ impl<'src> SuspendedContext<'src> { Some(lazy_nodes.into_vnode(NodeFactory { bump })) } } + +#[derive(Clone, Copy)] +pub struct NodeRef<'src, T: 'static>(&'src RefCell); + +// impl NodeRef<'src, T> { +// fn set_ref(&self, new: Box) +// } +// impl<'a, T> std::ops::Deref for NodeRef<'a, T> { +// type Target = Option<&'a T>; + +// fn deref(&self) -> &Self::Target { +// // &self.node +// } +// } + +pub fn use_node_ref(cx: Context

) -> NodeRef { + cx.use_hook( + |f| {}, + |f| { + // + todo!() + // NodeRef {} + }, + |f| { + // + }, + ) +} diff --git a/packages/core/src/noderef.rs b/packages/core/src/noderef.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index d880b110b..76b323568 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -47,11 +47,11 @@ pub struct VirtualDom { /// /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas /// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually. - pub shared: SharedResources, + shared: SharedResources, /// The index of the root component /// Should always be the first (gen=0, id=0) - pub base_scope: ScopeId, + base_scope: ScopeId, active_fibers: Vec>, @@ -197,8 +197,7 @@ impl VirtualDom { /// pub fn rebuild<'s>(&'s mut self) -> Result>> { let mut edits = Vec::new(); - let mutations = Mutations { edits: Vec::new() }; - let mut diff_machine = DiffMachine::new(mutations, self.base_scope, &self.shared); + let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); let cur_component = diff_machine .get_scope_mut(&self.base_scope) @@ -274,7 +273,7 @@ impl VirtualDom { self.run_with_deadline(async {}).await } - /// Run the virtualdom with a time limit. + /// Run the virtualdom with a deadline. /// /// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline, /// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will @@ -287,7 +286,7 @@ impl VirtualDom { /// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the /// deadline closure manually. /// - /// The deadline is checked before starting to diff components. This strikes a balance between the overhead of checking + /// The deadline is polled before starting to diff components. This strikes a balance between the overhead of checking /// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then /// the screen will "jank" up. In debug, this will trigger an alert. /// @@ -309,6 +308,14 @@ impl VirtualDom { /// apply_mutations(mutations); /// } /// ``` + /// + /// ## Mutations + /// + /// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also + /// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has + /// applied the edits. + /// + /// Mutations are the only link between the RealDOM and the VirtualDOM. pub async fn run_with_deadline<'s>( &'s mut self, mut deadline: impl Future, @@ -319,11 +326,7 @@ impl VirtualDom { let is_ready = || -> bool { (&mut deadline_future).now_or_never().is_some() }; - let mut diff_machine = DiffMachine::new( - Mutations { edits: Vec::new() }, - self.base_scope, - &self.shared, - ); + let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); /* Strategy: @@ -348,6 +351,11 @@ impl VirtualDom { // 1. Consume any pending events and create new fibers let mut receiver = self.shared.task_receiver.borrow_mut(); + let current_fiber = { + // + self.active_fibers.get_mut(0).unwrap() + }; + // On the primary event queue, there is no batching. let mut trigger = { match receiver.try_next() { @@ -357,6 +365,8 @@ impl VirtualDom { let mut tasks = self.shared.async_tasks.borrow_mut(); let tasks_tasks = tasks.next(); + // if the new event generates work more important than our current fiber, we should consider switching + // only switch if it impacts different scopes. let mut receiver = self.shared.task_receiver.borrow_mut(); let reciv_task = receiver.next(); @@ -365,10 +375,9 @@ impl VirtualDom { // Poll the event receiver and the future pool for work // Abort early if our deadline has ran out - use futures_util::select; let mut deadline = (&mut deadline_future).fuse(); - let trig = select! { + let trig = futures_util::select! { trigger = tasks_tasks => trigger, trigger = reciv_task => trigger, _ = deadline => { return Ok(diff_machine.mutations); } @@ -401,6 +410,7 @@ impl VirtualDom { | VirtualEvent::ToggleEvent(_) | VirtualEvent::MouseEvent(_) | VirtualEvent::PointerEvent(_) => { + // if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { scope.call_listener(trigger)?; } @@ -411,7 +421,7 @@ impl VirtualDom { // These shouldn't normally be received, but if they are, it's done because some task set state manually // Instead of processing it serially, // We will batch all the scheduled updates together in one go. - VirtualEvent::ScheduledUpdate { height: u32 } => {} + VirtualEvent::ScheduledUpdate { .. } => {} // Suspense Events! A component's suspended node is updated VirtualEvent::SuspenseEvent { hook_idx, domnode } => { @@ -556,4 +566,35 @@ pub struct Mutations<'s> { // todo: apply node refs // todo: apply effects pub edits: Vec>, + + pub noderefs: Vec>, +} + +impl<'s> Mutations<'s> { + pub fn new() -> Self { + let edits = Vec::new(); + let noderefs = Vec::new(); + Self { edits, noderefs } + } +} + +// refs are only assigned once +pub struct NodeRefMutation<'a> { + element: &'a mut Option>>, + element_id: ElementId, +} + +impl<'a> NodeRefMutation<'a> { + pub fn downcast_ref(&self) -> Option<&T> { + self.element + .as_ref() + .and_then(|f| f.get()) + .and_then(|f| f.downcast_ref::()) + } + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.element + .as_mut() + .and_then(|f| f.get_mut()) + .and_then(|f| f.downcast_mut::()) + } } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 97c794b6e..9f74901ce 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -40,7 +40,7 @@ impl TestDom { fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Vec> { let mut edits = Vec::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &self.resources); + let mut machine = DiffMachine::new_headless(&self.resources); machine.diff_node(old, new); edits } @@ -52,7 +52,7 @@ impl TestDom { let old = self.bump.alloc(self.render(left)); let mut edits = Vec::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &self.resources); + let mut machine = DiffMachine::new_headless(&self.resources); let meta = machine.create_vnode(old); (meta, edits) } @@ -72,11 +72,11 @@ impl TestDom { let mut create_edits = Vec::new(); - let mut machine = DiffMachine::new_headless(&mut create_edits, &self.resources); + let mut machine = DiffMachine::new_headless(&self.resources); machine.create_vnode(old); let mut edits = Vec::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &self.resources); + let mut machine = DiffMachine::new_headless(&self.resources); machine.diff_node(old, new); (create_edits, edits) } diff --git a/packages/core/tests/hooks.rs b/packages/core/tests/hooks.rs new file mode 100644 index 000000000..6e3941e37 --- /dev/null +++ b/packages/core/tests/hooks.rs @@ -0,0 +1,28 @@ +use anyhow::{Context, Result}; +use dioxus::{ + arena::SharedResources, + diff::{CreateMeta, DiffMachine}, + prelude::*, + DomEdit, +}; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; + +#[test] +fn sample_refs() { + static App: FC<()> = |cx| { + let div_ref = use_node_ref::(cx); + + cx.render(rsx! { + div { + style: { color: "red" } + node_ref: div_ref + onmouseover: move |_| { + div_ref.borrow_mut().focus(); + } + } + }) + }; +} + +struct MyRef {} diff --git a/packages/desktop/src/dom.rs b/packages/desktop/src/dom.rs index 001552586..09323d40b 100644 --- a/packages/desktop/src/dom.rs +++ b/packages/desktop/src/dom.rs @@ -1,6 +1,6 @@ //! webview dom -use dioxus_core::{DomEdit, RealDom}; +use dioxus_core::DomEdit; // pub struct WebviewRegistry {} @@ -29,9 +29,3 @@ impl WebviewDom<'_> { // self.registry // } } -impl RealDom for WebviewDom<'_> { - fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - todo!() - // self.edits.push(PushRoot { root }); - } -} diff --git a/packages/mobile/src/dom.rs b/packages/mobile/src/dom.rs index c9e457479..35e74986c 100644 --- a/packages/mobile/src/dom.rs +++ b/packages/mobile/src/dom.rs @@ -1,6 +1,6 @@ //! webview dom -use dioxus_core::{DomEdit, ElementId, RealDom, ScopeId}; +use dioxus_core::{DomEdit, ElementId, ScopeId}; use DomEdit::*; pub struct WebviewRegistry {} @@ -30,9 +30,3 @@ impl WebviewDom<'_> { self.registry } } -impl<'bump> RealDom for WebviewDom<'bump> { - fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - todo!() - // self.edits.push(PushRoot { root }); - } -} diff --git a/packages/mobile/src/lib.rs b/packages/mobile/src/lib.rs index 1ee4bc652..fccdd4317 100644 --- a/packages/mobile/src/lib.rs +++ b/packages/mobile/src/lib.rs @@ -117,8 +117,8 @@ impl WebviewRenderer { // Serialize the edit stream let edits = { - let mut edits = Vec::new(); - lock.rebuild(&mut edits).unwrap(); + let mut edits = Vec::::new(); + // lock.rebuild(&mut edits).unwrap(); serde_json::to_value(edits).unwrap() }; @@ -139,8 +139,8 @@ impl WebviewRenderer { // Serialize the edit stream let edits = { - let mut edits = Vec::new(); - lock.rebuild(&mut edits).unwrap(); + let mut edits = Vec::::new(); + // lock.rebuild(&mut edits).unwrap(); serde_json::to_value(edits).unwrap() }; diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 10300c023..e02b10861 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -23,7 +23,7 @@ wasm-bindgen-test = "0.3.21" once_cell = "1.8" async-channel = "1.6.1" anyhow = "1.0" - +gloo-timers = { version = "0.2.1", features = ["futures"] } futures-util = "0.3.15" [dependencies.web-sys] @@ -78,7 +78,6 @@ im-rc = "15.0.0" separator = "0.4.1" uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } dioxus-hooks = { path = "../hooks" } -gloo-timers = { version = "0.2.1", features = ["futures"] } serde = { version = "1.0.126", features = ["derive"] } surf = { git = "https://github.com/http-rs/surf", rev = "1ffaba8873", default-features = false, features = [ "wasm-client", diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 37bbfc50c..e3a4e2c53 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -395,18 +395,6 @@ impl WebsysDom { } } -impl<'a> dioxus_core::diff::RealDom for WebsysDom { - // fn request_available_node(&mut self) -> ElementId { - // let key = self.nodes.insert(None); - // log::debug!("making new key: {:#?}", key); - // ElementId(key.data().as_ffi()) - // } - - fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - todo!() - } -} - #[derive(Debug, Default)] pub struct Stack { pub list: Vec, diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index abf9c7a83..8527ea82d 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -107,8 +107,6 @@ pub async fn run_with_props( let tasks = dom.get_event_sender(); - let mut real = RealDomWebsys {}; - // initialize the virtualdom first if cfg.hydrate { dom.rebuild_in_place()?; @@ -120,7 +118,12 @@ pub async fn run_with_props( Rc::new(move |event| tasks.unbounded_send(event).unwrap()), ); - dom.run_with_deadline(&mut websys_dom).await?; + loop { + let deadline = gloo_timers::future::TimeoutFuture::new(16); + let mut mutations = dom.run_with_deadline(deadline).await?; + + websys_dom.process_edits(&mut mutations.edits); + } Ok(()) } @@ -129,10 +132,3 @@ struct HydrationNode { id: usize, node: Node, } - -struct RealDomWebsys {} -impl dioxus::RealDom for RealDomWebsys { - fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - todo!() - } -} From 85e2dc259aba38ec1654ab7581418aaa9c1c543c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 10 Aug 2021 00:29:53 -0400 Subject: [PATCH 03/53] wip: close on putting it all together just gotta tie a bow --- README.md | 3 +- packages/core/src/arena.rs | 60 +++-- packages/core/src/events.rs | 52 ++--- packages/core/src/hooks.rs | 12 - packages/core/src/lib.rs | 2 + packages/core/src/noderef.rs | 119 ++++++++++ packages/core/src/scheduler.rs | 116 ++++++++++ packages/core/src/scope.rs | 41 +--- packages/core/src/virtual_dom.rs | 361 +++++++++---------------------- 9 files changed, 406 insertions(+), 360 deletions(-) create mode 100644 packages/core/src/scheduler.rs diff --git a/README.md b/README.md index 85edb1c5f..f1a6e02a1 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | Custom elements | ✅ | ✅ | Define new element primitives | | Suspense | ✅ | ✅ | schedule future render from future/promise | | Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax | -| NodeRef | ✅ | ✅ | gain direct access to nodes [1] | +| NodeRef | ✅ | ✅ | gain direct access to nodes | | Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint | | Jank-Free Rendering | ✅ | ✅ | Large diffs are segmented across frames for silky-smooth transitions | | Cooperative Scheduling | ✅ | ✅ | Prioritize important events over non-important events | @@ -178,7 +178,6 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | | Effects | 🛠 | ✅ | Run effects after a component has been committed to render | -- [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API. ### Phase 2: Advanced Toolkits diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 55c5c31e3..840cb34b8 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -4,6 +4,7 @@ use std::{cell::UnsafeCell, rc::Rc}; use crate::heuristics::*; use crate::innerlude::*; +use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::stream::FuturesUnordered; use fxhash::{FxHashMap, FxHashSet}; use slab::Slab; @@ -33,8 +34,11 @@ impl ElementId { } type Shared = Rc>; -type TaskReceiver = futures_channel::mpsc::UnboundedReceiver; -type TaskSender = futures_channel::mpsc::UnboundedSender; +type UiReceiver = UnboundedReceiver; +type UiSender = UnboundedSender; + +type TaskReceiver = UnboundedReceiver; +type TaskSender = UnboundedSender; /// These are resources shared among all the components and the virtualdom itself #[derive(Clone)] @@ -43,16 +47,23 @@ pub struct SharedResources { pub(crate) heuristics: Shared, - /// - pub task_sender: TaskSender, + // Used by "set_state" and co - is its own queue + pub immediate_sender: TaskSender, + pub immediate_receiver: Shared, - pub task_receiver: Shared, + /// Triggered by event listeners + pub ui_event_sender: UiSender, + pub ui_event_receiver: Shared, + // Garbage stored + pub pending_garbage: Shared>, + + // In-flight futures pub async_tasks: Shared>, /// We use a SlotSet to keep track of the keys that are currently being used. /// However, we don't store any specific data since the "mirror" - pub raw_elements: Rc>>, + pub raw_elements: Shared>, pub task_setter: Rc, } @@ -66,28 +77,30 @@ impl SharedResources { // elements are super cheap - the value takes no space let raw_elements = Slab::with_capacity(2000); - let (sender, receiver) = futures_channel::mpsc::unbounded(); + let (ui_sender, ui_receiver) = futures_channel::mpsc::unbounded(); + let (immediate_sender, immediate_receiver) = futures_channel::mpsc::unbounded(); let heuristics = HeuristicsEngine::new(); // we allocate this task setter once to save us from having to allocate later let task_setter = { - let queue = sender.clone(); + let queue = immediate_sender.clone(); let components = components.clone(); Rc::new(move |idx: ScopeId| { let comps = unsafe { &*components.get() }; if let Some(scope) = comps.get(idx.0) { - queue - .unbounded_send(EventTrigger::new( - VirtualEvent::ScheduledUpdate { - height: scope.height, - }, - idx, - None, - EventPriority::High, - )) - .expect("The event queu receiver should *never* be dropped"); + todo!("implement immediates again") + // queue + // .unbounded_send(EventTrigger::new( + // VirtualEvent::ScheduledUpdate { + // height: scope.height, + // }, + // idx, + // None, + // EventPriority::High, + // )) + // .expect("The event queu receiver should *never* be dropped"); } }) as Rc }; @@ -95,8 +108,15 @@ impl SharedResources { Self { components, async_tasks: Rc::new(RefCell::new(FuturesUnordered::new())), - task_receiver: Rc::new(RefCell::new(receiver)), - task_sender: sender, + + ui_event_receiver: Rc::new(RefCell::new(ui_receiver)), + ui_event_sender: ui_sender, + + immediate_receiver: Rc::new(RefCell::new(immediate_receiver)), + immediate_sender: immediate_sender, + + pending_garbage: Rc::new(RefCell::new(FxHashSet::default())), + heuristics: Rc::new(RefCell::new(heuristics)), raw_elements: Rc::new(RefCell::new(raw_elements)), task_setter, diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 01fac846d..6c3d7c1d0 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -58,32 +58,40 @@ impl Ord for EventKey { /// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. /// /// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. +/// +/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, +/// we keep it simple, and just use a 3-tier priority system. +/// +/// - NoPriority = 0 +/// - LowPriority = 1 +/// - NormalPriority = 2 +/// - UserBlocking = 3 +/// - HighPriority = 4 +/// - ImmediatePriority = 5 #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] pub enum EventPriority { - /// Garbage collection is a type of work than can be scheduled around other work, but must be completed in a specific - /// order. The GC must be run for a component before any other future work for that component is run. Otherwise, - /// we will leak slots in our slab. - /// - /// Garbage collection mixes with the safety aspects of the virtualdom so it's very important to get it done before - /// other work. - GarbageCollection, - /// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work. /// /// This is typically reserved for things like user interaction. - High, + /// + /// React calls these "discrete" events, but with an extra category of "user-blocking". + High = 2, /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important /// than "High Priority" events and will take presedence over low priority events. /// /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - Medium, + /// + /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). + Medium = 1, /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be /// advanced to the front of the work queue until completed. /// /// The primary user of Low Priority work is the asynchronous work system (suspense). - Low, + /// + /// This is considered "idle" work or "background" work. + Low = 0, } impl EventTrigger { @@ -103,21 +111,6 @@ impl EventTrigger { } pub enum VirtualEvent { - /// Generated during diffing to signal that a component's nodes to be given back - /// - /// Typically has a high priority - /// - /// If an event is scheduled for a component that has "garbage", that garabge will be cleaned up before the event can - /// be processed. - GarbageCollection, - - /// A type of "immediate" event scheduled by components - /// - /// Usually called through "set_state" - ScheduledUpdate { - height: u32, - }, - // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" // // Async events don't necessarily propagate into a scope being ran. It's up to the event itself @@ -179,10 +172,7 @@ impl VirtualEvent { | VirtualEvent::MouseEvent(_) | VirtualEvent::PointerEvent(_) => true, - VirtualEvent::GarbageCollection - | VirtualEvent::ScheduledUpdate { .. } - | VirtualEvent::AsyncEvent { .. } - | VirtualEvent::SuspenseEvent { .. } => false, + VirtualEvent::AsyncEvent { .. } | VirtualEvent::SuspenseEvent { .. } => false, } } } @@ -205,8 +195,6 @@ impl std::fmt::Debug for VirtualEvent { VirtualEvent::ToggleEvent(_) => "ToggleEvent", VirtualEvent::MouseEvent(_) => "MouseEvent", VirtualEvent::PointerEvent(_) => "PointerEvent", - VirtualEvent::GarbageCollection => "GarbageCollection", - VirtualEvent::ScheduledUpdate { .. } => "SetStateEvent", VirtualEvent::AsyncEvent { .. } => "AsyncEvent", VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent", }; diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 74fe330a6..29ecebe7d 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -260,24 +260,12 @@ impl<'src> SuspendedContext<'src> { #[derive(Clone, Copy)] pub struct NodeRef<'src, T: 'static>(&'src RefCell); -// impl NodeRef<'src, T> { -// fn set_ref(&self, new: Box) -// } -// impl<'a, T> std::ops::Deref for NodeRef<'a, T> { -// type Target = Option<&'a T>; - -// fn deref(&self) -> &Self::Target { -// // &self.node -// } -// } - pub fn use_node_ref(cx: Context

) -> NodeRef { cx.use_hook( |f| {}, |f| { // todo!() - // NodeRef {} }, |f| { // diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index c627496b2..e46de477d 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -40,6 +40,7 @@ pub(crate) mod innerlude { pub use crate::hooklist::*; pub use crate::hooks::*; pub use crate::nodes::*; + pub use crate::scheduler::*; pub use crate::scope::*; pub use crate::util::*; pub use crate::virtual_dom::*; @@ -67,6 +68,7 @@ pub mod heuristics; pub mod hooklist; pub mod hooks; pub mod nodes; +pub mod scheduler; pub mod scope; pub mod signals; pub mod util; diff --git a/packages/core/src/noderef.rs b/packages/core/src/noderef.rs index e69de29bb..16db8fe0d 100644 --- a/packages/core/src/noderef.rs +++ b/packages/core/src/noderef.rs @@ -0,0 +1,119 @@ +// let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + +// let mut garbage_list = scope.consume_garbage(); + +// let mut scopes_to_kill = Vec::new(); +// while let Some(node) = garbage_list.pop() { +// match &node.kind { +// VNodeKind::Text(_) => { +// self.shared.collect_garbage(node.direct_id()); +// } +// VNodeKind::Anchor(_) => { +// self.shared.collect_garbage(node.direct_id()); +// } +// VNodeKind::Suspended(_) => { +// self.shared.collect_garbage(node.direct_id()); +// } + +// VNodeKind::Element(el) => { +// self.shared.collect_garbage(node.direct_id()); +// for child in el.children { +// garbage_list.push(child); +// } +// } + +// VNodeKind::Fragment(frag) => { +// for child in frag.children { +// garbage_list.push(child); +// } +// } + +// VNodeKind::Component(comp) => { +// // TODO: run the hook destructors and then even delete the scope + +// let scope_id = comp.ass_scope.get().unwrap(); +// let scope = self.get_scope(scope_id).unwrap(); +// let root = scope.root(); +// garbage_list.push(root); +// scopes_to_kill.push(scope_id); +// } +// } +// } + +// for scope in scopes_to_kill { +// // oy kill em +// log::debug!("should be removing scope {:#?}", scope); +// } + +// // On the primary event queue, there is no batching, we take them off one-by-one +// let trigger = match receiver.try_next() { +// Ok(Some(trigger)) => trigger, +// _ => { +// // Continuously poll the future pool and the event receiver for work +// let mut tasks = self.shared.async_tasks.borrow_mut(); +// let tasks_tasks = tasks.next(); + +// // if the new event generates work more important than our current fiber, we should consider switching +// // only switch if it impacts different scopes. +// let mut ui_receiver = self.shared.ui_event_receiver.borrow_mut(); +// let ui_reciv_task = ui_receiver.next(); + +// // right now, this polling method will only catch batched set_states that don't get awaited. +// // However, in the future, we might be interested in batching set_states across await points +// let immediate_tasks = (); + +// futures_util::pin_mut!(tasks_tasks); +// futures_util::pin_mut!(ui_reciv_task); + +// // Poll the event receiver and the future pool for work +// // Abort early if our deadline has ran out +// let mut deadline = (&mut deadline_future).fuse(); + +// let trig = futures_util::select! { +// trigger = tasks_tasks => trigger, +// trigger = ui_reciv_task => trigger, + +// // abort if we're out of time +// _ = deadline => { return Ok(diff_machine.mutations); } +// }; + +// trig.unwrap() +// } +// }; + +// async fn select_next_event(&mut self) -> Option { +// let mut receiver = self.shared.task_receiver.borrow_mut(); + +// // drain the in-flight events so that we can sort them out with the current events +// while let Ok(Some(trigger)) = receiver.try_next() { +// log::info!("retrieving event from receiver"); +// let key = self.shared.make_trigger_key(&trigger); +// self.pending_events.insert(key, trigger); +// } + +// if self.pending_events.is_empty() { +// // Continuously poll the future pool and the event receiver for work +// let mut tasks = self.shared.async_tasks.borrow_mut(); +// let tasks_tasks = tasks.next(); + +// let mut receiver = self.shared.task_receiver.borrow_mut(); +// let reciv_task = receiver.next(); + +// futures_util::pin_mut!(tasks_tasks); +// futures_util::pin_mut!(reciv_task); + +// let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await { +// futures_util::future::Either::Left((trigger, _)) => trigger, +// futures_util::future::Either::Right((trigger, _)) => trigger, +// } +// .unwrap(); +// let key = self.shared.make_trigger_key(&trigger); +// self.pending_events.insert(key, trigger); +// } + +// // pop the most important event off +// let key = self.pending_events.keys().next().unwrap().clone(); +// let trigger = self.pending_events.remove(&key).unwrap(); + +// Some(trigger) +// } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs new file mode 100644 index 000000000..af327a56c --- /dev/null +++ b/packages/core/src/scheduler.rs @@ -0,0 +1,116 @@ +use std::any::Any; + +use std::any::TypeId; +use std::cell::{Ref, RefCell, RefMut}; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; +use std::pin::Pin; + +use crate::innerlude::*; + +/// The "Mutations" object holds the changes that need to be made to the DOM. +pub struct Mutations<'s> { + pub edits: Vec>, + pub noderefs: Vec>, +} + +impl<'s> Mutations<'s> { + pub fn new() -> Self { + let edits = Vec::new(); + let noderefs = Vec::new(); + Self { edits, noderefs } + } +} + +// refs are only assigned once +pub struct NodeRefMutation<'a> { + element: &'a mut Option>>, + element_id: ElementId, +} + +impl<'a> NodeRefMutation<'a> { + pub fn downcast_ref(&self) -> Option<&T> { + self.element + .as_ref() + .and_then(|f| f.get()) + .and_then(|f| f.downcast_ref::()) + } + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.element + .as_mut() + .and_then(|f| f.get_mut()) + .and_then(|f| f.downcast_mut::()) + } +} + +pub struct Scheduler { + current_priority: EventPriority, + + dirty_scopes: [HashSet; 3], + + fibers: Vec>, +} + +impl Scheduler { + pub fn new() -> Self { + Self { + fibers: Vec::new(), + + current_priority: EventPriority::Low, + + // low, medium, high + dirty_scopes: [HashSet::new(), HashSet::new(), HashSet::new()], + } + } + + pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { + // + + // generated_immediates + // .entry(dirty_scope) + // .and_modify(|cur_priority| { + // if *cur_priority > new_priority { + // *cur_priority = new_priority; + // } + // }) + // .or_insert_with(|| new_priority); + } + + pub fn has_work() {} + + pub fn progress_work(&mut self, machine: &mut DiffMachine) {} +} + +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub struct DirtyScope { + height: u32, + priority: EventPriority, + 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 Fiber<'a> { + // scopes that haven't been updated yet + pending_scopes: Vec, + + pending_nodes: Vec<*const VNode<'a>>, + + // WIP edits + edits: Vec>, + + started: bool, + + // a fiber is finished when no more scopes or nodes are pending + completed: bool, +} + +impl Fiber<'_> { + fn new() -> Self { + Self { + pending_scopes: Vec::new(), + pending_nodes: Vec::new(), + edits: Vec::new(), + started: false, + completed: false, + } + } +} diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 951b7280b..673944df2 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -193,51 +193,24 @@ impl Scope { } // A safe wrapper around calling listeners - // calling listeners will invalidate the list of listeners - // The listener list will be completely drained because the next frame will write over previous listeners - pub(crate) fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> { - let EventTrigger { - real_node_id, - event, - .. - } = trigger; - - if let &VirtualEvent::AsyncEvent { .. } = &event { - log::info!("arrived a fiber event"); - return Ok(()); - } - - log::debug!( - "There are {:?} listeners associated with this scope {:#?}", - self.listeners.borrow().len(), - self.our_arena_idx - ); - + // + // + pub(crate) fn call_listener(&mut self, event: VirtualEvent, element: ElementId) -> Result<()> { let listners = self.listeners.borrow_mut(); let raw_listener = listners.iter().find(|lis| { let search = unsafe { &***lis }; let search_id = search.mounted_node.get(); - log::info!( - "searching listener {:#?} for real {:?}", - search_id, - real_node_id - ); - match (real_node_id, search_id) { - (Some(e), Some(search_id)) => search_id == e, - _ => false, + // this assumes the node might not be mounted - should we assume that though? + match search_id.map(|f| f == element) { + Some(same) => same, + None => false, } }); if let Some(raw_listener) = raw_listener { let listener = unsafe { &**raw_listener }; - - // log::info!( - // "calling listener {:?}, {:?}", - // listener.event, - // // listener.scope - // ); let mut cb = listener.callback.borrow_mut(); if let Some(cb) = cb.as_mut() { (cb)(event); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 76b323568..1745d4740 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -18,18 +18,17 @@ //! //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. -#![allow(unreachable_code)] use futures_util::{Future, StreamExt}; use fxhash::FxHashMap; use crate::hooks::{SuspendedContext, SuspenseHook}; -use crate::{arena::SharedResources, innerlude::*}; +use crate::innerlude::*; use std::any::Any; use std::any::TypeId; use std::cell::{Ref, RefCell, RefMut}; -use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; use std::pin::Pin; /// An integrated virtual node system that progresses events and diffs UI trees. @@ -53,7 +52,7 @@ pub struct VirtualDom { /// Should always be the first (gen=0, id=0) base_scope: ScopeId, - active_fibers: Vec>, + scheduler: Scheduler, // for managing the props that were used to create the dom #[doc(hidden)] @@ -144,7 +143,7 @@ impl VirtualDom { base_scope, _root_props: root_props, shared: components, - active_fibers: Vec::new(), + scheduler: Scheduler::new(), _root_prop_type: TypeId::of::

(), } } @@ -218,43 +217,6 @@ impl VirtualDom { Ok(edits) } - // async fn select_next_event(&mut self) -> Option { - // let mut receiver = self.shared.task_receiver.borrow_mut(); - - // // drain the in-flight events so that we can sort them out with the current events - // while let Ok(Some(trigger)) = receiver.try_next() { - // log::info!("retrieving event from receiver"); - // let key = self.shared.make_trigger_key(&trigger); - // self.pending_events.insert(key, trigger); - // } - - // if self.pending_events.is_empty() { - // // Continuously poll the future pool and the event receiver for work - // let mut tasks = self.shared.async_tasks.borrow_mut(); - // let tasks_tasks = tasks.next(); - - // let mut receiver = self.shared.task_receiver.borrow_mut(); - // let reciv_task = receiver.next(); - - // futures_util::pin_mut!(tasks_tasks); - // futures_util::pin_mut!(reciv_task); - - // let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await { - // futures_util::future::Either::Left((trigger, _)) => trigger, - // futures_util::future::Either::Right((trigger, _)) => trigger, - // } - // .unwrap(); - // let key = self.shared.make_trigger_key(&trigger); - // self.pending_events.insert(key, trigger); - // } - - // // pop the most important event off - // let key = self.pending_events.keys().next().unwrap().clone(); - // let trigger = self.pending_events.remove(&key).unwrap(); - - // Some(trigger) - // } - /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. /// /// This method will not wait for any suspended tasks, completely skipping over @@ -320,24 +282,38 @@ impl VirtualDom { &'s mut self, mut deadline: impl Future, ) -> Result> { - // Configure our deadline - use futures_util::FutureExt; - let mut deadline_future = deadline.boxed_local(); - - let is_ready = || -> bool { (&mut deadline_future).now_or_never().is_some() }; - - let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); - /* Strategy: - 1. Check if there are any events in the receiver. - 2. If there are, process them and create a new fiber. - 3. If there are no events, then choose a fiber to work on. + 1. Check if there are any UI events in the receiver. + 2. If there are, run the listener and then mark the dirty nodes + 3. If there are dirty nodes to be progressed, do so. + 4. Poll the task queue to see if we can create more dirty scopes. + 5. Resume any current in-flight work if there is some. + 6. While the deadline is not met, progress work, periodically checking the deadline. + + + How to choose work: + - When a scope is marked as dirty, it is given a priority. + - If a dirty scope chains (borrowed) into children, mark those as dirty as well. + - When the work loop starts, work on the highest priority scopes first. + - Work by priority, choosing to pause in-flight work if higher-priority work is ready. + + + 4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached. 5. While processing a fiber, periodically check if we're out of time 6. If our deadling is reached, then commit our edits to the realdom 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) + + // 1. Check if there are any events in the receiver. + // 2. If there are, process them and create a new fiber. + // 3. If there are no events, then choose a fiber to work on. + // 4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached. + // 5. While processing a fiber, periodically check if we're out of time + // 6. If our deadling is reached, then commit our edits to the realdom + // 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) + We slice fibers based on time. Each batch of events between frames is its own fiber. This is the simplest way to conceptualize what *is* or *isn't* a fiber. IE if a bunch of events occur during a time slice, they all get batched together as a single operation of "dirty" scopes. @@ -348,101 +324,39 @@ impl VirtualDom { and listeners hold references to hook data, it is wrong to run a scope that is already being diffed. */ - // 1. Consume any pending events and create new fibers - let mut receiver = self.shared.task_receiver.borrow_mut(); + let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); - let current_fiber = { - // - self.active_fibers.get_mut(0).unwrap() - }; + // 1. Drain the existing immediates. + // + // These are generated by async tasks that we never got a chance to finish. + // All of these get scheduled with the lowest priority. + while let Ok(Some(dirty_scope)) = self.shared.immediate_receiver.borrow_mut().try_next() { + self.scheduler + .add_dirty_scope(dirty_scope, EventPriority::Low); + } - // On the primary event queue, there is no batching. - let mut trigger = { - match receiver.try_next() { - Ok(Some(trigger)) => trigger, - _ => { - // Continuously poll the future pool and the event receiver for work - let mut tasks = self.shared.async_tasks.borrow_mut(); - let tasks_tasks = tasks.next(); + // 2. Drain the event queue, calling whatever listeners need to be called + // + while let Ok(Some(trigger)) = self.shared.ui_event_receiver.borrow_mut().try_next() { + match &trigger.event { + VirtualEvent::AsyncEvent { .. } => {} - // if the new event generates work more important than our current fiber, we should consider switching - // only switch if it impacts different scopes. - let mut receiver = self.shared.task_receiver.borrow_mut(); - let reciv_task = receiver.next(); + // This suspense system works, but it's not the most elegant solution. + // TODO: Replace this system + VirtualEvent::SuspenseEvent { hook_idx, domnode } => { + // Safety: this handler is the only thing that can mutate shared items at this moment in tim + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - futures_util::pin_mut!(tasks_tasks); - futures_util::pin_mut!(reciv_task); + // safety: we are sure that there are no other references to the inner content of suspense hooks + let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - // Poll the event receiver and the future pool for work - // Abort early if our deadline has ran out - let mut deadline = (&mut deadline_future).fuse(); + let cx = Context { scope, props: &() }; + let scx = SuspendedContext { inner: cx }; - let trig = futures_util::select! { - trigger = tasks_tasks => trigger, - trigger = reciv_task => trigger, - _ = deadline => { return Ok(diff_machine.mutations); } - }; + // generate the new node! + let nodes: Option = (&hook.callback)(scx); - trig.unwrap() - } - } - }; - - // since the last time we were ran with a deadline, we've accumulated many updates - // IE a button was clicked twice, or a scroll trigger was fired twice. - // We consider the button a event to be a function of the current state, which means we can batch many updates - // together. - - match &trigger.event { - // If any input event is received, then we need to create a new fiber - VirtualEvent::ClipboardEvent(_) - | VirtualEvent::CompositionEvent(_) - | VirtualEvent::KeyboardEvent(_) - | VirtualEvent::FocusEvent(_) - | VirtualEvent::FormEvent(_) - | VirtualEvent::SelectionEvent(_) - | VirtualEvent::TouchEvent(_) - | VirtualEvent::UIEvent(_) - | VirtualEvent::WheelEvent(_) - | VirtualEvent::MediaEvent(_) - | VirtualEvent::AnimationEvent(_) - | VirtualEvent::TransitionEvent(_) - | VirtualEvent::ToggleEvent(_) - | VirtualEvent::MouseEvent(_) - | VirtualEvent::PointerEvent(_) => { - // - if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { - scope.call_listener(trigger)?; - } - } - - VirtualEvent::AsyncEvent { .. } => while let Ok(Some(event)) = receiver.try_next() {}, - - // These shouldn't normally be received, but if they are, it's done because some task set state manually - // Instead of processing it serially, - // We will batch all the scheduled updates together in one go. - VirtualEvent::ScheduledUpdate { .. } => {} - - // Suspense Events! A component's suspended node is updated - VirtualEvent::SuspenseEvent { hook_idx, domnode } => { - // Safety: this handler is the only thing that can mutate shared items at this moment in tim - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - // safety: we are sure that there are no other references to the inner content of suspense hooks - let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - - let cx = Context { scope, props: &() }; - let scx = SuspendedContext { inner: cx }; - - // generate the new node! - let nodes: Option = (&hook.callback)(scx); - match nodes { - None => { - log::warn!( - "Suspense event came through, but there were no generated nodes >:(." - ); - } - Some(nodes) => { + if let Some(nodes) = nodes { // allocate inside the finished frame - not the WIP frame let nodes = scope.frames.finished_frame().bump.alloc(nodes); @@ -455,74 +369,65 @@ impl VirtualDom { // replace the placeholder with the new nodes we just pushed on the stack diff_machine.edit_replace_with(1, meta.added_to_stack); - } - } - } - - // Collecting garabge is not currently interruptible. - // - // In the future, it could be though - VirtualEvent::GarbageCollection => { - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - let mut garbage_list = scope.consume_garbage(); - - let mut scopes_to_kill = Vec::new(); - while let Some(node) = garbage_list.pop() { - match &node.kind { - VNodeKind::Text(_) => { - self.shared.collect_garbage(node.direct_id()); - } - VNodeKind::Anchor(_) => { - self.shared.collect_garbage(node.direct_id()); - } - VNodeKind::Suspended(_) => { - self.shared.collect_garbage(node.direct_id()); - } - - VNodeKind::Element(el) => { - self.shared.collect_garbage(node.direct_id()); - for child in el.children { - garbage_list.push(child); - } - } - - VNodeKind::Fragment(frag) => { - for child in frag.children { - garbage_list.push(child); - } - } - - VNodeKind::Component(comp) => { - // TODO: run the hook destructors and then even delete the scope - - let scope_id = comp.ass_scope.get().unwrap(); - let scope = self.get_scope(scope_id).unwrap(); - let root = scope.root(); - garbage_list.push(root); - scopes_to_kill.push(scope_id); - } + } else { + log::warn!( + "Suspense event came through, but there were no generated nodes >:(." + ); } } - for scope in scopes_to_kill { - // oy kill em - log::debug!("should be removing scope {:#?}", scope); + VirtualEvent::ClipboardEvent(_) + | VirtualEvent::CompositionEvent(_) + | VirtualEvent::KeyboardEvent(_) + | VirtualEvent::FocusEvent(_) + | VirtualEvent::FormEvent(_) + | VirtualEvent::SelectionEvent(_) + | VirtualEvent::TouchEvent(_) + | VirtualEvent::UIEvent(_) + | VirtualEvent::WheelEvent(_) + | VirtualEvent::MediaEvent(_) + | VirtualEvent::AnimationEvent(_) + | VirtualEvent::TransitionEvent(_) + | VirtualEvent::ToggleEvent(_) + | VirtualEvent::MouseEvent(_) + | VirtualEvent::PointerEvent(_) => { + if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { + if let Some(element) = trigger.real_node_id { + scope.call_listener(trigger.event, element)?; + + // Drain the immediates into the dirty scopes, setting the appropiate priorities + while let Ok(Some(dirty_scope)) = + self.shared.immediate_receiver.borrow_mut().try_next() + { + self.scheduler + .add_dirty_scope(dirty_scope, trigger.priority) + } + } + } } } } - // while !deadline() { - // let mut receiver = self.shared.task_receiver.borrow_mut(); + // 3. Work through the fibers, and wait for any future work to be ready - // // no messages to receive, just work on the fiber - // } + // Configure our deadline + use futures_util::FutureExt; + let mut deadline_future = deadline.boxed_local(); + let mut is_ready = || -> bool { (&mut deadline_future).now_or_never().is_some() }; + + loop { + if is_ready() { + break; + } + + self.scheduler + } Ok(diff_machine.mutations) } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - self.shared.task_sender.clone() + self.shared.ui_event_sender.clone() } fn get_scope_mut(&mut self, id: ScopeId) -> Option<&mut Scope> { @@ -534,67 +439,3 @@ impl VirtualDom { // These impls are actually wrong. The DOM needs to have a mutex implemented. unsafe impl Sync for VirtualDom {} unsafe impl Send for VirtualDom {} - -struct Fiber<'a> { - // scopes that haven't been updated yet - pending_scopes: Vec, - - pending_nodes: Vec<*const VNode<'a>>, - - // WIP edits - edits: Vec>, - - started: bool, - - completed: bool, -} - -impl Fiber<'_> { - fn new() -> Self { - Self { - pending_scopes: Vec::new(), - pending_nodes: Vec::new(), - edits: Vec::new(), - started: false, - completed: false, - } - } -} - -/// The "Mutations" object holds the changes that need to be made to the DOM. -pub struct Mutations<'s> { - // todo: apply node refs - // todo: apply effects - pub edits: Vec>, - - pub noderefs: Vec>, -} - -impl<'s> Mutations<'s> { - pub fn new() -> Self { - let edits = Vec::new(); - let noderefs = Vec::new(); - Self { edits, noderefs } - } -} - -// refs are only assigned once -pub struct NodeRefMutation<'a> { - element: &'a mut Option>>, - element_id: ElementId, -} - -impl<'a> NodeRefMutation<'a> { - pub fn downcast_ref(&self) -> Option<&T> { - self.element - .as_ref() - .and_then(|f| f.get()) - .and_then(|f| f.downcast_ref::()) - } - pub fn downcast_mut(&mut self) -> Option<&mut T> { - self.element - .as_mut() - .and_then(|f| f.get_mut()) - .and_then(|f| f.downcast_mut::()) - } -} From 97745c6a7f2094deca32c7a1c30890853067848e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 10 Aug 2021 01:21:13 -0400 Subject: [PATCH 04/53] wip: got the structure figured out! --- packages/core/src/scheduler.rs | 142 ++++++++++++++++++++++++- packages/core/src/virtual_dom.rs | 173 +++++++++++++++++-------------- 2 files changed, 235 insertions(+), 80 deletions(-) diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index af327a56c..99b5fd61b 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -5,6 +5,8 @@ use std::cell::{Ref, RefCell, RefMut}; use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; use std::pin::Pin; +use futures_util::Future; + use crate::innerlude::*; /// The "Mutations" object holds the changes that need to be made to the DOM. @@ -45,16 +47,33 @@ impl<'a> NodeRefMutation<'a> { pub struct Scheduler { current_priority: EventPriority, + pending_events: VecDeque, + pending_immediates: VecDeque, + pending_tasks: VecDeque, + + garbage_scopes: HashSet, + + shared: SharedResources, + dirty_scopes: [HashSet; 3], fibers: Vec>, } impl Scheduler { - pub fn new() -> Self { + pub fn new(shared: SharedResources) -> Self { Self { + shared, + + // a storage for our receiver to dump into + pending_events: VecDeque::new(), + pending_immediates: VecDeque::new(), + pending_tasks: VecDeque::new(), + fibers: Vec::new(), + garbage_scopes: HashSet::new(), + current_priority: EventPriority::Low, // low, medium, high @@ -62,6 +81,64 @@ impl Scheduler { } } + // channels don't have these methods, so we just implement our own wrapper + pub fn next_event(&mut self) -> Option { + // pop the last event off the internal queue + self.pending_events.pop_back().or_else(|| { + self.shared + .ui_event_receiver + .borrow_mut() + .try_next() + .ok() + .and_then(|f| f) + }) + } + + // waits for a trigger, canceling early if the deadline is reached + // returns true if the deadline was reached + pub async fn wait_for_any_trigger(&mut self, deadline: &mut impl Future) -> bool { + todo!() + // match raw_trigger { + // Ok(Some(trigger)) => trigger, + // _ => { + // // nothing to do - let's clean up any garbage we might have + // self.scheduler.clean_up_garbage(); + + // // Continuously poll the future pool and the event receiver for work + // let mut tasks = self.shared.async_tasks.borrow_mut(); + // let tasks_tasks = tasks.next(); + + // // if the new event generates work more important than our current fiber, we should consider switching + // // only switch if it impacts different scopes. + // let mut ui_receiver = self.shared.ui_event_receiver.borrow_mut(); + // let ui_reciv_task = ui_receiver.next(); + + // // right now, this polling method will only catch batched set_states that don't get awaited. + // // However, in the future, we might be interested in batching set_states across await points + // let immediate_tasks = (); + + // futures_util::pin_mut!(tasks_tasks); + // futures_util::pin_mut!(ui_reciv_task); + + // // Poll the event receiver and the future pool for work + // // Abort early if our deadline has ran out + // let mut deadline = (&mut deadline_future).fuse(); + + // let trig = futures_util::select! { + // trigger = tasks_tasks => trigger, + // trigger = ui_reciv_task => trigger, + + // // abort if we're out of time + // _ = deadline => { return Ok(Mutations::new()); } + // // _ = deadline => { return Ok(diff_machine.mutations); } + // }; + + // trig.unwrap() + // } + // }; + // + } + pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { // @@ -75,9 +152,68 @@ impl Scheduler { // .or_insert_with(|| new_priority); } - pub fn has_work() {} + pub fn has_work(&self) -> bool { + true + } - pub fn progress_work(&mut self, machine: &mut DiffMachine) {} + // returns true if the deadline is reached + // returns false if the deadline is not reached + pub fn progress_work( + &mut self, + machine: &mut DiffMachine, + is_deadline_reached: &mut impl FnMut() -> bool, + ) -> bool { + todo!() + } + + pub fn clean_up_garbage(&mut self) { + let mut scopes_to_kill = Vec::new(); + + let mut garbage_list = Vec::new(); + for scope in self.garbage_scopes.drain() { + let scope = self.shared.get_scope_mut(scope).unwrap(); + for node in scope.consume_garbage() { + garbage_list.push(node); + } + + while let Some(node) = garbage_list.pop() { + match &node.kind { + VNodeKind::Text(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Anchor(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Suspended(_) => { + self.shared.collect_garbage(node.direct_id()); + } + + VNodeKind::Element(el) => { + self.shared.collect_garbage(node.direct_id()); + for child in el.children { + garbage_list.push(child); + } + } + + VNodeKind::Fragment(frag) => { + for child in frag.children { + garbage_list.push(child); + } + } + + VNodeKind::Component(comp) => { + // TODO: run the hook destructors and then even delete the scope + + let scope_id = comp.ass_scope.get().unwrap(); + let scope = self.shared.get_scope(scope_id).unwrap(); + let root = scope.root(); + garbage_list.push(root); + scopes_to_kill.push(scope_id); + } + } + } + } + } } #[derive(PartialEq, Eq, Copy, Clone, Debug)] diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1745d4740..8b72a6a7d 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -142,8 +142,8 @@ impl VirtualDom { Self { base_scope, _root_props: root_props, + scheduler: Scheduler::new(components.clone()), shared: components, - scheduler: Scheduler::new(), _root_prop_type: TypeId::of::

(), } } @@ -324,106 +324,125 @@ impl VirtualDom { and listeners hold references to hook data, it is wrong to run a scope that is already being diffed. */ - let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); + // let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); - // 1. Drain the existing immediates. - // - // These are generated by async tasks that we never got a chance to finish. - // All of these get scheduled with the lowest priority. - while let Ok(Some(dirty_scope)) = self.shared.immediate_receiver.borrow_mut().try_next() { - self.scheduler - .add_dirty_scope(dirty_scope, EventPriority::Low); - } + // Configure our deadline + use futures_util::FutureExt; + let mut deadline_future = deadline.shared(); - // 2. Drain the event queue, calling whatever listeners need to be called - // - while let Ok(Some(trigger)) = self.shared.ui_event_receiver.borrow_mut().try_next() { - match &trigger.event { - VirtualEvent::AsyncEvent { .. } => {} + let mut is_ready_deadline = deadline_future.clone(); + let mut is_ready = || -> bool { (&mut is_ready_deadline).now_or_never().is_some() }; - // This suspense system works, but it's not the most elegant solution. - // TODO: Replace this system - VirtualEvent::SuspenseEvent { hook_idx, domnode } => { - // Safety: this handler is the only thing that can mutate shared items at this moment in tim - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + loop { + // 1. Drain the existing immediates. + // + // These are generated by async tasks that we never got a chance to finish. + // All of these get scheduled with the lowest priority. + while let Ok(Some(dirty_scope)) = self.shared.immediate_receiver.borrow_mut().try_next() + { + self.scheduler + .add_dirty_scope(dirty_scope, EventPriority::Low); + } - // safety: we are sure that there are no other references to the inner content of suspense hooks - let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); + // 2. Drain the event queue, calling whatever listeners need to be called + // + // First, check if there's any set_states in the queue that we can mark as low priority + // Next, check the UI event receiver so we can mark those scopes as medium/high priority + // Next, work on any fibers. + // Once the fiber work is finished + // + // + // + // - let cx = Context { scope, props: &() }; - let scx = SuspendedContext { inner: cx }; + while let Some(trigger) = self.scheduler.next_event() { + match &trigger.event { + VirtualEvent::AsyncEvent { .. } => {} - // generate the new node! - let nodes: Option = (&hook.callback)(scx); + // This suspense system works, but it's not the most elegant solution. + // TODO: Replace this system + VirtualEvent::SuspenseEvent { hook_idx, domnode } => { + todo!(); + // // Safety: this handler is the only thing that can mutate shared items at this moment in tim + // let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - if let Some(nodes) = nodes { - // allocate inside the finished frame - not the WIP frame - let nodes = scope.frames.finished_frame().bump.alloc(nodes); + // // safety: we are sure that there are no other references to the inner content of suspense hooks + // let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - // push the old node's root onto the stack - let real_id = domnode.get().ok_or(Error::NotMounted)?; - diff_machine.edit_push_root(real_id); + // let cx = Context { scope, props: &() }; + // let scx = SuspendedContext { inner: cx }; - // push these new nodes onto the diff machines stack - let meta = diff_machine.create_vnode(&*nodes); + // // generate the new node! + // let nodes: Option = (&hook.callback)(scx); - // replace the placeholder with the new nodes we just pushed on the stack - diff_machine.edit_replace_with(1, meta.added_to_stack); - } else { - log::warn!( - "Suspense event came through, but there were no generated nodes >:(." - ); + // if let Some(nodes) = nodes { + // // allocate inside the finished frame - not the WIP frame + // let nodes = scope.frames.finished_frame().bump.alloc(nodes); + + // // push the old node's root onto the stack + // let real_id = domnode.get().ok_or(Error::NotMounted)?; + // diff_machine.edit_push_root(real_id); + + // // push these new nodes onto the diff machines stack + // let meta = diff_machine.create_vnode(&*nodes); + + // // replace the placeholder with the new nodes we just pushed on the stack + // diff_machine.edit_replace_with(1, meta.added_to_stack); + // } else { + // log::warn!( + // "Suspense event came through, but there were no generated nodes >:(." + // ); + // } } - } - VirtualEvent::ClipboardEvent(_) - | VirtualEvent::CompositionEvent(_) - | VirtualEvent::KeyboardEvent(_) - | VirtualEvent::FocusEvent(_) - | VirtualEvent::FormEvent(_) - | VirtualEvent::SelectionEvent(_) - | VirtualEvent::TouchEvent(_) - | VirtualEvent::UIEvent(_) - | VirtualEvent::WheelEvent(_) - | VirtualEvent::MediaEvent(_) - | VirtualEvent::AnimationEvent(_) - | VirtualEvent::TransitionEvent(_) - | VirtualEvent::ToggleEvent(_) - | VirtualEvent::MouseEvent(_) - | VirtualEvent::PointerEvent(_) => { - if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { - if let Some(element) = trigger.real_node_id { - scope.call_listener(trigger.event, element)?; + VirtualEvent::ClipboardEvent(_) + | VirtualEvent::CompositionEvent(_) + | VirtualEvent::KeyboardEvent(_) + | VirtualEvent::FocusEvent(_) + | VirtualEvent::FormEvent(_) + | VirtualEvent::SelectionEvent(_) + | VirtualEvent::TouchEvent(_) + | VirtualEvent::UIEvent(_) + | VirtualEvent::WheelEvent(_) + | VirtualEvent::MediaEvent(_) + | VirtualEvent::AnimationEvent(_) + | VirtualEvent::TransitionEvent(_) + | VirtualEvent::ToggleEvent(_) + | VirtualEvent::MouseEvent(_) + | VirtualEvent::PointerEvent(_) => { + if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { + if let Some(element) = trigger.real_node_id { + scope.call_listener(trigger.event, element)?; - // Drain the immediates into the dirty scopes, setting the appropiate priorities - while let Ok(Some(dirty_scope)) = - self.shared.immediate_receiver.borrow_mut().try_next() - { - self.scheduler - .add_dirty_scope(dirty_scope, trigger.priority) + // Drain the immediates into the dirty scopes, setting the appropiate priorities + while let Ok(Some(dirty_scope)) = + self.shared.immediate_receiver.borrow_mut().try_next() + { + self.scheduler + .add_dirty_scope(dirty_scope, trigger.priority) + } } } } } } - } - // 3. Work through the fibers, and wait for any future work to be ready + // 3. Work through the fibers, and wait for any future work to be ready + let mut machine = DiffMachine::new_headless(&self.shared); + self.scheduler.progress_work(&mut machine, &mut is_ready); - // Configure our deadline - use futures_util::FutureExt; - let mut deadline_future = deadline.boxed_local(); - let mut is_ready = || -> bool { (&mut deadline_future).now_or_never().is_some() }; - - loop { - if is_ready() { + // 4. Wait for any new triggers before looping again + if self + .scheduler + .wait_for_any_trigger(&mut deadline_future) + .await + { break; } - - self.scheduler } - Ok(diff_machine.mutations) + todo!() + // Ok(diff_machine.mutations) } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { From 5965bee1d143ee50ed24d4bde5176e057bce4363 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 10 Aug 2021 01:38:58 -0400 Subject: [PATCH 05/53] wip: more work on deadline, support immediates --- packages/core/src/virtual_dom.rs | 171 +++++++++++++------------------ 1 file changed, 71 insertions(+), 100 deletions(-) diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 8b72a6a7d..152dc1a7b 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -18,18 +18,12 @@ //! //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. -use futures_util::{Future, StreamExt}; -use fxhash::FxHashMap; - -use crate::hooks::{SuspendedContext, SuspenseHook}; use crate::innerlude::*; - -use std::any::Any; - -use std::any::TypeId; -use std::cell::{Ref, RefCell, RefMut}; -use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; -use std::pin::Pin; +use futures_util::Future; +use std::{ + any::{Any, TypeId}, + pin::Pin, +}; /// An integrated virtual node system that progresses events and diffs UI trees. /// Differences are converted into patches which a renderer can use to draw the UI. @@ -63,69 +57,50 @@ pub struct VirtualDom { } impl VirtualDom { - /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component. + /// Create a new VirtualDOM with a component that does not have special props. /// - /// This means that the root component must either consumes its own context, or statics are used to generate the page. - /// The root component can access things like routing in its context. + /// # Description /// - /// As an end-user, you'll want to use the Renderer's "new" method instead of this method. - /// Directly creating the VirtualDOM is only useful when implementing a new renderer. - /// - /// - /// ```ignore - /// // Directly from a closure - /// - /// let dom = VirtualDom::new(|cx| cx.render(rsx!{ div {"hello world"} })); - /// - /// // or pass in... - /// - /// let root = |cx| { - /// cx.render(rsx!{ - /// div {"hello world"} - /// }) - /// } - /// let dom = VirtualDom::new(root); - /// - /// // or directly from a fn - /// - /// fn Example(cx: Context<()>) -> DomTree { - /// cx.render(rsx!{ div{"hello world"} }) - /// } - /// - /// let dom = VirtualDom::new(Example); - /// ``` - pub fn new(root: FC<()>) -> Self { - Self::new_with_props(root, ()) - } - - /// Start a new VirtualDom instance with a dependent cx. /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders. /// /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive /// to toss out the entire tree. /// - /// ```ignore - /// // Directly from a closure /// - /// let dom = VirtualDom::new(|cx| cx.render(rsx!{ div {"hello world"} })); - /// - /// // or pass in... - /// - /// let root = |cx| { - /// cx.render(rsx!{ - /// div {"hello world"} - /// }) - /// } - /// let dom = VirtualDom::new(root); - /// - /// // or directly from a fn - /// - /// fn Example(cx: Context, props: &SomeProps) -> VNode { + /// # Example + /// ``` + /// fn Example(cx: Context) -> VNode { /// cx.render(rsx!{ div{"hello world"} }) /// } /// /// let dom = VirtualDom::new(Example); /// ``` + /// + /// Note: the VirtualDOM is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. + pub fn new(root: FC<()>) -> Self { + Self::new_with_props(root, ()) + } + + /// Create a new VirtualDOM with the given properties for the root component. + /// + /// # Description + /// + /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders. + /// + /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive + /// to toss out the entire tree. + /// + /// + /// # Example + /// ``` + /// fn Example(cx: Context) -> VNode { + /// cx.render(rsx!{ div{"hello world"} }) + /// } + /// + /// let dom = VirtualDom::new(Example); + /// ``` + /// + /// Note: the VirtualDOM is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. pub fn new_with_props(root: FC

, root_props: P) -> Self { let components = SharedResources::new(); @@ -150,7 +125,7 @@ impl VirtualDom { pub fn launch_in_place(root: FC<()>) -> Self { let mut s = Self::new(root); - s.rebuild_in_place().unwrap(); + s.rebuild().unwrap(); s } @@ -158,7 +133,7 @@ impl VirtualDom { /// pub fn launch_with_props_in_place(root: FC

, root_props: P) -> Self { let mut s = Self::new_with_props(root, root_props); - s.rebuild_in_place().unwrap(); + s.rebuild().unwrap(); s } @@ -170,23 +145,6 @@ impl VirtualDom { unsafe { self.shared.get_scope(id) } } - /// Rebuilds the VirtualDOM from scratch, but uses a "dummy" RealDom. - /// - /// Used in contexts where a real copy of the structure doesn't matter, and the VirtualDOM is the source of truth. - /// - /// ## Why? - /// - /// This method uses the `DebugDom` under the hood - essentially making the VirtualDOM's diffing patches a "no-op". - /// - /// SSR takes advantage of this by using Dioxus itself as the source of truth, and rendering from the tree directly. - pub fn rebuild_in_place(&mut self) -> Result> { - todo!(); - // let mut realdom = DebugDom::new(); - // let mut edits = Vec::new(); - // self.rebuild(&mut realdom, &mut edits)?; - // Ok(edits) - } - /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch /// /// The diff machine expects the RealDom's stack to be the root of the application @@ -219,11 +177,13 @@ impl VirtualDom { /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. /// - /// This method will not wait for any suspended tasks, completely skipping over + /// This method will not wait for any suspended nodes to complete. pub fn run_immediate<'s>(&'s mut self) -> Result> { - // - - todo!() + 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") } /// Runs the virtualdom with no time limit. @@ -281,6 +241,28 @@ impl VirtualDom { pub async fn run_with_deadline<'s>( &'s mut self, mut deadline: impl Future, + ) -> Result> { + // let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); + + // Configure our deadline + use futures_util::FutureExt; + let mut 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() }; + + todo!() + // Ok(diff_machine.mutations) + } + + /// 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, + mut deadline_future: impl Future, + mut is_ready: &mut impl FnMut() -> bool, ) -> Result> { /* Strategy: @@ -324,15 +306,6 @@ impl VirtualDom { and listeners hold references to hook data, it is wrong to run a scope that is already being diffed. */ - // let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); - - // Configure our deadline - use futures_util::FutureExt; - let mut 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() }; - loop { // 1. Drain the existing immediates. // @@ -427,9 +400,13 @@ impl VirtualDom { } } + // todo: for these, make these methods return results where deadlinereached is an error type + // 3. Work through the fibers, and wait for any future work to be ready let mut machine = DiffMachine::new_headless(&self.shared); - self.scheduler.progress_work(&mut machine, &mut is_ready); + if self.scheduler.progress_work(&mut machine, &mut is_ready) { + break; + } // 4. Wait for any new triggers before looping again if self @@ -440,18 +417,12 @@ impl VirtualDom { break; } } - todo!() - // Ok(diff_machine.mutations) } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { self.shared.ui_event_sender.clone() } - - fn get_scope_mut(&mut self, id: ScopeId) -> Option<&mut Scope> { - unsafe { self.shared.get_scope_mut(id) } - } } // TODO! From 9d14faf62c654e6958b5e5dc444374146977f52c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 10 Aug 2021 12:16:49 -0400 Subject: [PATCH 06/53] wip: scheduler skeleton --- packages/core/src/scheduler.rs | 222 ++++++++++++++++++++++++++++++- packages/core/src/virtual_dom.rs | 175 +++--------------------- 2 files changed, 239 insertions(+), 158 deletions(-) diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 99b5fd61b..07030a2bc 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -60,6 +60,11 @@ pub struct Scheduler { fibers: Vec>, } +pub enum FiberResult<'a> { + Done(Mutations<'a>), + Interrupted, +} + impl Scheduler { pub fn new(shared: SharedResources) -> Self { Self { @@ -90,13 +95,77 @@ impl Scheduler { .borrow_mut() .try_next() .ok() - .and_then(|f| f) + .flatten() }) } + pub fn manually_poll_channels(&mut self) { + todo!() + } + + pub fn consume_pending_events(&mut self) {} + + // nothing to do, no events on channels, no work + pub fn is_idle(&self) -> bool { + !self.has_work() && self.has_pending_events() + } + + pub fn has_pending_events(&self) -> bool { + todo!() + } + + pub fn has_work(&self) -> bool { + todo!() + } + + pub fn work_with_deadline( + &mut self, + deadline: &mut impl Future, + is_deadline_reached: &mut impl FnMut() -> bool, + ) -> FiberResult { + todo!() + } + // waits for a trigger, canceling early if the deadline is reached // returns true if the deadline was reached + // does not return the trigger, but caches it in the scheduler pub async fn wait_for_any_trigger(&mut self, deadline: &mut impl Future) -> bool { + // poll_all_channels() + // if no events and no work { + // wait for events, cache them + // } + + // for event in pending_evenst { + // mark_dirty_scopes_with_priorities + // } + + // // level shift the priority + // match current_priority { + // high -> finish high queue + // medium -> if work in high { set_current_level_high } + // low -> if work in high { set_current_level_high } else if work in medium { set_current_level_medium } + // } + + // let mut fiber = load_priority_fiber(); + + // fiber.load_work(high_priority_work); + + // fiber.progress_with_deadline(deadline); + + // if fiber.finished() { + // if work_pending_for_priority(fiber.priority()) { + // fiber.load_work(priority_work) + // } else { + // shift_level_down() + // } + // } + + // load current events + // if work queue is empty { + // wait for events + // load current events + // } + todo!() // match raw_trigger { // Ok(Some(trigger)) => trigger, @@ -152,10 +221,6 @@ impl Scheduler { // .or_insert_with(|| new_priority); } - pub fn has_work(&self) -> bool { - true - } - // returns true if the deadline is reached // returns false if the deadline is not reached pub fn progress_work( @@ -250,3 +315,150 @@ impl Fiber<'_> { } } } + +/* +Strategy: +1. Check if there are any UI events in the receiver. +2. If there are, run the listener and then mark the dirty nodes +3. If there are dirty nodes to be progressed, do so. +4. Poll the task queue to see if we can create more dirty scopes. +5. Resume any current in-flight work if there is some. +6. While the deadline is not met, progress work, periodically checking the deadline. + + +How to choose work: +- When a scope is marked as dirty, it is given a priority. +- If a dirty scope chains (borrowed) into children, mark those as dirty as well. +- When the work loop starts, work on the highest priority scopes first. +- Work by priority, choosing to pause in-flight work if higher-priority work is ready. + + + +4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached. +5. While processing a fiber, periodically check if we're out of time +6. If our deadling is reached, then commit our edits to the realdom +7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) + + +// 1. Check if there are any events in the receiver. +// 2. If there are, process them and create a new fiber. +// 3. If there are no events, then choose a fiber to work on. +// 4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached. +// 5. While processing a fiber, periodically check if we're out of time +// 6. If our deadling is reached, then commit our edits to the realdom +// 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) + +We slice fibers based on time. Each batch of events between frames is its own fiber. This is the simplest way +to conceptualize what *is* or *isn't* a fiber. IE if a bunch of events occur during a time slice, they all +get batched together as a single operation of "dirty" scopes. + +This approach is designed around the "diff during rIC and commit during rAF" + +We need to make sure to not call multiple events while the diff machine is borrowing the same scope. Because props +and listeners hold references to hook data, it is wrong to run a scope that is already being diffed. +*/ + +// // 1. Drain the existing immediates. +// // +// // These are generated by async tasks that we never got a chance to finish. +// // All of these get scheduled with the lowest priority. +// while let Ok(Some(dirty_scope)) = self.shared.immediate_receiver.borrow_mut().try_next() +// { +// self.scheduler +// .add_dirty_scope(dirty_scope, EventPriority::Low); +// } + +// // 2. Drain the event queues, calling whatever listeners need to be called +// // +// // First, check if there's any set_states in the queue that we can mark as low priority +// // Next, check the UI event receiver so we can mark those scopes as medium/high priority +// // Next, work on any fibers. +// // Once the fiber work is finished +// while let Some(trigger) = self.scheduler.next_event() { +// match &trigger.event { +// VirtualEvent::AsyncEvent { .. } => {} + +// // This suspense system works, but it's not the most elegant solution. +// // TODO: Replace this system +// VirtualEvent::SuspenseEvent { hook_idx, domnode } => { +// todo!(); +// // // Safety: this handler is the only thing that can mutate shared items at this moment in tim +// // let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + +// // // safety: we are sure that there are no other references to the inner content of suspense hooks +// // let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); + +// // let cx = Context { scope, props: &() }; +// // let scx = SuspendedContext { inner: cx }; + +// // // generate the new node! +// // let nodes: Option = (&hook.callback)(scx); + +// // if let Some(nodes) = nodes { +// // // allocate inside the finished frame - not the WIP frame +// // let nodes = scope.frames.finished_frame().bump.alloc(nodes); + +// // // push the old node's root onto the stack +// // let real_id = domnode.get().ok_or(Error::NotMounted)?; +// // diff_machine.edit_push_root(real_id); + +// // // push these new nodes onto the diff machines stack +// // let meta = diff_machine.create_vnode(&*nodes); + +// // // replace the placeholder with the new nodes we just pushed on the stack +// // diff_machine.edit_replace_with(1, meta.added_to_stack); +// // } else { +// // log::warn!( +// // "Suspense event came through, but there were no generated nodes >:(." +// // ); +// // } +// } + +// VirtualEvent::ClipboardEvent(_) +// | VirtualEvent::CompositionEvent(_) +// | VirtualEvent::KeyboardEvent(_) +// | VirtualEvent::FocusEvent(_) +// | VirtualEvent::FormEvent(_) +// | VirtualEvent::SelectionEvent(_) +// | VirtualEvent::TouchEvent(_) +// | VirtualEvent::UIEvent(_) +// | VirtualEvent::WheelEvent(_) +// | VirtualEvent::MediaEvent(_) +// | VirtualEvent::AnimationEvent(_) +// | VirtualEvent::TransitionEvent(_) +// | VirtualEvent::ToggleEvent(_) +// | VirtualEvent::MouseEvent(_) +// | VirtualEvent::PointerEvent(_) => { +// if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { +// if let Some(element) = trigger.real_node_id { +// scope.call_listener(trigger.event, element)?; + +// // Drain the immediates into the dirty scopes, setting the appropiate priorities +// while let Ok(Some(dirty_scope)) = +// self.shared.immediate_receiver.borrow_mut().try_next() +// { +// self.scheduler +// .add_dirty_scope(dirty_scope, trigger.priority) +// } +// } +// } +// } +// } +// } + +// // todo: for these, make these methods return results where deadlinereached is an error type + +// // 3. Work through the fibers, and wait for any future work to be ready +// let mut machine = DiffMachine::new_headless(&self.shared); +// if self.scheduler.progress_work(&mut machine, &mut is_ready) { +// break; +// } + +// // 4. Wait for any new triggers before looping again +// if self +// .scheduler +// .wait_for_any_trigger(&mut deadline_future) +// .await +// { +// break; +// } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 152dc1a7b..23141c127 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -240,19 +240,16 @@ impl VirtualDom { /// Mutations are the only link between the RealDOM and the VirtualDOM. pub async fn run_with_deadline<'s>( &'s mut self, - mut deadline: impl Future, + deadline: impl Future, ) -> Result> { - // let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); - - // Configure our deadline use futures_util::FutureExt; - let mut deadline_future = deadline.shared(); + 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() }; - todo!() - // Ok(diff_machine.mutations) + self.run_with_deadline_and_is_ready(deadline_future, &mut is_ready) + .await } /// Runs the virtualdom with a deadline and a custom "check" function. @@ -261,163 +258,35 @@ impl VirtualDom { /// queue is completely drained; async fn run_with_deadline_and_is_ready<'s>( &'s mut self, - mut deadline_future: impl Future, - mut is_ready: &mut impl FnMut() -> bool, + mut deadline: impl Future, + is_ready: &mut impl FnMut() -> bool, ) -> Result> { - /* - Strategy: - 1. Check if there are any UI events in the receiver. - 2. If there are, run the listener and then mark the dirty nodes - 3. If there are dirty nodes to be progressed, do so. - 4. Poll the task queue to see if we can create more dirty scopes. - 5. Resume any current in-flight work if there is some. - 6. While the deadline is not met, progress work, periodically checking the deadline. - - - How to choose work: - - When a scope is marked as dirty, it is given a priority. - - If a dirty scope chains (borrowed) into children, mark those as dirty as well. - - When the work loop starts, work on the highest priority scopes first. - - Work by priority, choosing to pause in-flight work if higher-priority work is ready. - - - - 4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached. - 5. While processing a fiber, periodically check if we're out of time - 6. If our deadling is reached, then commit our edits to the realdom - 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) - - - // 1. Check if there are any events in the receiver. - // 2. If there are, process them and create a new fiber. - // 3. If there are no events, then choose a fiber to work on. - // 4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached. - // 5. While processing a fiber, periodically check if we're out of time - // 6. If our deadling is reached, then commit our edits to the realdom - // 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) - - We slice fibers based on time. Each batch of events between frames is its own fiber. This is the simplest way - to conceptualize what *is* or *isn't* a fiber. IE if a bunch of events occur during a time slice, they all - get batched together as a single operation of "dirty" scopes. - - This approach is designed around the "diff during rIC and commit during rAF" - - We need to make sure to not call multiple events while the diff machine is borrowing the same scope. Because props - and listeners hold references to hook data, it is wrong to run a scope that is already being diffed. - */ + let mutations = Mutations::new(); loop { - // 1. Drain the existing immediates. - // - // These are generated by async tasks that we never got a chance to finish. - // All of these get scheduled with the lowest priority. - while let Ok(Some(dirty_scope)) = self.shared.immediate_receiver.borrow_mut().try_next() - { - self.scheduler - .add_dirty_scope(dirty_scope, EventPriority::Low); - } + self.scheduler.manually_poll_channels(); - // 2. Drain the event queue, calling whatever listeners need to be called - // - // First, check if there's any set_states in the queue that we can mark as low priority - // Next, check the UI event receiver so we can mark those scopes as medium/high priority - // Next, work on any fibers. - // Once the fiber work is finished - // - // - // - // + // essentially, is idle + if self.scheduler.is_idle() { + let deadline_expired = self.scheduler.wait_for_any_trigger(&mut deadline).await; - while let Some(trigger) = self.scheduler.next_event() { - match &trigger.event { - VirtualEvent::AsyncEvent { .. } => {} - - // This suspense system works, but it's not the most elegant solution. - // TODO: Replace this system - VirtualEvent::SuspenseEvent { hook_idx, domnode } => { - todo!(); - // // Safety: this handler is the only thing that can mutate shared items at this moment in tim - // let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - // // safety: we are sure that there are no other references to the inner content of suspense hooks - // let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - - // let cx = Context { scope, props: &() }; - // let scx = SuspendedContext { inner: cx }; - - // // generate the new node! - // let nodes: Option = (&hook.callback)(scx); - - // if let Some(nodes) = nodes { - // // allocate inside the finished frame - not the WIP frame - // let nodes = scope.frames.finished_frame().bump.alloc(nodes); - - // // push the old node's root onto the stack - // let real_id = domnode.get().ok_or(Error::NotMounted)?; - // diff_machine.edit_push_root(real_id); - - // // push these new nodes onto the diff machines stack - // let meta = diff_machine.create_vnode(&*nodes); - - // // replace the placeholder with the new nodes we just pushed on the stack - // diff_machine.edit_replace_with(1, meta.added_to_stack); - // } else { - // log::warn!( - // "Suspense event came through, but there were no generated nodes >:(." - // ); - // } - } - - VirtualEvent::ClipboardEvent(_) - | VirtualEvent::CompositionEvent(_) - | VirtualEvent::KeyboardEvent(_) - | VirtualEvent::FocusEvent(_) - | VirtualEvent::FormEvent(_) - | VirtualEvent::SelectionEvent(_) - | VirtualEvent::TouchEvent(_) - | VirtualEvent::UIEvent(_) - | VirtualEvent::WheelEvent(_) - | VirtualEvent::MediaEvent(_) - | VirtualEvent::AnimationEvent(_) - | VirtualEvent::TransitionEvent(_) - | VirtualEvent::ToggleEvent(_) - | VirtualEvent::MouseEvent(_) - | VirtualEvent::PointerEvent(_) => { - if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { - if let Some(element) = trigger.real_node_id { - scope.call_listener(trigger.event, element)?; - - // Drain the immediates into the dirty scopes, setting the appropiate priorities - while let Ok(Some(dirty_scope)) = - self.shared.immediate_receiver.borrow_mut().try_next() - { - self.scheduler - .add_dirty_scope(dirty_scope, trigger.priority) - } - } - } - } + if deadline_expired { + return Ok(mutations); } } - // todo: for these, make these methods return results where deadlinereached is an error type + self.scheduler.consume_pending_events(); - // 3. Work through the fibers, and wait for any future work to be ready - let mut machine = DiffMachine::new_headless(&self.shared); - if self.scheduler.progress_work(&mut machine, &mut is_ready) { - break; - } - - // 4. Wait for any new triggers before looping again - if self - .scheduler - .wait_for_any_trigger(&mut deadline_future) - .await - { - break; + match self.scheduler.work_with_deadline(&mut deadline, is_ready) { + FiberResult::Done(mutations) => { + // commit these mutations + } + FiberResult::Interrupted => { + // + return Ok(mutations); + } } } - todo!() } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { From 882d69571dcc807a5fa3734f14980e5233f895f5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 15 Aug 2021 10:13:03 -0400 Subject: [PATCH 07/53] wip: more work on scheduler --- packages/core/Cargo.toml | 1 + packages/core/src/scheduler.rs | 468 +++++++++++++--------------- packages/core/src/virtual_dom.rs | 53 +++- packages/core/tests/hooks.rs | 26 +- packages/core/tests/vdom_rebuild.rs | 16 +- 5 files changed, 268 insertions(+), 296 deletions(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 2ad42bccf..d0da2ff07 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -43,6 +43,7 @@ futures-channel = "0.3.16" # used for noderefs once_cell = "1.8.0" +indexmap = "1.7.0" [dev-dependencies] diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 07030a2bc..364f76609 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -5,11 +5,18 @@ use std::cell::{Ref, RefCell, RefMut}; use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; use std::pin::Pin; +use futures_util::future::FusedFuture; +use futures_util::pin_mut; use futures_util::Future; +use futures_util::FutureExt; +use futures_util::StreamExt; +use indexmap::IndexSet; use crate::innerlude::*; /// The "Mutations" object holds the changes that need to be made to the DOM. +/// +#[derive(Debug)] pub struct Mutations<'s> { pub edits: Vec>, pub noderefs: Vec>, @@ -21,6 +28,8 @@ impl<'s> Mutations<'s> { let noderefs = Vec::new(); Self { edits, noderefs } } + + pub fn extend(&mut self, other: &mut Mutations) {} } // refs are only assigned once @@ -29,6 +38,14 @@ pub struct NodeRefMutation<'a> { element_id: ElementId, } +impl<'a> std::fmt::Debug for NodeRefMutation<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NodeRefMutation") + .field("element_id", &self.element_id) + .finish() + } +} + impl<'a> NodeRefMutation<'a> { pub fn downcast_ref(&self) -> Option<&T> { self.element @@ -48,16 +65,18 @@ pub struct Scheduler { current_priority: EventPriority, pending_events: VecDeque, - pending_immediates: VecDeque, + + pending_immediates: VecDeque, + pending_tasks: VecDeque, garbage_scopes: HashSet, shared: SharedResources, - dirty_scopes: [HashSet; 3], - - fibers: Vec>, + high_priorty: PriorityFiber<'static>, + medium_priority: PriorityFiber<'static>, + low_priority: PriorityFiber<'static>, } pub enum FiberResult<'a> { @@ -75,14 +94,13 @@ impl Scheduler { pending_immediates: VecDeque::new(), pending_tasks: VecDeque::new(), - fibers: Vec::new(), - garbage_scopes: HashSet::new(), current_priority: EventPriority::Low, - // low, medium, high - dirty_scopes: [HashSet::new(), HashSet::new(), HashSet::new()], + high_priorty: PriorityFiber::new(), + medium_priority: PriorityFiber::new(), + low_priority: PriorityFiber::new(), } } @@ -99,142 +117,206 @@ impl Scheduler { }) } - pub fn manually_poll_channels(&mut self) { - todo!() + pub fn manually_poll_events(&mut self) { + // 1. Poll the UI event receiver + // 2. Poll the set_state receiver + + // poll the immediates first, creating work out of them + let shared_receiver = self.shared.immediate_receiver.clone(); + let mut receiver = shared_receiver.borrow_mut(); + while let Ok(Some(trigger)) = receiver.try_next() { + self.add_dirty_scope(trigger, EventPriority::Low); + } + + // next poll the UI events, + let mut receiver = self.shared.ui_event_receiver.borrow_mut(); + while let Ok(Some(trigger)) = receiver.try_next() { + self.pending_events.push_back(trigger); + } } - pub fn consume_pending_events(&mut self) {} + // Converts UI events into dirty scopes with various priorities + pub fn consume_pending_events(&mut self) -> Result<()> { + while let Some(trigger) = self.pending_events.pop_back() { + match &trigger.event { + VirtualEvent::AsyncEvent { .. } => {} + + // This suspense system works, but it's not the most elegant solution. + // TODO: Replace this system + VirtualEvent::SuspenseEvent { hook_idx, domnode } => { + todo!("suspense needs to be converted into its own channel"); + + // // Safety: this handler is the only thing that can mutate shared items at this moment in tim + // let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + + // // safety: we are sure that there are no other references to the inner content of suspense hooks + // let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); + + // let cx = Context { scope, props: &() }; + // let scx = SuspendedContext { inner: cx }; + + // // generate the new node! + // let nodes: Option = (&hook.callback)(scx); + + // if let Some(nodes) = nodes { + // // allocate inside the finished frame - not the WIP frame + // let nodes = scope.frames.finished_frame().bump.alloc(nodes); + + // // push the old node's root onto the stack + // let real_id = domnode.get().ok_or(Error::NotMounted)?; + // diff_machine.edit_push_root(real_id); + + // // push these new nodes onto the diff machines stack + // let meta = diff_machine.create_vnode(&*nodes); + + // // replace the placeholder with the new nodes we just pushed on the stack + // diff_machine.edit_replace_with(1, meta.added_to_stack); + // } else { + // log::warn!( + // "Suspense event came through, but there were no generated nodes >:(." + // ); + // } + } + + VirtualEvent::ClipboardEvent(_) + | VirtualEvent::CompositionEvent(_) + | VirtualEvent::KeyboardEvent(_) + | VirtualEvent::FocusEvent(_) + | VirtualEvent::FormEvent(_) + | VirtualEvent::SelectionEvent(_) + | VirtualEvent::TouchEvent(_) + | VirtualEvent::UIEvent(_) + | VirtualEvent::WheelEvent(_) + | VirtualEvent::MediaEvent(_) + | VirtualEvent::AnimationEvent(_) + | VirtualEvent::TransitionEvent(_) + | VirtualEvent::ToggleEvent(_) + | VirtualEvent::MouseEvent(_) + | VirtualEvent::PointerEvent(_) => { + if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { + if let Some(element) = trigger.real_node_id { + scope.call_listener(trigger.event, element)?; + + let receiver = self.shared.immediate_receiver.clone(); + let mut receiver = receiver.borrow_mut(); + + // Drain the immediates into the dirty scopes, setting the appropiate priorities + while let Ok(Some(dirty_scope)) = receiver.try_next() { + self.add_dirty_scope(dirty_scope, trigger.priority) + } + } + } + } + } + } + + Ok(()) + } // nothing to do, no events on channels, no work - pub fn is_idle(&self) -> bool { - !self.has_work() && self.has_pending_events() + pub fn has_any_work(&self) -> bool { + self.has_work() || self.has_pending_events() || self.has_pending_garbage() } pub fn has_pending_events(&self) -> bool { - todo!() + self.pending_events.len() > 0 } pub fn has_work(&self) -> bool { - todo!() + let has_work = self.high_priorty.has_work() + || self.medium_priority.has_work() + || self.low_priority.has_work(); + !has_work } + pub fn has_pending_garbage(&self) -> bool { + !self.garbage_scopes.is_empty() + } + + /// 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, - deadline: &mut impl Future, + mut deadline: &mut Pin>>, is_deadline_reached: &mut impl FnMut() -> bool, ) -> FiberResult { + // check if we need to elevate priority + self.current_priority = match ( + self.high_priorty.has_work(), + self.medium_priority.has_work(), + self.low_priority.has_work(), + ) { + (true, _, _) => EventPriority::High, + (false, true, _) => EventPriority::Medium, + (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, + }; + todo!() } // waits for a trigger, canceling early if the deadline is reached // returns true if the deadline was reached // does not return the trigger, but caches it in the scheduler - pub async fn wait_for_any_trigger(&mut self, deadline: &mut impl Future) -> bool { - // poll_all_channels() - // if no events and no work { - // wait for events, cache them - // } + pub async fn wait_for_any_trigger( + &mut self, + mut deadline: &mut Pin>>, + ) -> bool { + use futures_util::select; - // for event in pending_evenst { - // mark_dirty_scopes_with_priorities - // } + let _immediates = self.shared.immediate_receiver.clone(); + let mut immediates = _immediates.borrow_mut(); - // // level shift the priority - // match current_priority { - // high -> finish high queue - // medium -> if work in high { set_current_level_high } - // low -> if work in high { set_current_level_high } else if work in medium { set_current_level_medium } - // } + let mut _ui_events = self.shared.ui_event_receiver.clone(); + let mut ui_events = _ui_events.borrow_mut(); - // let mut fiber = load_priority_fiber(); + let mut _tasks = self.shared.async_tasks.clone(); + let mut tasks = _tasks.borrow_mut(); - // fiber.load_work(high_priority_work); + // set_state called + select! { + dirty_scope = immediates.next() => { + if let Some(scope) = dirty_scope { + self.add_dirty_scope(scope, EventPriority::Low); + } + } - // fiber.progress_with_deadline(deadline); + ui_event = ui_events.next() => { + if let Some(event) = ui_event { + self.pending_events.push_back(event); + } + } - // if fiber.finished() { - // if work_pending_for_priority(fiber.priority()) { - // fiber.load_work(priority_work) - // } else { - // shift_level_down() - // } - // } + async_task = tasks.next() => { + if let Some(event) = async_task { + self.pending_events.push_back(event); + } + } - // load current events - // if work queue is empty { - // wait for events - // load current events - // } + _ = deadline => { + return true; + } - todo!() - // match raw_trigger { - // Ok(Some(trigger)) => trigger, - // _ => { - // // nothing to do - let's clean up any garbage we might have - // self.scheduler.clean_up_garbage(); - - // // Continuously poll the future pool and the event receiver for work - // let mut tasks = self.shared.async_tasks.borrow_mut(); - // let tasks_tasks = tasks.next(); - - // // if the new event generates work more important than our current fiber, we should consider switching - // // only switch if it impacts different scopes. - // let mut ui_receiver = self.shared.ui_event_receiver.borrow_mut(); - // let ui_reciv_task = ui_receiver.next(); - - // // right now, this polling method will only catch batched set_states that don't get awaited. - // // However, in the future, we might be interested in batching set_states across await points - // let immediate_tasks = (); - - // futures_util::pin_mut!(tasks_tasks); - // futures_util::pin_mut!(ui_reciv_task); - - // // Poll the event receiver and the future pool for work - // // Abort early if our deadline has ran out - // let mut deadline = (&mut deadline_future).fuse(); - - // let trig = futures_util::select! { - // trigger = tasks_tasks => trigger, - // trigger = ui_reciv_task => trigger, - - // // abort if we're out of time - // _ = deadline => { return Ok(Mutations::new()); } - // // _ = deadline => { return Ok(diff_machine.mutations); } - // }; - - // trig.unwrap() - // } - // }; - // + } + false } pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { - // - - // generated_immediates - // .entry(dirty_scope) - // .and_modify(|cur_priority| { - // if *cur_priority > new_priority { - // *cur_priority = new_priority; - // } - // }) - // .or_insert_with(|| new_priority); - } - - // returns true if the deadline is reached - // returns false if the deadline is not reached - pub fn progress_work( - &mut self, - machine: &mut DiffMachine, - is_deadline_reached: &mut impl FnMut() -> bool, - ) -> bool { - todo!() + match priority { + EventPriority::High => self.high_priorty.dirty_scopes.insert(scope), + EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope), + EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), + }; } pub fn clean_up_garbage(&mut self) { let mut scopes_to_kill = Vec::new(); - let mut garbage_list = Vec::new(); + for scope in self.garbage_scopes.drain() { let scope = self.shared.get_scope_mut(scope).unwrap(); for node in scope.consume_garbage() { @@ -278,18 +360,22 @@ impl Scheduler { } } } + + for scope in scopes_to_kill.drain(..) { + // + // kill em + } } } -#[derive(PartialEq, Eq, Copy, Clone, Debug)] +#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)] pub struct DirtyScope { height: u32, - priority: EventPriority, 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 Fiber<'a> { +pub struct PriorityFiber<'a> { // scopes that haven't been updated yet pending_scopes: Vec, @@ -302,9 +388,15 @@ pub struct Fiber<'a> { // a fiber is finished when no more scopes or nodes are pending completed: bool, + + dirty_scopes: IndexSet, + + wip_edits: Vec>, + + current_batch_scopes: HashSet, } -impl Fiber<'_> { +impl PriorityFiber<'_> { fn new() -> Self { Self { pending_scopes: Vec::new(), @@ -312,153 +404,15 @@ impl Fiber<'_> { edits: Vec::new(), started: false, completed: false, + dirty_scopes: IndexSet::new(), + wip_edits: Vec::new(), + current_batch_scopes: HashSet::new(), } } + + fn has_work(&self) -> bool { + self.dirty_scopes.is_empty() + && self.wip_edits.is_empty() + && self.current_batch_scopes.is_empty() + } } - -/* -Strategy: -1. Check if there are any UI events in the receiver. -2. If there are, run the listener and then mark the dirty nodes -3. If there are dirty nodes to be progressed, do so. -4. Poll the task queue to see if we can create more dirty scopes. -5. Resume any current in-flight work if there is some. -6. While the deadline is not met, progress work, periodically checking the deadline. - - -How to choose work: -- When a scope is marked as dirty, it is given a priority. -- If a dirty scope chains (borrowed) into children, mark those as dirty as well. -- When the work loop starts, work on the highest priority scopes first. -- Work by priority, choosing to pause in-flight work if higher-priority work is ready. - - - -4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached. -5. While processing a fiber, periodically check if we're out of time -6. If our deadling is reached, then commit our edits to the realdom -7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) - - -// 1. Check if there are any events in the receiver. -// 2. If there are, process them and create a new fiber. -// 3. If there are no events, then choose a fiber to work on. -// 4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached. -// 5. While processing a fiber, periodically check if we're out of time -// 6. If our deadling is reached, then commit our edits to the realdom -// 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) - -We slice fibers based on time. Each batch of events between frames is its own fiber. This is the simplest way -to conceptualize what *is* or *isn't* a fiber. IE if a bunch of events occur during a time slice, they all -get batched together as a single operation of "dirty" scopes. - -This approach is designed around the "diff during rIC and commit during rAF" - -We need to make sure to not call multiple events while the diff machine is borrowing the same scope. Because props -and listeners hold references to hook data, it is wrong to run a scope that is already being diffed. -*/ - -// // 1. Drain the existing immediates. -// // -// // These are generated by async tasks that we never got a chance to finish. -// // All of these get scheduled with the lowest priority. -// while let Ok(Some(dirty_scope)) = self.shared.immediate_receiver.borrow_mut().try_next() -// { -// self.scheduler -// .add_dirty_scope(dirty_scope, EventPriority::Low); -// } - -// // 2. Drain the event queues, calling whatever listeners need to be called -// // -// // First, check if there's any set_states in the queue that we can mark as low priority -// // Next, check the UI event receiver so we can mark those scopes as medium/high priority -// // Next, work on any fibers. -// // Once the fiber work is finished -// while let Some(trigger) = self.scheduler.next_event() { -// match &trigger.event { -// VirtualEvent::AsyncEvent { .. } => {} - -// // This suspense system works, but it's not the most elegant solution. -// // TODO: Replace this system -// VirtualEvent::SuspenseEvent { hook_idx, domnode } => { -// todo!(); -// // // Safety: this handler is the only thing that can mutate shared items at this moment in tim -// // let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - -// // // safety: we are sure that there are no other references to the inner content of suspense hooks -// // let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - -// // let cx = Context { scope, props: &() }; -// // let scx = SuspendedContext { inner: cx }; - -// // // generate the new node! -// // let nodes: Option = (&hook.callback)(scx); - -// // if let Some(nodes) = nodes { -// // // allocate inside the finished frame - not the WIP frame -// // let nodes = scope.frames.finished_frame().bump.alloc(nodes); - -// // // push the old node's root onto the stack -// // let real_id = domnode.get().ok_or(Error::NotMounted)?; -// // diff_machine.edit_push_root(real_id); - -// // // push these new nodes onto the diff machines stack -// // let meta = diff_machine.create_vnode(&*nodes); - -// // // replace the placeholder with the new nodes we just pushed on the stack -// // diff_machine.edit_replace_with(1, meta.added_to_stack); -// // } else { -// // log::warn!( -// // "Suspense event came through, but there were no generated nodes >:(." -// // ); -// // } -// } - -// VirtualEvent::ClipboardEvent(_) -// | VirtualEvent::CompositionEvent(_) -// | VirtualEvent::KeyboardEvent(_) -// | VirtualEvent::FocusEvent(_) -// | VirtualEvent::FormEvent(_) -// | VirtualEvent::SelectionEvent(_) -// | VirtualEvent::TouchEvent(_) -// | VirtualEvent::UIEvent(_) -// | VirtualEvent::WheelEvent(_) -// | VirtualEvent::MediaEvent(_) -// | VirtualEvent::AnimationEvent(_) -// | VirtualEvent::TransitionEvent(_) -// | VirtualEvent::ToggleEvent(_) -// | VirtualEvent::MouseEvent(_) -// | VirtualEvent::PointerEvent(_) => { -// if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { -// if let Some(element) = trigger.real_node_id { -// scope.call_listener(trigger.event, element)?; - -// // Drain the immediates into the dirty scopes, setting the appropiate priorities -// while let Ok(Some(dirty_scope)) = -// self.shared.immediate_receiver.borrow_mut().try_next() -// { -// self.scheduler -// .add_dirty_scope(dirty_scope, trigger.priority) -// } -// } -// } -// } -// } -// } - -// // todo: for these, make these methods return results where deadlinereached is an error type - -// // 3. Work through the fibers, and wait for any future work to be ready -// let mut machine = DiffMachine::new_headless(&self.shared); -// if self.scheduler.progress_work(&mut machine, &mut is_ready) { -// break; -// } - -// // 4. Wait for any new triggers before looping again -// if self -// .scheduler -// .wait_for_any_trigger(&mut deadline_future) -// .await -// { -// break; -// } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 23141c127..0dafc2ea2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -19,7 +19,7 @@ //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. use crate::innerlude::*; -use futures_util::Future; +use futures_util::{pin_mut, Future, FutureExt}; use std::{ any::{Any, TypeId}, pin::Pin, @@ -152,8 +152,7 @@ impl VirtualDom { /// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed /// through "run" /// - pub fn rebuild<'s>(&'s mut self) -> Result>> { - let mut edits = Vec::new(); + pub fn rebuild<'s>(&'s mut self) -> Result> { let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); let cur_component = diff_machine @@ -172,7 +171,7 @@ impl VirtualDom { ); } - Ok(edits) + Ok(diff_machine.mutations) } /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. @@ -258,33 +257,53 @@ impl VirtualDom { /// queue is completely drained; async fn run_with_deadline_and_is_ready<'s>( &'s mut self, - mut deadline: impl Future, + deadline: impl Future, is_ready: &mut impl FnMut() -> bool, ) -> Result> { - let mutations = Mutations::new(); + let mut committed_mutations = Mutations::new(); + let mut deadline = Box::pin(deadline.fuse()); + // TODO: + // the scheduler uses a bunch of different receivers to mimic a "topic" queue system. The futures-channel implementation + // doesn't really have a concept of a "topic" queue, so there's a lot of noise in the hand-rolled scheduler. We should + // explore abstracting the scheduler into a topic-queue channel system - similar to Kafka or something similar. loop { - self.scheduler.manually_poll_channels(); + // Internalize any pending work since the last time we ran + self.scheduler.manually_poll_events(); - // essentially, is idle - if self.scheduler.is_idle() { + // Wait for any new events if we have nothing to do + if !self.scheduler.has_any_work() { + self.scheduler.clean_up_garbage(); let deadline_expired = self.scheduler.wait_for_any_trigger(&mut deadline).await; if deadline_expired { - return Ok(mutations); + return Ok(committed_mutations); } } - self.scheduler.consume_pending_events(); + // Create work from the pending event queue + self.scheduler.consume_pending_events()?; + // 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) { - FiberResult::Done(mutations) => { - // commit these mutations - } - FiberResult::Interrupted => { - // - return Ok(mutations); + FiberResult::Done(mut mutations) => { + committed_mutations.extend(&mut mutations); + + /* + quick return if there's no work left, so we can commit before the deadline expires + When we loop over again, we'll re-wait for any new work. + + I'm not quite sure how this *should* work. + + It makes sense to try and progress the DOM faster + */ + + if !self.scheduler.has_any_work() { + return Ok(committed_mutations); + } } + FiberResult::Interrupted => return Ok(committed_mutations), } } } diff --git a/packages/core/tests/hooks.rs b/packages/core/tests/hooks.rs index 6e3941e37..2c434eabe 100644 --- a/packages/core/tests/hooks.rs +++ b/packages/core/tests/hooks.rs @@ -10,19 +10,17 @@ use dioxus_html as dioxus_elements; #[test] fn sample_refs() { - static App: FC<()> = |cx| { - let div_ref = use_node_ref::(cx); + // static App: FC<()> = |cx| { + // let div_ref = use_node_ref::(cx); - cx.render(rsx! { - div { - style: { color: "red" } - node_ref: div_ref - onmouseover: move |_| { - div_ref.borrow_mut().focus(); - } - } - }) - }; + // cx.render(rsx! { + // div { + // style: { color: "red" }, + // node_ref: div_ref, + // onmouseover: move |_| { + // div_ref.borrow_mut().focus(); + // }, + // }, + // }) + // }; } - -struct MyRef {} diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 54d1676c8..a3c1fdb3b 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -19,7 +19,7 @@ fn app_runs() { cx.render(rsx!( div{"hello"} )) }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild_in_place().unwrap(); + let edits = vdom.rebuild().unwrap(); dbg!(edits); } @@ -32,7 +32,7 @@ fn fragments_work() { )) }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild_in_place().unwrap(); + let edits = vdom.rebuild().unwrap(); // should result in a final "appendchildren n=2" dbg!(edits); } @@ -46,7 +46,7 @@ fn lists_work() { )) }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild_in_place().unwrap(); + let edits = vdom.rebuild().unwrap(); dbg!(edits); } @@ -61,10 +61,10 @@ fn conditional_rendering() { }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild_in_place().unwrap(); - dbg!(&edits); + let mutations = vdom.rebuild().unwrap(); + dbg!(&mutations); // the "false" fragment should generate an empty placeholder to re-visit - assert!(edits[edits.len() - 2].is("CreatePlaceholder")); + assert!(mutations.edits[mutations.edits.len() - 2].is("CreatePlaceholder")); } #[test] @@ -82,7 +82,7 @@ fn child_components() { )) }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild_in_place().unwrap(); + let edits = vdom.rebuild().unwrap(); dbg!(edits); } @@ -94,6 +94,6 @@ fn suspended_works() { }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild_in_place().unwrap(); + let edits = vdom.rebuild().unwrap(); dbg!(edits); } From f41cff571fd11bb00820c0bf9d147e81ed5a6a53 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 17 Aug 2021 22:25:09 -0400 Subject: [PATCH 08/53] wip: working on async diff --- examples/ssr.rs | 9 ++- packages/core/src/diff.rs | 86 +++++++++++++--------- packages/core/src/lib.rs | 2 + packages/core/src/scheduler.rs | 120 +++++++++++++++++++------------ packages/core/src/virtual_dom.rs | 32 ++------- packages/core/src/yield_now.rs | 52 ++++++++++++++ packages/web/src/lib.rs | 6 +- 7 files changed, 198 insertions(+), 109 deletions(-) create mode 100644 packages/core/src/yield_now.rs 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, From 9652ccdcf19189e2aac9262bcfa30e2b04c36d37 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 19 Aug 2021 02:50:35 -0400 Subject: [PATCH 09/53] wip: move from recursive to iterative --- packages/core/src/diff.rs | 444 +++++++- packages/core/src/diff.rs.old | 1708 ++++++++++++++++++++++++++++++ packages/core/src/scheduler.rs | 3 +- packages/core/tests/iterative.rs | 47 + 4 files changed, 2173 insertions(+), 29 deletions(-) create mode 100644 packages/core/src/diff.rs.old create mode 100644 packages/core/tests/iterative.rs diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 1c10f2113..ad1cabf83 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -79,17 +79,44 @@ use std::{ }; use DomEdit::*; +/// Our DiffMachine is an iterative tree differ. +/// +/// It uses techniques of a register-based Turing Machines to allow pausing and restarting of the diff algorithm. This +/// was origially implemented using recursive techniques, but Rust lacks the abilty to call async functions recursively, +/// meaning we could not "pause" the diffing algorithm. +/// +/// Instead, we use a traditional stack machine approach to diff and create new nodes. pub struct DiffMachine<'bump> { vdom: &'bump SharedResources, pub mutations: Mutations<'bump>, + pub nodes_created_stack: SmallVec<[usize; 10]>, + + pub node_stack: SmallVec<[DiffInstruction<'bump>; 10]>, + pub scope_stack: SmallVec<[ScopeId; 5]>, pub diffed: FxHashSet, pub seen_scopes: FxHashSet, } +pub enum DiffInstruction<'a> { + DiffNode { + old: &'a VNode<'a>, + new: &'a VNode<'a>, + progress: usize, + }, + + Append {}, + Replace { + with: usize, + }, + + Create { + node: &'a VNode<'a>, + }, +} impl<'bump> DiffMachine<'bump> { pub(crate) fn new( @@ -98,6 +125,8 @@ impl<'bump> DiffMachine<'bump> { shared: &'bump SharedResources, ) -> Self { Self { + node_stack: smallvec![], + nodes_created_stack: smallvec![], mutations: edits, scope_stack: smallvec![cur_scope], vdom: shared, @@ -111,19 +140,15 @@ impl<'bump> DiffMachine<'bump> { /// /// This will PANIC if any component elements are passed in. pub fn new_headless(shared: &'bump SharedResources) -> Self { - Self { - mutations: Mutations::new(), - scope_stack: smallvec![ScopeId(0)], - vdom: shared, - diffed: FxHashSet::default(), - seen_scopes: FxHashSet::default(), - } - } - - // make incremental progress on the current task - pub fn work(&mut self, is_ready: impl FnMut() -> bool) -> Result { todo!() - // Ok(FiberResult::D) + // Self { + // node_stack: smallvec![], + // mutations: Mutations::new(), + // scope_stack: smallvec![ScopeId(0)], + // vdom: shared, + // diffed: FxHashSet::default(), + // seen_scopes: FxHashSet::default(), + // } } // @@ -134,17 +159,153 @@ impl<'bump> DiffMachine<'bump> { Ok(()) } - // Diff the `old` node with the `new` node. Emits instructions to modify a - // physical DOM node that reflects `old` into something that reflects `new`. - // - // 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 async fn diff_node( - &mut self, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, - ) { + /// Progress the diffing for this "fiber" + /// + /// 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 + /// + /// + /// + pub async fn work(&mut self) -> Result<()> { + // todo: don't move the reused instructions around + while let Some(mut make) = self.node_stack.pop() { + match &mut make { + DiffInstruction::DiffNode { old, new, progress } => { + // + } + + DiffInstruction::Append {} => { + let many = self.nodes_created_stack.pop().unwrap(); + self.edit_append_children(many as u32); + } + + DiffInstruction::Replace { with } => { + let many = self.nodes_created_stack.pop().unwrap(); + self.edit_replace_with(*with as u32, many as u32); + } + + DiffInstruction::Create { node, .. } => { + match &node.kind { + VNodeKind::Text(text) => { + let real_id = self.vdom.reserve_node(); + self.edit_create_text_node(text.text, real_id); + text.dom_id.set(Some(real_id)); + *self.nodes_created_stack.last_mut().unwrap() += 1; + } + VNodeKind::Suspended(suspended) => { + let real_id = self.vdom.reserve_node(); + self.edit_create_placeholder(real_id); + suspended.node.set(Some(real_id)); + *self.nodes_created_stack.last_mut().unwrap() += 1; + } + VNodeKind::Anchor(anchor) => { + let real_id = self.vdom.reserve_node(); + self.edit_create_placeholder(real_id); + anchor.dom_id.set(Some(real_id)); + *self.nodes_created_stack.last_mut().unwrap() += 1; + } + + VNodeKind::Element(el) => { + let VElement { + tag_name, + listeners, + attributes, + children, + namespace, + static_attrs: _, + static_children: _, + static_listeners: _, + dom_id, + } = el; + + let real_id = self.vdom.reserve_node(); + self.edit_create_element(tag_name, *namespace, real_id); + dom_id.set(Some(real_id)); + + let cur_scope = self.current_scope().unwrap(); + + listeners.iter().for_each(|listener| { + self.fix_listener(listener); + listener.mounted_node.set(Some(real_id)); + self.edit_new_event_listener(listener, cur_scope.clone()); + + // if the node has an event listener, then it must be visited ? + }); + + for attr in *attributes { + self.edit_set_attribute(attr); + } + + // TODO: the append child edit + + *self.nodes_created_stack.last_mut().unwrap() += 1; + + // push every child onto the stack + self.nodes_created_stack.push(0); + for child in *children { + self.node_stack + .push(DiffInstruction::Create { node: child }) + } + } + + VNodeKind::Fragment(frag) => { + for node in frag.children { + self.node_stack.push(DiffInstruction::Create { node }) + } + } + + VNodeKind::Component(_) => { + // + } + } + } + } + } + + Ok(()) + } + + /// Create the new node, pushing instructions on our instruction stack to create any further children + /// + /// + pub fn create_iterative(&mut self, node: &'bump VNode<'bump>) { + match &node.kind { + // singles + // update the parent + VNodeKind::Text(text) => { + let real_id = self.vdom.reserve_node(); + self.edit_create_text_node(text.text, real_id); + text.dom_id.set(Some(real_id)); + *self.nodes_created_stack.last_mut().unwrap() += 1; + } + VNodeKind::Suspended(suspended) => { + let real_id = self.vdom.reserve_node(); + self.edit_create_placeholder(real_id); + suspended.node.set(Some(real_id)); + *self.nodes_created_stack.last_mut().unwrap() += 1; + } + VNodeKind::Anchor(anchor) => { + let real_id = self.vdom.reserve_node(); + self.edit_create_placeholder(real_id); + anchor.dom_id.set(Some(real_id)); + *self.nodes_created_stack.last_mut().unwrap() += 1; + } + + VNodeKind::Element(el) => { + // + } + VNodeKind::Fragment(frag) => { + // + } + VNodeKind::Component(comp) => { + // + } + } + } + + pub fn diff_iterative(&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". @@ -275,8 +436,7 @@ impl<'bump> DiffMachine<'bump> { false => { // the props are different... scope.run_scope().unwrap(); - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()) - .await; + self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); } } @@ -308,7 +468,222 @@ impl<'bump> DiffMachine<'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]).await; + self.diff_node(&old.children[0], &new.children[0]); + return; + } + + self.diff_children(old.children, new.children); + } + + (VNodeKind::Anchor(old), VNodeKind::Anchor(new)) => { + new.dom_id.set(old.dom_id.get()); + } + + // The strategy here is to pick the first possible node from the previous set and use that as our replace with root + // + // We also walk the "real node" list to make sure all latent roots are claened up + // This covers the case any time a fragment or component shows up with pretty much anything else + // + // This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases + // are pretty rare. IE replacing a list (component or fragment) with a single node. + ( + VNodeKind::Component(_) + | VNodeKind::Fragment(_) + | VNodeKind::Text(_) + | VNodeKind::Element(_) + | VNodeKind::Anchor(_), + VNodeKind::Component(_) + | VNodeKind::Fragment(_) + | VNodeKind::Text(_) + | VNodeKind::Element(_) + | VNodeKind::Anchor(_), + ) => { + self.replace_and_create_many_with_many([old_node], [new_node]); + } + + // TODO + (VNodeKind::Suspended(old), new) => { + // + self.replace_and_create_many_with_many([old_node], [new_node]); + } + // a node that was once real is now suspended + (old, VNodeKind::Suspended(_)) => { + // + self.replace_and_create_many_with_many([old_node], [new_node]); + } + } + } + + // Diff the `old` node with the `new` node. Emits instructions to modify a + // physical DOM node that reflects `old` into something that reflects `new`. + // + // 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>) { + 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". + // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. + (VNodeKind::Text(old), VNodeKind::Text(new)) => { + let root = old_node.direct_id(); + + if old.text != new.text { + self.edit_push_root(root); + self.edit_set_text(new.text); + self.edit_pop(); + } + + new.dom_id.set(Some(root)); + } + + (VNodeKind::Element(old), VNodeKind::Element(new)) => { + let root = old_node.direct_id(); + + // 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_name != old.tag_name || new.namespace != old.namespace { + self.replace_node_with_node(root, old_node, new_node); + return; + } + + new.dom_id.set(Some(root)); + + // Don't push the root if we don't have to + let mut has_comitted = false; + let mut please_commit = |edits: &mut Vec| { + if !has_comitted { + has_comitted = true; + edits.push(PushRoot { id: root.as_u64() }); + } + }; + + // 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 { + please_commit(&mut self.mutations.edits); + self.edit_set_attribute(new_attr); + } + } + } else { + // TODO: provide some sort of report on how "good" the diffing was + please_commit(&mut self.mutations.edits); + for attribute in old.attributes { + self.edit_remove_attribute(attribute); + } + for attribute in new.attributes { + self.edit_set_attribute(attribute) + } + } + + // 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 + let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); + 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 { + please_commit(&mut self.mutations.edits); + self.edit_remove_event_listener(old_l.event); + self.edit_new_event_listener(new_l, cur_scope); + } + new_l.mounted_node.set(old_l.mounted_node.get()); + self.fix_listener(new_l); + } + } else { + please_commit(&mut self.mutations.edits); + for listener in old.listeners { + self.edit_remove_event_listener(listener.event); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.edit_new_event_listener(listener, cur_scope); + + // Make sure the listener gets attached to the scope list + self.fix_listener(listener); + } + } + + if has_comitted { + self.edit_pop(); + } + + self.diff_children(old.children, new.children); + } + + (VNodeKind::Component(old), VNodeKind::Component(new)) => { + let scope_addr = old.ass_scope.get().unwrap(); + + // Make sure we're dealing with the same component (by function pointer) + if old.user_fc == new.user_fc { + // + self.scope_stack.push(scope_addr); + + // Make sure the new component vnode is referencing the right scope id + new.ass_scope.set(Some(scope_addr)); + + // make sure the component's caller function is up to date + let scope = self.get_scope_mut(&scope_addr).unwrap(); + + scope + .update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); + + // React doesn't automatically memoize, but we do. + let compare = old.comparator.unwrap(); + + match compare(new) { + true => { + // the props are the same... + } + false => { + // the props are different... + scope.run_scope().unwrap(); + self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); + } + } + + self.scope_stack.pop(); + + self.seen_scopes.insert(scope_addr); + } else { + let mut old_iter = RealChildIterator::new(old_node, &self.vdom); + let first = old_iter + .next() + .expect("Components should generate a placeholder root"); + + // remove any leftovers + for to_remove in old_iter { + self.edit_push_root(to_remove.direct_id()); + self.edit_remove(); + } + + // seems like we could combine this into a single instruction.... + self.replace_node_with_node(first.direct_id(), old_node, new_node); + + // Wipe the old one and plant the new one + let old_scope = old.ass_scope.get().unwrap(); + self.destroy_scopes(old_scope); + } + } + + (VNodeKind::Fragment(old), VNodeKind::Fragment(new)) => { + // 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; } @@ -1116,14 +1491,14 @@ impl<'bump> DiffMachine<'bump> { // diff the rest for (new_child, old_child) in new.iter().zip(old.iter()) { - self.diff_node(old_child, new_child).await + self.diff_node(old_child, new_child) } } // 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).await; + self.diff_node(old_child, new_child); } } } @@ -1659,3 +2034,16 @@ fn compare_strs(a: &str, b: &str) -> bool { true } } + +struct DfsIterator<'a> { + idx: usize, + node: Option<(&'a VNode<'a>, &'a VNode<'a>)>, + nodes: Option<(&'a [VNode<'a>], &'a [VNode<'a>])>, +} +impl<'a> Iterator for DfsIterator<'a> { + type Item = (&'a VNode<'a>, &'a VNode<'a>); + + fn next(&mut self) -> Option { + todo!() + } +} diff --git a/packages/core/src/diff.rs.old b/packages/core/src/diff.rs.old new file mode 100644 index 000000000..6bcf03568 --- /dev/null +++ b/packages/core/src/diff.rs.old @@ -0,0 +1,1708 @@ +//! 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, incremental diffing, cancelation, NodeRefs, and additional +//! batching operations. +//! +//! ## Implementation Details: +//! +//! ### IDs for elements +//! -------------------- +//! 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 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 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 +//! +//! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments +//! can be particularly challenging when they are empty, so the placeholder node lets us "reserve" a spot for the empty +//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the NodeFactory - it is +//! 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 +//! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the +//! calls to "create" propogate this information upwards. Structures like the one below are entirely static: +//! ```rust +//! 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 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. 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 +//! --------------------- +//! Dioxus uses a passive garbage collection system to clean up old nodes once the work has been completed. This garabge +//! collection is done internally once the main diffing work is complete. After the "garbage" is collected, Dioxus will then +//! start to re-use old keys for new nodes. This results in a passive memory management system that is very efficient. +//! +//! The IDs used by the key/map are just an index into a vec. This means that Dioxus will drive the key allocation strategy +//! 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. +//! +//! Further Reading and Thoughts +//! ---------------------------- +//! There are more ways of increasing diff performance here that are currently not implemented. +//! More info on how to improve this diffing algorithm: +//! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ + +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, collections::HashSet, marker::PhantomData, pin::Pin, +}; +use DomEdit::*; + +pub struct DiffMachine<'bump> { + vdom: &'bump SharedResources, + + pub mutations: Mutations<'bump>, + + pub node_stack: SmallVec<[SearchNode<'bump>; 10]>, + + pub scope_stack: SmallVec<[ScopeId; 5]>, + + pub diffed: FxHashSet, + + pub seen_scopes: FxHashSet, +} +enum SearchNode<'a> { + Node { + old: &'a VNode<'a>, + new: &'a VNode<'a>, + }, + NodeList { + old: &'a [VNode<'a>], + node: &'a [VNode<'a>], + }, +} + +impl<'bump> DiffMachine<'bump> { + pub(crate) fn new( + edits: Mutations<'bump>, + cur_scope: ScopeId, + shared: &'bump SharedResources, + ) -> Self { + Self { + node_stack: smallvec![], + mutations: edits, + scope_stack: smallvec![cur_scope], + vdom: shared, + diffed: FxHashSet::default(), + seen_scopes: FxHashSet::default(), + } + } + + /// Allows the creation of a diff machine without the concept of scopes or a virtualdom + /// this is mostly useful for testing + /// + /// This will PANIC if any component elements are passed in. + pub fn new_headless(shared: &'bump SharedResources) -> Self { + Self { + node_stack: smallvec![], + mutations: Mutations::new(), + scope_stack: smallvec![ScopeId(0)], + vdom: shared, + diffed: FxHashSet::default(), + seen_scopes: FxHashSet::default(), + } + } + + // + 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); + Ok(()) + } + + /// Progress the diffing for this "fiber" + /// + /// 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 + /// + /// + /// + pub async fn work(&mut self) -> Result<()> { + // + while let Some(search) = self.node_stack.last_mut() { + // Prevent our task from blocking permanently + yield_now().await; + + match search { + SearchNode::Node { old, new } => { + // + // self.diff_node(old, new); + } + + SearchNode::NodeList { old, node } => { + // + todo!() + } + } + } + + Ok(()) + } + + // Diff the `old` node with the `new` node. Emits instructions to modify a + // physical DOM node that reflects `old` into something that reflects `new`. + // + // 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>) { + 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". + // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. + (VNodeKind::Text(old), VNodeKind::Text(new)) => { + let root = old_node.direct_id(); + + if old.text != new.text { + self.edit_push_root(root); + self.edit_set_text(new.text); + self.edit_pop(); + } + + new.dom_id.set(Some(root)); + } + + (VNodeKind::Element(old), VNodeKind::Element(new)) => { + let root = old_node.direct_id(); + + // 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_name != old.tag_name || new.namespace != old.namespace { + self.replace_node_with_node(root, old_node, new_node); + return; + } + + new.dom_id.set(Some(root)); + + // Don't push the root if we don't have to + let mut has_comitted = false; + let mut please_commit = |edits: &mut Vec| { + if !has_comitted { + has_comitted = true; + edits.push(PushRoot { id: root.as_u64() }); + } + }; + + // 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 { + please_commit(&mut self.mutations.edits); + self.edit_set_attribute(new_attr); + } + } + } else { + // TODO: provide some sort of report on how "good" the diffing was + please_commit(&mut self.mutations.edits); + for attribute in old.attributes { + self.edit_remove_attribute(attribute); + } + for attribute in new.attributes { + self.edit_set_attribute(attribute) + } + } + + // 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 + let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); + 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 { + please_commit(&mut self.mutations.edits); + self.edit_remove_event_listener(old_l.event); + self.edit_new_event_listener(new_l, cur_scope); + } + new_l.mounted_node.set(old_l.mounted_node.get()); + self.fix_listener(new_l); + } + } else { + please_commit(&mut self.mutations.edits); + for listener in old.listeners { + self.edit_remove_event_listener(listener.event); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.edit_new_event_listener(listener, cur_scope); + + // Make sure the listener gets attached to the scope list + self.fix_listener(listener); + } + } + + if has_comitted { + self.edit_pop(); + } + + self.diff_children(old.children, new.children); + } + + (VNodeKind::Component(old), VNodeKind::Component(new)) => { + let scope_addr = old.ass_scope.get().unwrap(); + + // Make sure we're dealing with the same component (by function pointer) + if old.user_fc == new.user_fc { + // + self.scope_stack.push(scope_addr); + + // Make sure the new component vnode is referencing the right scope id + new.ass_scope.set(Some(scope_addr)); + + // make sure the component's caller function is up to date + let scope = self.get_scope_mut(&scope_addr).unwrap(); + + scope + .update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); + + // React doesn't automatically memoize, but we do. + let compare = old.comparator.unwrap(); + + match compare(new) { + true => { + // the props are the same... + } + false => { + // the props are different... + scope.run_scope().unwrap(); + self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); + } + } + + self.scope_stack.pop(); + + self.seen_scopes.insert(scope_addr); + } else { + let mut old_iter = RealChildIterator::new(old_node, &self.vdom); + let first = old_iter + .next() + .expect("Components should generate a placeholder root"); + + // remove any leftovers + for to_remove in old_iter { + self.edit_push_root(to_remove.direct_id()); + self.edit_remove(); + } + + // seems like we could combine this into a single instruction.... + self.replace_node_with_node(first.direct_id(), old_node, new_node); + + // Wipe the old one and plant the new one + let old_scope = old.ass_scope.get().unwrap(); + self.destroy_scopes(old_scope); + } + } + + (VNodeKind::Fragment(old), VNodeKind::Fragment(new)) => { + // 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; + } + + self.diff_children(old.children, new.children); + } + + (VNodeKind::Anchor(old), VNodeKind::Anchor(new)) => { + new.dom_id.set(old.dom_id.get()); + } + + // The strategy here is to pick the first possible node from the previous set and use that as our replace with root + // + // We also walk the "real node" list to make sure all latent roots are claened up + // This covers the case any time a fragment or component shows up with pretty much anything else + // + // This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases + // are pretty rare. IE replacing a list (component or fragment) with a single node. + ( + VNodeKind::Component(_) + | VNodeKind::Fragment(_) + | VNodeKind::Text(_) + | VNodeKind::Element(_) + | VNodeKind::Anchor(_), + VNodeKind::Component(_) + | VNodeKind::Fragment(_) + | VNodeKind::Text(_) + | VNodeKind::Element(_) + | VNodeKind::Anchor(_), + ) => { + self.replace_and_create_many_with_many([old_node], [new_node]); + } + + // TODO + (VNodeKind::Suspended(old), new) => { + // + self.replace_and_create_many_with_many([old_node], [new_node]); + } + // a node that was once real is now suspended + (old, VNodeKind::Suspended(_)) => { + // + self.replace_and_create_many_with_many([old_node], [new_node]); + } + } + } + + // Emit instructions to create the given virtual node. + // + // The change list stack may have any shape upon entering this function: + // + // [...] + // + // When this function returns, the new node is on top of the change list stack: + // + // [... node] + pub fn create_vnode(&mut self, node: &'bump VNode<'bump>) -> CreateMeta { + match &node.kind { + VNodeKind::Text(text) => { + let real_id = self.vdom.reserve_node(); + self.edit_create_text_node(text.text, real_id); + text.dom_id.set(Some(real_id)); + CreateMeta::new(text.is_static, 1) + } + + VNodeKind::Anchor(anchor) => { + let real_id = self.vdom.reserve_node(); + self.edit_create_placeholder(real_id); + anchor.dom_id.set(Some(real_id)); + CreateMeta::new(false, 1) + } + + VNodeKind::Element(el) => { + // we have the potential to completely eliminate working on this node in the future(!) + // + // This can only be done if all of the elements properties (attrs, children, listeners, etc) are static + // While creating these things, keep track if we can memoize this element. + // At the end, we'll set this flag on the element to skip it + let mut is_static: bool = true; + + let VElement { + tag_name, + listeners, + attributes, + children, + namespace, + static_attrs: _, + static_children: _, + static_listeners: _, + dom_id, + } = el; + + let real_id = self.vdom.reserve_node(); + self.edit_create_element(tag_name, *namespace, real_id); + dom_id.set(Some(real_id)); + + let cur_scope = self.current_scope().unwrap(); + + listeners.iter().for_each(|listener| { + self.fix_listener(listener); + listener.mounted_node.set(Some(real_id)); + self.edit_new_event_listener(listener, cur_scope.clone()); + + // if the node has an event listener, then it must be visited ? + is_static = false; + }); + + for attr in *attributes { + is_static = is_static && attr.is_static; + self.edit_set_attribute(attr); + } + + // Fast path: if there is a single text child, it is faster to + // create-and-append the text node all at once via setting the + // parent's `textContent` in a single change list instruction than + // to emit three instructions to (1) create a text node, (2) set its + // text content, and finally (3) append the text node to this + // parent. + // + // Notice: this is a web-specific optimization and may be changed in the future + // + // TODO move over + // if children.len() == 1 { + // if let VNodeKind::Text(text) = &children[0].kind { + // self.set_text(text.text); + // return CreateMeta::new(is_static, 1); + // } + // } + + for child in *children { + let child_meta = self.create_vnode(child); + is_static = is_static && child_meta.is_static; + + // append whatever children were generated by this call + self.edit_append_children(child_meta.added_to_stack); + } + + CreateMeta::new(is_static, 1) + } + + VNodeKind::Component(vcomponent) => { + let caller = vcomponent.caller.clone(); + + let parent_idx = self.scope_stack.last().unwrap().clone(); + + // Insert a new scope into our component list + let new_idx = self.vdom.insert_scope_with_key(|new_idx| { + let parent_scope = self.get_scope(&parent_idx).unwrap(); + let height = parent_scope.height + 1; + Scope::new( + caller, + new_idx, + Some(parent_idx), + height, + ScopeChildren(vcomponent.children), + self.vdom.clone(), + ) + }); + + // Actually initialize the caller's slot with the right address + vcomponent.ass_scope.set(Some(new_idx)); + + if !vcomponent.can_memoize { + let cur_scope = self.get_scope_mut(&parent_idx).unwrap(); + let extended = *vcomponent as *const VComponent; + let extended: *const VComponent<'static> = + unsafe { std::mem::transmute(extended) }; + cur_scope.borrowed_props.borrow_mut().push(extended); + } + + // TODO: + // add noderefs to current noderef list Noderefs + // add effects to current effect list Effects + + let new_component = self.get_scope_mut(&new_idx).unwrap(); + + // Run the scope for one iteration to initialize it + match new_component.run_scope() { + Ok(_) => { + // all good, new nodes exist + } + Err(err) => { + // failed to run. this is the first time the component ran, and it failed + // we manually set its head node to an empty fragment + panic!("failing components not yet implemented"); + } + } + + // 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); + + // Run the creation algorithm with this scope on the stack + 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. + self.seen_scopes.insert(new_idx); + + CreateMeta::new(vcomponent.is_static, meta.added_to_stack) + } + + // Fragments are the only nodes that can contain dynamic content (IE through curlies or iterators). + // We can never ignore their contents, so the prescence of a fragment indicates that we need always diff them. + // Fragments will just put all their nodes onto the stack after creation + VNodeKind::Fragment(frag) => self.create_children(frag.children), + + VNodeKind::Suspended(VSuspended { node: real_node }) => { + let id = self.vdom.reserve_node(); + self.edit_create_placeholder(id); + real_node.set(Some(id)); + CreateMeta::new(false, 1) + } + } + } + + fn create_children(&mut self, children: &'bump [VNode<'bump>]) -> CreateMeta { + let mut is_static = true; + let mut added_to_stack = 0; + + // add them backwards + for child in children.iter().rev() { + let child_meta = self.create_vnode(child); + is_static = is_static && child_meta.is_static; + added_to_stack += child_meta.added_to_stack; + } + + CreateMeta { + is_static, + added_to_stack, + } + } + + /// Destroy a scope and all of its descendents. + /// + /// Calling this will run the destuctors on all hooks in the tree. + /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. + fn destroy_scopes(&mut self, old_scope: ScopeId) { + let mut nodes_to_delete = vec![old_scope]; + let mut scopes_to_explore = vec![old_scope]; + + // explore the scope tree breadth first + while let Some(scope_id) = scopes_to_explore.pop() { + // If we're planning on deleting this node, then we don't need to both rendering it + self.seen_scopes.insert(scope_id); + let scope = self.get_scope(&scope_id).unwrap(); + for child in scope.descendents.borrow().iter() { + // Add this node to be explored + scopes_to_explore.push(child.clone()); + + // Also add it for deletion + nodes_to_delete.push(child.clone()); + } + } + + // Delete all scopes that we found as part of this subtree + for node in nodes_to_delete { + log::debug!("Removing scope {:#?}", node); + let _scope = self.vdom.try_remove(node).unwrap(); + // do anything we need to do to delete the scope + // I think we need to run the destructors on the hooks + // TODO + } + } + + // 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. + fn diff_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + const IS_EMPTY: bool = true; + const IS_NOT_EMPTY: bool = false; + + 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) => { + let meta = self.create_children(new); + self.edit_append_children(meta.added_to_stack); + } + + // Completely removing old nodes and putting an anchor in its place + // no anchor (old has nodes) and the new is empty + // remove all the old nodes + (IS_NOT_EMPTY, IS_EMPTY) => { + for node in old { + self.remove_vnode(node); + } + } + + (IS_NOT_EMPTY, IS_NOT_EMPTY) => { + let first_old = &old[0]; + let first_new = &new[0]; + + match (&first_old.kind, &first_new.kind) { + // Anchors can only appear in empty fragments + (VNodeKind::Anchor(old_anchor), VNodeKind::Anchor(new_anchor)) => { + old_anchor.dom_id.set(new_anchor.dom_id.get()); + } + + // Replace the anchor with whatever new nodes are coming down the pipe + (VNodeKind::Anchor(anchor), _) => { + self.edit_push_root(anchor.dom_id.get().unwrap()); + let mut added = 0; + for el in new { + let meta = self.create_vnode(el); + added += meta.added_to_stack; + } + self.edit_replace_with(1, added); + } + + // Replace whatever nodes are sitting there with the anchor + (_, VNodeKind::Anchor(anchor)) => { + self.replace_and_create_many_with_many(old, [first_new]); + } + + // Use the complex diff algorithm to diff the nodes + _ => { + 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); + } + } + } + } + } + } + + // 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: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + if cfg!(debug_assertions) { + let mut keys = fxhash::FxHashSet::default(); + let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { + 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. + // + // TODO: just inline this + let shared_prefix_count = match self.diff_keyed_prefix(old, new) { + KeyedPrefixResult::Finished => return, + KeyedPrefixResult::MoreWorkToDo(count) => count, + }; + + // Next, we find out how many of the nodes at the end of the children have + // the same key. We do _not_ diff them yet, since we want to emit the change + // list instructions such that they can be applied in a single pass over the + // DOM. Instead, we just save this information for later. + // + // `shared_suffix_count` is the count of how many nodes at the end of `new` + // and `old` share the same keys. + let shared_suffix_count = old[shared_prefix_count..] + .iter() + .rev() + .zip(new[shared_prefix_count..].iter().rev()) + .take_while(|&(old, new)| old.key == new.key) + .count(); + + let old_shared_suffix_start = old.len() - shared_suffix_count; + let new_shared_suffix_start = new.len() - shared_suffix_count; + + // 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. + self.diff_keyed_middle( + &old[shared_prefix_count..old_shared_suffix_start], + &new[shared_prefix_count..new_shared_suffix_start], + shared_prefix_count, + shared_suffix_count, + old_shared_suffix_start, + ); + + // Finally, diff the nodes at the end of `old` and `new` that share keys. + let old_suffix = &old[old_shared_suffix_start..]; + let new_suffix = &new[new_shared_suffix_start..]; + debug_assert_eq!(old_suffix.len(), new_suffix.len()); + if !old_suffix.is_empty() { + self.diff_keyed_suffix(old_suffix, new_suffix, new_shared_suffix_start) + } + } + + // Diff the prefix of children in `new` and `old` that share the same keys in + // the same order. + // + // The stack is empty upon entry. + fn diff_keyed_prefix( + &mut self, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], + ) -> KeyedPrefixResult { + let mut shared_prefix_count = 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); + shared_prefix_count += 1; + } + + // If that was all of the old children, then create and append the remaining + // new children and we're finished. + if shared_prefix_count == old.len() { + // Load the last element + let last_node = self.find_last_element(new.last().unwrap()).direct_id(); + self.edit_push_root(last_node); + + // Create the new children and insert them after + let meta = self.create_children(&new[shared_prefix_count..]); + self.edit_insert_after(meta.added_to_stack); + + return KeyedPrefixResult::Finished; + } + + // And if that was all of the new children, then remove all of the remaining + // old children and we're finished. + if shared_prefix_count == new.len() { + self.remove_children(&old[shared_prefix_count..]); + return KeyedPrefixResult::Finished; + } + + KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) + } + + // Create the given children and append them to the parent node. + // + // The parent node must currently be on top of the change list stack: + // + // [... parent] + // + // When this function returns, the change list stack is in the same state. + pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { + for child in new { + let meta = self.create_vnode(child); + self.edit_append_children(meta.added_to_stack); + } + } + + // 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 state. + fn diff_keyed_middle( + &mut self, + old: &'bump [VNode<'bump>], + mut new: &'bump [VNode<'bump>], + shared_prefix_count: usize, + shared_suffix_count: usize, + old_shared_suffix_start: usize, + ) { + // 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`. + // 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 + .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 + .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 + } + } + }) + .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. + if shared_suffix_count == 0 && shared_keys.is_empty() { + self.replace_and_create_many_with_many(old, new); + 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; + // } + + // The longest increasing subsequence within `new_index_to_old_index`. This + // is the longest sequence on DOM nodes in `old` that are relatively ordered + // correctly within `new`. We will leave these nodes in place in the DOM, + // and only move nodes that are not part of the LIS. This results in the + // maximum number of DOM nodes left in place, AKA the minimum number of DOM + // nodes moved. + let mut new_index_is_in_lis = FxHashSet::default(); + new_index_is_in_lis.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 new_index_is_in_lis, + |a, b| a < b, + &mut predecessors, + &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 { + let new_node = &new[*id]; + let key = new_node.key().unwrap(); + let old_index = old_key_to_old_index.get(&key).unwrap(); + let old_node = &old[*old_index]; + self.diff_node(old_node, new_node); + } + + // return the old node from the key + let load_old_node_from_lsi = |key| -> &VNode { + let old_index = old_key_to_old_index.get(key).unwrap(); + let old_node = &old[*old_index]; + old_node + }; + + let mut root = None; + let mut new_iter = new.iter().enumerate(); + for lis_id in &lis_in_order { + eprintln!("tracking {:?}", lis_id); + // this is the next milestone node we are working up to + 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; + + 'inner: loop { + let (next_id, next_new) = new_iter.next().unwrap(); + if next_id == *lis_id { + // we've reached the milestone, break this loop so we can step to the next milestone + // remember: we already diffed this node + eprintln!("breaking {:?}", next_id); + break 'inner; + } else { + let key = next_new.key().unwrap(); + eprintln!("found key {:?}", key); + if shared_keys.contains(&key) { + eprintln!("key is contained {:?}", key); + shared_keys.remove(key); + // diff the two nodes + let old_node = load_old_node_from_lsi(key); + self.diff_node(old_node, next_new); + + // now move all the nodes into the right spot + for child in RealChildIterator::new(next_new, self.vdom) { + let el = child.direct_id(); + self.edit_push_root(el); + self.edit_insert_before(1); + } + } else { + eprintln!("key is not contained {:?}", key); + // new node needs to be created + // insert it before the current milestone + let meta = self.create_vnode(next_new); + self.edit_insert_before(meta.added_to_stack); + } + } + } + + self.edit_pop(); + } + + let final_lis_node = root.unwrap(); + let final_el_node = self.find_last_element(final_lis_node); + let final_el = final_el_node.direct_id(); + self.edit_push_root(final_el); + + let mut last_iter = new.iter().rev().enumerate(); + let last_key = final_lis_node.key().unwrap(); + loop { + let (last_id, last_node) = last_iter.next().unwrap(); + let key = last_node.key().unwrap(); + + eprintln!("checking final nodes {:?}", key); + + if last_key == key { + eprintln!("breaking final nodes"); + break; + } + + if shared_keys.contains(&key) { + eprintln!("key is contained {:?}", key); + shared_keys.remove(key); + // diff the two nodes + let old_node = load_old_node_from_lsi(key); + self.diff_node(old_node, last_node); + + // now move all the nodes into the right spot + for child in RealChildIterator::new(last_node, self.vdom) { + let el = child.direct_id(); + self.edit_push_root(el); + self.edit_insert_after(1); + } + } else { + eprintln!("key is not contained {:?}", key); + // new node needs to be created + // insert it before the current milestone + let meta = self.create_vnode(last_node); + self.edit_insert_after(meta.added_to_stack); + } + } + self.edit_pop(); + } + + // Diff the suffix of keyed children that share the same keys in the same order. + // + // The parent must be on the change list stack when we enter this function: + // + // [... parent] + // + // When this function exits, the change list stack remains the same. + fn diff_keyed_suffix( + &mut self, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], + new_shared_suffix_start: usize, + ) { + debug_assert_eq!(old.len(), new.len()); + debug_assert!(!old.is_empty()); + + for (old_child, new_child) in old.iter().zip(new.iter()) { + self.diff_node(old_child, new_child); + } + } + + // 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. + 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()); + debug_assert!(!old.is_empty()); + + match old.len().cmp(&new.len()) { + // old.len > new.len -> removing some nodes + Ordering::Greater => { + // diff them together + for (new_child, old_child) in new.iter().zip(old.iter()) { + self.diff_node(old_child, new_child); + } + + // todo: we would emit fewer instructions if we just did a replace many + // remove whatever is still dangling + for item in &old[new.len()..] { + for i in RealChildIterator::new(item, self.vdom) { + self.edit_push_root(i.direct_id()); + self.edit_remove(); + } + } + } + + // old.len < new.len -> adding some nodes + // this is wrong in the case where we're diffing fragments + // + // we need to save the last old element and then replace it with all the new ones + Ordering::Less => { + // Add the new elements to the last old element while it still exists + let last = self.find_last_element(old.last().unwrap()); + self.edit_push_root(last.direct_id()); + + // create the rest and insert them + let meta = self.create_children(&new[old.len()..]); + self.edit_insert_after(meta.added_to_stack); + + self.edit_pop(); + + // diff the rest + 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).await; + } + } + } + } + + // ====================== + // Support methods + // ====================== + // Remove all of a node's children. + // + // The change list stack must have this shape upon entry to this function: + // + // [... parent] + // + // When this function returns, the change list stack is in the same state. + fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { + // debug_assert!(self.traversal_is_committed()); + log::debug!("REMOVING CHILDREN"); + for _child in old { + // registry.remove_subtree(child); + } + // Fast way to remove all children: set the node's textContent to an empty + // string. + todo!() + // self.set_inner_text(""); + } + // Remove the current child and all of its following siblings. + // + // The change list stack must have this shape upon entry to this function: + // + // [... parent child] + // + // After the function returns, the child is no longer on the change list stack: + // + // [... parent] + fn remove_children(&mut self, old: &'bump [VNode<'bump>]) { + self.replace_and_create_many_with_many(old, None) + } + + fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { + let mut search_node = Some(vnode); + + loop { + let node = search_node.take().unwrap(); + match &node.kind { + // the ones that have a direct id + VNodeKind::Text(_) + | VNodeKind::Element(_) + | VNodeKind::Anchor(_) + | VNodeKind::Suspended(_) => break node, + + VNodeKind::Fragment(frag) => { + search_node = frag.children.last(); + } + VNodeKind::Component(el) => { + let scope_id = el.ass_scope.get().unwrap(); + let scope = self.get_scope(&scope_id).unwrap(); + search_node = Some(scope.root()); + } + } + } + } + + fn find_first_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { + let mut search_node = Some(vnode); + + loop { + let node = search_node.take().unwrap(); + match &node.kind { + // the ones that have a direct id + VNodeKind::Text(_) + | VNodeKind::Element(_) + | VNodeKind::Anchor(_) + | VNodeKind::Suspended(_) => break node, + + VNodeKind::Fragment(frag) => { + search_node = Some(&frag.children[0]); + } + VNodeKind::Component(el) => { + let scope_id = el.ass_scope.get().unwrap(); + let scope = self.get_scope(&scope_id).unwrap(); + search_node = Some(scope.root()); + } + } + } + } + + fn remove_child(&mut self, node: &'bump VNode<'bump>) { + self.replace_and_create_many_with_many(Some(node), None); + } + + /// Remove all the old nodes and replace them with newly created new nodes. + /// + /// The new nodes *will* be created - don't create them yourself! + fn replace_and_create_many_with_many( + &mut self, + old_nodes: impl IntoIterator>, + new_nodes: impl IntoIterator>, + ) { + let mut nodes_to_replace = Vec::new(); + let mut nodes_to_search = old_nodes.into_iter().collect::>(); + let mut scopes_obliterated = Vec::new(); + while let Some(node) = nodes_to_search.pop() { + match &node.kind { + // the ones that have a direct id return immediately + VNodeKind::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + VNodeKind::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + VNodeKind::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + VNodeKind::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), + + // Fragments will either have a single anchor or a list of children + VNodeKind::Fragment(frag) => { + for child in frag.children { + nodes_to_search.push(child); + } + } + + // Components can be any of the nodes above + // However, we do need to track which components need to be removed + VNodeKind::Component(el) => { + let scope_id = el.ass_scope.get().unwrap(); + let scope = self.get_scope(&scope_id).unwrap(); + let root = scope.root(); + nodes_to_search.push(root); + scopes_obliterated.push(scope_id); + } + } + // TODO: enable internal garabge collection + // self.create_garbage(node); + } + + let n = nodes_to_replace.len(); + for node in nodes_to_replace { + self.edit_push_root(node); + } + + let mut nodes_created = 0; + for node in new_nodes { + let meta = self.create_vnode(node); + nodes_created += meta.added_to_stack; + } + + // if 0 nodes are created, then it gets interperted as a deletion + self.edit_replace_with(n as u32, nodes_created); + + // obliterate! + for scope in scopes_obliterated { + self.destroy_scopes(scope); + } + } + + fn create_garbage(&mut self, node: &'bump VNode<'bump>) { + match self.current_scope().and_then(|id| self.get_scope(&id)) { + Some(scope) => { + let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) }; + scope.pending_garbage.borrow_mut().push(garbage); + } + None => { + log::info!("No scope to collect garbage into") + } + } + } + + fn immediately_dispose_garabage(&mut self, node: ElementId) { + self.vdom.collect_garbage(node) + } + + fn replace_node_with_node( + &mut self, + anchor: ElementId, + old_node: &'bump VNode<'bump>, + new_node: &'bump VNode<'bump>, + ) { + self.edit_push_root(anchor); + let meta = self.create_vnode(new_node); + self.edit_replace_with(1, meta.added_to_stack); + self.create_garbage(old_node); + self.edit_pop(); + } + + fn remove_vnode(&mut self, node: &'bump VNode<'bump>) { + match &node.kind { + VNodeKind::Text(el) => self.immediately_dispose_garabage(node.direct_id()), + VNodeKind::Element(el) => { + self.immediately_dispose_garabage(node.direct_id()); + for child in el.children { + self.remove_vnode(&child); + } + } + VNodeKind::Anchor(a) => { + // + } + VNodeKind::Fragment(frag) => { + for child in frag.children { + self.remove_vnode(&child); + } + } + VNodeKind::Component(el) => { + // + // self.destroy_scopes(old_scope) + } + VNodeKind::Suspended(_) => todo!(), + } + } + + fn current_scope(&self) -> Option { + self.scope_stack.last().map(|f| f.clone()) + } + + fn fix_listener<'a>(&mut self, listener: &'a Listener<'a>) { + let scope_id = self.current_scope(); + if let Some(scope_id) = scope_id { + let scope = self.get_scope(&scope_id).unwrap(); + let mut queue = scope.listeners.borrow_mut(); + let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; + queue.push(long_listener as *const _) + } + } + + pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'bump mut Scope> { + // ensure we haven't seen this scope before + // if we have, then we're trying to alias it, which is not allowed + debug_assert!(!self.seen_scopes.contains(id)); + + unsafe { self.vdom.get_scope_mut(*id) } + } + pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'bump Scope> { + // ensure we haven't seen this scope before + // if we have, then we're trying to alias it, which is not allowed + unsafe { self.vdom.get_scope(*id) } + } + + // Navigation + pub(crate) fn edit_push_root(&mut self, root: ElementId) { + let id = root.as_u64(); + self.mutations.edits.push(PushRoot { id }); + } + + pub(crate) fn edit_pop(&mut self) { + self.mutations.edits.push(PopRoot {}); + } + + // Add Nodes to the dom + // add m nodes from the stack + pub(crate) fn edit_append_children(&mut self, many: u32) { + self.mutations.edits.push(AppendChildren { many }); + } + + // replace the n-m node on the stack with the m nodes + // ends with the last element of the chain on the top of the stack + pub(crate) fn edit_replace_with(&mut self, n: u32, m: u32) { + self.mutations.edits.push(ReplaceWith { n, m }); + } + + pub(crate) fn edit_insert_after(&mut self, n: u32) { + self.mutations.edits.push(InsertAfter { n }); + } + + pub(crate) fn edit_insert_before(&mut self, n: u32) { + self.mutations.edits.push(InsertBefore { n }); + } + + // Remove Nodesfrom the dom + pub(crate) fn edit_remove(&mut self) { + self.mutations.edits.push(Remove); + } + + // Create + pub(crate) fn edit_create_text_node(&mut self, text: &'bump str, id: ElementId) { + let id = id.as_u64(); + self.mutations.edits.push(CreateTextNode { text, id }); + } + + pub(crate) fn edit_create_element( + &mut self, + tag: &'static str, + ns: Option<&'static str>, + id: ElementId, + ) { + let id = id.as_u64(); + match ns { + Some(ns) => self.mutations.edits.push(CreateElementNs { id, ns, tag }), + None => self.mutations.edits.push(CreateElement { id, tag }), + } + } + + // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom + pub(crate) fn edit_create_placeholder(&mut self, id: ElementId) { + let id = id.as_u64(); + self.mutations.edits.push(CreatePlaceholder { id }); + } + + // events + pub(crate) fn edit_new_event_listener(&mut self, listener: &Listener, scope: ScopeId) { + let Listener { + event, + mounted_node, + .. + } = listener; + + let element_id = mounted_node.get().unwrap().as_u64(); + + self.mutations.edits.push(NewEventListener { + scope, + event_name: event, + mounted_node_id: element_id, + }); + } + + pub(crate) fn edit_remove_event_listener(&mut self, event: &'static str) { + self.mutations.edits.push(RemoveEventListener { event }); + } + + // modify + pub(crate) fn edit_set_text(&mut self, text: &'bump str) { + self.mutations.edits.push(SetText { text }); + } + + pub(crate) fn edit_set_attribute(&mut self, attribute: &'bump Attribute) { + let Attribute { + name, + value, + is_static, + is_volatile, + namespace, + } = attribute; + // field: &'static str, + // value: &'bump str, + // ns: Option<&'static str>, + self.mutations.edits.push(SetAttribute { + field: name, + value, + ns: *namespace, + }); + } + + pub(crate) fn edit_set_attribute_ns( + &mut self, + attribute: &'bump Attribute, + namespace: &'bump str, + ) { + let Attribute { + name, + value, + is_static, + is_volatile, + // namespace, + .. + } = attribute; + // field: &'static str, + // value: &'bump str, + // ns: Option<&'static str>, + self.mutations.edits.push(SetAttribute { + field: name, + value, + ns: Some(namespace), + }); + } + + pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { + let name = attribute.name; + self.mutations.edits.push(RemoveAttribute { name }); + } +} + +// When we create new nodes, we need to propagate some information back up the call chain. +// This gives the caller some information on how to handle things like insertins, appending, and subtree discarding. +#[derive(Debug)] +pub struct CreateMeta { + pub is_static: bool, + pub added_to_stack: u32, +} + +impl CreateMeta { + fn new(is_static: bool, added_to_tack: u32) -> Self { + Self { + is_static, + added_to_stack: added_to_tack, + } + } +} + +enum KeyedPrefixResult { + // Fast path: we finished diffing all the children just by looking at the + // prefix of shared keys! + Finished, + // There is more diffing work to do. Here is a count of how many children at + // the beginning of `new` and `old` we already processed. + MoreWorkToDo(usize), +} + +fn find_first_real_node<'a>( + nodes: impl IntoIterator>, + scopes: &'a SharedResources, +) -> Option<&'a VNode<'a>> { + for node in nodes { + let mut iter = RealChildIterator::new(node, scopes); + if let Some(node) = iter.next() { + return Some(node); + } + } + + None +} + +/// This iterator iterates through a list of virtual children and only returns real children (Elements, Text, Anchors). +/// +/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like +/// "InsertBefore". +pub struct RealChildIterator<'a> { + scopes: &'a SharedResources, + + // Heuristcally we should never bleed into 4 completely nested fragments/components + // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane + // TODO: use const generics instead of the 4 estimation + stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 4]>, +} + +impl<'a> RealChildIterator<'a> { + pub fn new(starter: &'a VNode<'a>, scopes: &'a SharedResources) -> Self { + Self { + scopes, + stack: smallvec::smallvec![(0, starter)], + } + } + // keep the memory around + pub fn reset_with(&mut self, node: &'a VNode<'a>) { + self.stack.clear(); + self.stack.push((0, node)); + } +} + +impl<'a> Iterator for RealChildIterator<'a> { + type Item = &'a VNode<'a>; + + fn next(&mut self) -> Option<&'a VNode<'a>> { + let mut should_pop = false; + let mut returned_node: Option<&'a VNode<'a>> = None; + let mut should_push = None; + + while returned_node.is_none() { + if let Some((count, node)) = self.stack.last_mut() { + match &node.kind { + // We can only exit our looping when we get "real" nodes + // This includes fragments and components when they're empty (have a single root) + VNodeKind::Element(_) | VNodeKind::Text(_) => { + // We've recursed INTO an element/text + // We need to recurse *out* of it and move forward to the next + should_pop = true; + returned_node = Some(&*node); + } + + // If we get a fragment we push the next child + VNodeKind::Fragment(frag) => { + let subcount = *count as usize; + + if frag.children.len() == 0 { + should_pop = true; + returned_node = Some(&*node); + } + + if subcount >= frag.children.len() { + should_pop = true; + } else { + should_push = Some(&frag.children[subcount]); + } + } + // // If we get a fragment we push the next child + // VNodeKind::Fragment(frag) => { + // let subcount = *count as usize; + + // if frag.children.len() == 0 { + // should_pop = true; + // returned_node = Some(&*node); + // } + + // if subcount >= frag.children.len() { + // should_pop = true; + // } else { + // should_push = Some(&frag.children[subcount]); + // } + // } + + // Immediately abort suspended nodes - can't do anything with them yet + VNodeKind::Suspended(node) => { + // VNodeKind::Suspended => should_pop = true, + todo!() + } + + VNodeKind::Anchor(a) => { + todo!() + } + + // For components, we load their root and push them onto the stack + VNodeKind::Component(sc) => { + let scope = + unsafe { self.scopes.get_scope(sc.ass_scope.get().unwrap()) }.unwrap(); + // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); + + // Simply swap the current node on the stack with the root of the component + *node = scope.frames.fin_head(); + } + } + } else { + // If there's no more items on the stack, we're done! + return None; + } + + if should_pop { + self.stack.pop(); + if let Some((id, _)) = self.stack.last_mut() { + *id += 1; + } + should_pop = false; + } + + if let Some(push) = should_push { + self.stack.push((0, push)); + should_push = None; + } + } + + returned_node + } +} + +fn compare_strs(a: &str, b: &str) -> bool { + // Check by pointer, optimizing for static strs + if !std::ptr::eq(a, b) { + // If the pointers are different then check by value + a == b + } else { + true + } +} + +struct DfsIterator<'a> { + idx: usize, + node: Option<(&'a VNode<'a>, &'a VNode<'a>)>, + nodes: Option<(&'a [VNode<'a>], &'a [VNode<'a>])>, +} +impl<'a> Iterator for DfsIterator<'a> { + type Item = (&'a VNode<'a>, &'a VNode<'a>); + + fn next(&mut self) -> Option { + todo!() + } +} diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 95a8b8918..85ad7a8b1 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -285,7 +285,8 @@ impl Scheduler { let mut is_ready = || -> bool { (&mut deadline).now_or_never().is_some() }; // TODO: remove this unwrap - proprogate errors out - self.get_current_fiber().work(is_ready).unwrap() + // self.get_current_fiber().work(is_ready).unwrap() + todo!() } // waits for a trigger, canceling early if the deadline is reached diff --git a/packages/core/tests/iterative.rs b/packages/core/tests/iterative.rs new file mode 100644 index 000000000..295abbe81 --- /dev/null +++ b/packages/core/tests/iterative.rs @@ -0,0 +1,47 @@ +//! tests to prove that the iterative implementation works + +use anyhow::{Context, Result}; +use dioxus::{ + arena::SharedResources, + diff::{CreateMeta, DiffMachine}, + prelude::*, + scheduler::Mutations, + DomEdit, +}; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; + +#[test] +fn test_original_diff() { + static App: FC<()> = |cx| { + cx.render(rsx! { + div { + div { + "Hello, world!" + } + } + }) + }; + + let mut dom = VirtualDom::new(App); + let mutations = dom.rebuild().unwrap(); + dbg!(mutations); +} + +#[async_std::test] +async fn test_iterative_diff() { + static App: FC<()> = |cx| { + cx.render(rsx! { + div { + div { + "Hello, world!" + } + } + }) + }; + + let shared = SharedResources::new(); + + let mut machine = DiffMachine::new_headless(&shared); + let a = machine.work().await.unwrap(); +} From 64f289a61c18b6b4c1adf785a864171e51615780 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 20 Aug 2021 10:34:41 -0400 Subject: [PATCH 10/53] wip: back to vnode enum --- packages/core/src/diff.rs | 210 +++++++++++++++++---------------- packages/core/src/hooks.rs | 9 +- packages/core/src/lib.rs | 4 +- packages/core/src/nodes.rs | 190 ++++++++++++++--------------- packages/core/src/scheduler.rs | 14 +-- packages/ssr/src/lib.rs | 12 +- 6 files changed, 213 insertions(+), 226 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index ad1cabf83..9760e758f 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -85,7 +85,9 @@ use DomEdit::*; /// was origially implemented using recursive techniques, but Rust lacks the abilty to call async functions recursively, /// meaning we could not "pause" the diffing algorithm. /// -/// Instead, we use a traditional stack machine approach to diff and create new nodes. +/// Instead, we use a traditional stack machine approach to diff and create new nodes. The diff algorithm periodically +/// calls "yield_now" which allows the machine to pause and return control to the caller. The caller can then wait for +/// the next period of idle time, preventing our diff algorithm from blocking the maint thread. pub struct DiffMachine<'bump> { vdom: &'bump SharedResources, @@ -187,27 +189,27 @@ impl<'bump> DiffMachine<'bump> { } DiffInstruction::Create { node, .. } => { - match &node.kind { - VNodeKind::Text(text) => { + match &node { + VNode::Text(text) => { let real_id = self.vdom.reserve_node(); self.edit_create_text_node(text.text, real_id); text.dom_id.set(Some(real_id)); *self.nodes_created_stack.last_mut().unwrap() += 1; } - VNodeKind::Suspended(suspended) => { + VNode::Suspended(suspended) => { let real_id = self.vdom.reserve_node(); self.edit_create_placeholder(real_id); suspended.node.set(Some(real_id)); *self.nodes_created_stack.last_mut().unwrap() += 1; } - VNodeKind::Anchor(anchor) => { + VNode::Anchor(anchor) => { let real_id = self.vdom.reserve_node(); self.edit_create_placeholder(real_id); anchor.dom_id.set(Some(real_id)); *self.nodes_created_stack.last_mut().unwrap() += 1; } - VNodeKind::Element(el) => { + VNode::Element(el) => { let VElement { tag_name, listeners, @@ -218,6 +220,7 @@ impl<'bump> DiffMachine<'bump> { static_children: _, static_listeners: _, dom_id, + key, } = el; let real_id = self.vdom.reserve_node(); @@ -250,13 +253,13 @@ impl<'bump> DiffMachine<'bump> { } } - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { for node in frag.children { self.node_stack.push(DiffInstruction::Create { node }) } } - VNodeKind::Component(_) => { + VNode::Component(_) => { // } } @@ -267,50 +270,52 @@ impl<'bump> DiffMachine<'bump> { Ok(()) } + fn create_text_node(&mut self) {} + /// Create the new node, pushing instructions on our instruction stack to create any further children /// /// pub fn create_iterative(&mut self, node: &'bump VNode<'bump>) { - match &node.kind { + match &node { // singles // update the parent - VNodeKind::Text(text) => { + VNode::Text(text) => { let real_id = self.vdom.reserve_node(); self.edit_create_text_node(text.text, real_id); text.dom_id.set(Some(real_id)); *self.nodes_created_stack.last_mut().unwrap() += 1; } - VNodeKind::Suspended(suspended) => { + VNode::Suspended(suspended) => { let real_id = self.vdom.reserve_node(); self.edit_create_placeholder(real_id); suspended.node.set(Some(real_id)); *self.nodes_created_stack.last_mut().unwrap() += 1; } - VNodeKind::Anchor(anchor) => { + VNode::Anchor(anchor) => { let real_id = self.vdom.reserve_node(); self.edit_create_placeholder(real_id); anchor.dom_id.set(Some(real_id)); *self.nodes_created_stack.last_mut().unwrap() += 1; } - VNodeKind::Element(el) => { + VNode::Element(el) => { // } - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { // } - VNodeKind::Component(comp) => { + VNode::Component(comp) => { // } } } pub fn diff_iterative(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { - match (&old_node.kind, &new_node.kind) { + match (&old_node, &new_node) { // Handle the "sane" cases first. // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment". // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. - (VNodeKind::Text(old), VNodeKind::Text(new)) => { + (VNode::Text(old), VNode::Text(new)) => { let root = old_node.direct_id(); if old.text != new.text { @@ -322,7 +327,7 @@ impl<'bump> DiffMachine<'bump> { new.dom_id.set(Some(root)); } - (VNodeKind::Element(old), VNodeKind::Element(new)) => { + (VNode::Element(old), VNode::Element(new)) => { let root = old_node.direct_id(); // If the element type is completely different, the element needs to be re-rendered completely @@ -409,7 +414,7 @@ impl<'bump> DiffMachine<'bump> { self.diff_children(old.children, new.children); } - (VNodeKind::Component(old), VNodeKind::Component(new)) => { + (VNode::Component(old), VNode::Component(new)) => { let scope_addr = old.ass_scope.get().unwrap(); // Make sure we're dealing with the same component (by function pointer) @@ -464,7 +469,7 @@ impl<'bump> DiffMachine<'bump> { } } - (VNodeKind::Fragment(old), VNodeKind::Fragment(new)) => { + (VNode::Fragment(old), VNode::Fragment(new)) => { // 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 { @@ -475,7 +480,7 @@ impl<'bump> DiffMachine<'bump> { self.diff_children(old.children, new.children); } - (VNodeKind::Anchor(old), VNodeKind::Anchor(new)) => { + (VNode::Anchor(old), VNode::Anchor(new)) => { new.dom_id.set(old.dom_id.get()); } @@ -487,27 +492,27 @@ impl<'bump> DiffMachine<'bump> { // This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases // are pretty rare. IE replacing a list (component or fragment) with a single node. ( - VNodeKind::Component(_) - | VNodeKind::Fragment(_) - | VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_), - VNodeKind::Component(_) - | VNodeKind::Fragment(_) - | VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_), + VNode::Component(_) + | VNode::Fragment(_) + | VNode::Text(_) + | VNode::Element(_) + | VNode::Anchor(_), + VNode::Component(_) + | VNode::Fragment(_) + | VNode::Text(_) + | VNode::Element(_) + | VNode::Anchor(_), ) => { self.replace_and_create_many_with_many([old_node], [new_node]); } // TODO - (VNodeKind::Suspended(old), new) => { + (VNode::Suspended(old), new) => { // self.replace_and_create_many_with_many([old_node], [new_node]); } // a node that was once real is now suspended - (old, VNodeKind::Suspended(_)) => { + (old, VNode::Suspended(_)) => { // self.replace_and_create_many_with_many([old_node], [new_node]); } @@ -521,11 +526,11 @@ impl<'bump> DiffMachine<'bump> { // // 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>) { - match (&old_node.kind, &new_node.kind) { + match (&old_node, &new_node) { // Handle the "sane" cases first. // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment". // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. - (VNodeKind::Text(old), VNodeKind::Text(new)) => { + (VNode::Text(old), VNode::Text(new)) => { let root = old_node.direct_id(); if old.text != new.text { @@ -537,7 +542,7 @@ impl<'bump> DiffMachine<'bump> { new.dom_id.set(Some(root)); } - (VNodeKind::Element(old), VNodeKind::Element(new)) => { + (VNode::Element(old), VNode::Element(new)) => { let root = old_node.direct_id(); // If the element type is completely different, the element needs to be re-rendered completely @@ -624,7 +629,7 @@ impl<'bump> DiffMachine<'bump> { self.diff_children(old.children, new.children); } - (VNodeKind::Component(old), VNodeKind::Component(new)) => { + (VNode::Component(old), VNode::Component(new)) => { let scope_addr = old.ass_scope.get().unwrap(); // Make sure we're dealing with the same component (by function pointer) @@ -679,7 +684,7 @@ impl<'bump> DiffMachine<'bump> { } } - (VNodeKind::Fragment(old), VNodeKind::Fragment(new)) => { + (VNode::Fragment(old), VNode::Fragment(new)) => { // 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 { @@ -690,7 +695,7 @@ impl<'bump> DiffMachine<'bump> { self.diff_children(old.children, new.children); } - (VNodeKind::Anchor(old), VNodeKind::Anchor(new)) => { + (VNode::Anchor(old), VNode::Anchor(new)) => { new.dom_id.set(old.dom_id.get()); } @@ -702,27 +707,27 @@ impl<'bump> DiffMachine<'bump> { // This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases // are pretty rare. IE replacing a list (component or fragment) with a single node. ( - VNodeKind::Component(_) - | VNodeKind::Fragment(_) - | VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_), - VNodeKind::Component(_) - | VNodeKind::Fragment(_) - | VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_), + VNode::Component(_) + | VNode::Fragment(_) + | VNode::Text(_) + | VNode::Element(_) + | VNode::Anchor(_), + VNode::Component(_) + | VNode::Fragment(_) + | VNode::Text(_) + | VNode::Element(_) + | VNode::Anchor(_), ) => { self.replace_and_create_many_with_many([old_node], [new_node]); } // TODO - (VNodeKind::Suspended(old), new) => { + (VNode::Suspended(old), new) => { // self.replace_and_create_many_with_many([old_node], [new_node]); } // a node that was once real is now suspended - (old, VNodeKind::Suspended(_)) => { + (old, VNode::Suspended(_)) => { // self.replace_and_create_many_with_many([old_node], [new_node]); } @@ -739,22 +744,22 @@ impl<'bump> DiffMachine<'bump> { // // [... node] pub fn create_vnode(&mut self, node: &'bump VNode<'bump>) -> CreateMeta { - match &node.kind { - VNodeKind::Text(text) => { + match &node { + VNode::Text(text) => { let real_id = self.vdom.reserve_node(); self.edit_create_text_node(text.text, real_id); text.dom_id.set(Some(real_id)); CreateMeta::new(text.is_static, 1) } - VNodeKind::Anchor(anchor) => { + VNode::Anchor(anchor) => { let real_id = self.vdom.reserve_node(); self.edit_create_placeholder(real_id); anchor.dom_id.set(Some(real_id)); CreateMeta::new(false, 1) } - VNodeKind::Element(el) => { + VNode::Element(el) => { // we have the potential to completely eliminate working on this node in the future(!) // // This can only be done if all of the elements properties (attrs, children, listeners, etc) are static @@ -772,6 +777,7 @@ impl<'bump> DiffMachine<'bump> { static_children: _, static_listeners: _, dom_id, + key, } = el; let real_id = self.vdom.reserve_node(); @@ -805,7 +811,7 @@ impl<'bump> DiffMachine<'bump> { // // TODO move over // if children.len() == 1 { - // if let VNodeKind::Text(text) = &children[0].kind { + // if let VNodeKind::Text(text) = &children[0] { // self.set_text(text.text); // return CreateMeta::new(is_static, 1); // } @@ -822,7 +828,7 @@ impl<'bump> DiffMachine<'bump> { CreateMeta::new(is_static, 1) } - VNodeKind::Component(vcomponent) => { + VNode::Component(vcomponent) => { let caller = vcomponent.caller.clone(); let parent_idx = self.scope_stack.last().unwrap().clone(); @@ -895,9 +901,9 @@ impl<'bump> DiffMachine<'bump> { // Fragments are the only nodes that can contain dynamic content (IE through curlies or iterators). // We can never ignore their contents, so the prescence of a fragment indicates that we need always diff them. // Fragments will just put all their nodes onto the stack after creation - VNodeKind::Fragment(frag) => self.create_children(frag.children), + VNode::Fragment(frag) => self.create_children(frag.children), - VNodeKind::Suspended(VSuspended { node: real_node }) => { + VNode::Suspended(VSuspended { node: real_node }) => { let id = self.vdom.reserve_node(); self.edit_create_placeholder(id); real_node.set(Some(id)); @@ -993,14 +999,14 @@ impl<'bump> DiffMachine<'bump> { let first_old = &old[0]; let first_new = &new[0]; - match (&first_old.kind, &first_new.kind) { + match (&first_old, &first_new) { // Anchors can only appear in empty fragments - (VNodeKind::Anchor(old_anchor), VNodeKind::Anchor(new_anchor)) => { + (VNode::Anchor(old_anchor), VNode::Anchor(new_anchor)) => { old_anchor.dom_id.set(new_anchor.dom_id.get()); } // Replace the anchor with whatever new nodes are coming down the pipe - (VNodeKind::Anchor(anchor), _) => { + (VNode::Anchor(anchor), _) => { self.edit_push_root(anchor.dom_id.get().unwrap()); let mut added = 0; for el in new { @@ -1011,21 +1017,21 @@ impl<'bump> DiffMachine<'bump> { } // Replace whatever nodes are sitting there with the anchor - (_, VNodeKind::Anchor(anchor)) => { + (_, VNode::Anchor(anchor)) => { self.replace_and_create_many_with_many(old, [first_new]); } // Use the complex diff algorithm to diff the nodes _ => { - let new_is_keyed = new[0].key.is_some(); - let old_is_keyed = old[0].key.is_some(); + 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), + 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), + old.iter().all(|o| o.key().is_some() == old_is_keyed), "all siblings must be keyed or all siblings must be non-keyed" ); @@ -1062,7 +1068,7 @@ impl<'bump> DiffMachine<'bump> { let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { keys.clear(); for child in children { - let key = child.key; + let key = child.key(); debug_assert!( key.is_some(), "if any sibling is keyed, all siblings must be keyed" @@ -1102,7 +1108,7 @@ impl<'bump> DiffMachine<'bump> { .iter() .rev() .zip(new[shared_prefix_count..].iter().rev()) - .take_while(|&(old, new)| old.key == new.key) + .take_while(|&(old, new)| old.key() == new.key()) .count(); let old_shared_suffix_start = old.len() - shared_suffix_count; @@ -1543,17 +1549,16 @@ impl<'bump> DiffMachine<'bump> { loop { let node = search_node.take().unwrap(); - match &node.kind { + match &node { // the ones that have a direct id - VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_) - | VNodeKind::Suspended(_) => break node, + VNode::Text(_) | VNode::Element(_) | VNode::Anchor(_) | VNode::Suspended(_) => { + break node + } - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { search_node = frag.children.last(); } - VNodeKind::Component(el) => { + VNode::Component(el) => { let scope_id = el.ass_scope.get().unwrap(); let scope = self.get_scope(&scope_id).unwrap(); search_node = Some(scope.root()); @@ -1567,17 +1572,16 @@ impl<'bump> DiffMachine<'bump> { loop { let node = search_node.take().unwrap(); - match &node.kind { + match &node { // the ones that have a direct id - VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_) - | VNodeKind::Suspended(_) => break node, + VNode::Text(_) | VNode::Element(_) | VNode::Anchor(_) | VNode::Suspended(_) => { + break node + } - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { search_node = Some(&frag.children[0]); } - VNodeKind::Component(el) => { + VNode::Component(el) => { let scope_id = el.ass_scope.get().unwrap(); let scope = self.get_scope(&scope_id).unwrap(); search_node = Some(scope.root()); @@ -1602,15 +1606,15 @@ impl<'bump> DiffMachine<'bump> { let mut nodes_to_search = old_nodes.into_iter().collect::>(); let mut scopes_obliterated = Vec::new(); while let Some(node) = nodes_to_search.pop() { - match &node.kind { + match &node { // the ones that have a direct id return immediately - VNodeKind::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNodeKind::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNodeKind::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNodeKind::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), + VNode::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + VNode::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + VNode::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + VNode::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), // Fragments will either have a single anchor or a list of children - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { for child in frag.children { nodes_to_search.push(child); } @@ -1618,7 +1622,7 @@ impl<'bump> DiffMachine<'bump> { // Components can be any of the nodes above // However, we do need to track which components need to be removed - VNodeKind::Component(el) => { + VNode::Component(el) => { let scope_id = el.ass_scope.get().unwrap(); let scope = self.get_scope(&scope_id).unwrap(); let root = scope.root(); @@ -1680,27 +1684,27 @@ impl<'bump> DiffMachine<'bump> { } fn remove_vnode(&mut self, node: &'bump VNode<'bump>) { - match &node.kind { - VNodeKind::Text(el) => self.immediately_dispose_garabage(node.direct_id()), - VNodeKind::Element(el) => { + match &node { + VNode::Text(el) => self.immediately_dispose_garabage(node.direct_id()), + VNode::Element(el) => { self.immediately_dispose_garabage(node.direct_id()); for child in el.children { self.remove_vnode(&child); } } - VNodeKind::Anchor(a) => { + VNode::Anchor(a) => { // } - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { for child in frag.children { self.remove_vnode(&child); } } - VNodeKind::Component(el) => { + VNode::Component(el) => { // // self.destroy_scopes(old_scope) } - VNodeKind::Suspended(_) => todo!(), + VNode::Suspended(_) => todo!(), } } @@ -1941,10 +1945,10 @@ impl<'a> Iterator for RealChildIterator<'a> { while returned_node.is_none() { if let Some((count, node)) = self.stack.last_mut() { - match &node.kind { + match &node { // We can only exit our looping when we get "real" nodes // This includes fragments and components when they're empty (have a single root) - VNodeKind::Element(_) | VNodeKind::Text(_) => { + VNode::Element(_) | VNode::Text(_) => { // We've recursed INTO an element/text // We need to recurse *out* of it and move forward to the next should_pop = true; @@ -1952,7 +1956,7 @@ impl<'a> Iterator for RealChildIterator<'a> { } // If we get a fragment we push the next child - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { let subcount = *count as usize; if frag.children.len() == 0 { @@ -1983,17 +1987,17 @@ impl<'a> Iterator for RealChildIterator<'a> { // } // Immediately abort suspended nodes - can't do anything with them yet - VNodeKind::Suspended(node) => { + VNode::Suspended(node) => { // VNodeKind::Suspended => should_pop = true, todo!() } - VNodeKind::Anchor(a) => { + VNode::Anchor(a) => { todo!() } // For components, we load their root and push them onto the stack - VNodeKind::Component(sc) => { + VNode::Component(sc) => { let scope = unsafe { self.scopes.get_scope(sc.ass_scope.get().unwrap()) }.unwrap(); // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 29ecebe7d..3a0510ace 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -191,12 +191,9 @@ where } None => { // - Some(VNode { - key: None, - kind: VNodeKind::Suspended(VSuspended { - node: domnode.clone(), - }), - }) + Some(VNode::Suspended(VSuspended { + node: domnode.clone(), + })) } } }); diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index fa2de0c8c..d26d4c161 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,8 +12,8 @@ pub use crate::innerlude::{ format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, - EventTrigger, LazyNodes, NodeFactory, Properties, ScopeId, SuspendedContext, VNode, VNodeKind, - VirtualDom, VirtualEvent, FC, + EventTrigger, LazyNodes, NodeFactory, Properties, ScopeId, SuspendedContext, VNode, VirtualDom, + VirtualEvent, FC, }; pub mod prelude { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index fae6c31d4..865284794 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -16,37 +16,12 @@ use std::{ rc::Rc, }; -pub struct VNode<'src> { - pub kind: VNodeKind<'src>, - - pub(crate) key: Option<&'src str>, -} - -impl<'src> VNode<'src> { - pub fn key(&self) -> Option<&'src str> { - self.key - } - pub fn direct_id(&self) -> ElementId { - self.try_direct_id().unwrap() - } - pub fn try_direct_id(&self) -> Option { - match &self.kind { - VNodeKind::Text(el) => el.dom_id.get(), - VNodeKind::Element(el) => el.dom_id.get(), - VNodeKind::Anchor(el) => el.dom_id.get(), - VNodeKind::Fragment(_) => None, - VNodeKind::Component(_) => None, - VNodeKind::Suspended(_) => None, - } - } -} - /// Tools for the base unit of the virtual dom - the VNode /// VNodes are intended to be quickly-allocated, lightweight enum values. /// /// Components will be generating a lot of these very quickly, so we want to /// limit the amount of heap allocations / overly large enum sizes. -pub enum VNodeKind<'src> { +pub enum VNode<'src> { Text(VText<'src>), Element(&'src VElement<'src>), @@ -60,6 +35,32 @@ pub enum VNodeKind<'src> { Anchor(VAnchor), } +impl<'src> VNode<'src> { + pub fn key(&self) -> Option<&'src str> { + match &self { + VNode::Text(_) => todo!(), + VNode::Element(_) => todo!(), + VNode::Fragment(_) => todo!(), + VNode::Component(_) => todo!(), + VNode::Suspended(_) => todo!(), + VNode::Anchor(_) => todo!(), + } + } + pub fn direct_id(&self) -> ElementId { + self.try_direct_id().unwrap() + } + pub fn try_direct_id(&self) -> Option { + match &self { + VNode::Text(el) => el.dom_id.get(), + VNode::Element(el) => el.dom_id.get(), + VNode::Anchor(el) => el.dom_id.get(), + VNode::Fragment(_) => None, + VNode::Component(_) => None, + VNode::Suspended(_) => None, + } + } +} + pub struct VAnchor { pub dom_id: Cell>, } @@ -72,6 +73,7 @@ pub struct VText<'src> { pub struct VFragment<'src> { pub children: &'src [VNode<'src>], + pub key: Option<&'src str>, pub is_static: bool, } @@ -89,6 +91,8 @@ pub trait DioxusElement { } pub struct VElement<'a> { + pub key: Option<&'a str>, + // tag is always static pub tag_name: &'static str, pub namespace: Option<&'static str>, @@ -144,6 +148,8 @@ impl Listener<'_> { /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { + pub key: Option<&'src str>, + pub ass_scope: Cell>, pub(crate) caller: Rc DomTree>, @@ -196,26 +202,20 @@ impl<'a> NodeFactory<'a> { } pub fn unstable_place_holder() -> VNode<'static> { - VNode { - key: None, - kind: VNodeKind::Text(VText { - text: "", - dom_id: empty_cell(), - is_static: true, - }), - } + VNode::Text(VText { + text: "", + dom_id: empty_cell(), + is_static: true, + }) } /// Used in a place or two to make it easier to build vnodes from dummy text pub fn static_text(&self, text: &'static str) -> VNode<'a> { - VNode { - key: None, - kind: VNodeKind::Text(VText { - dom_id: empty_cell(), - text, - is_static: true, - }), - } + VNode::Text(VText { + dom_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 @@ -237,14 +237,12 @@ impl<'a> NodeFactory<'a> { /// pub fn text(&self, args: Arguments) -> VNode<'a> { let (text, is_static) = self.raw_text(args); - VNode { - key: None, - kind: VNodeKind::Text(VText { - text, - is_static, - dom_id: empty_cell(), - }), - } + + VNode::Text(VText { + text, + is_static, + dom_id: empty_cell(), + }) } pub fn element( @@ -295,31 +293,26 @@ impl<'a> NodeFactory<'a> { let key = key.map(|f| self.raw_text(f).0); - VNode { + VNode::Element(self.bump().alloc(VElement { + tag_name: tag, key, - kind: VNodeKind::Element(self.bump().alloc(VElement { - tag_name: tag, - namespace, - listeners, - attributes, - children, - dom_id: empty_cell(), + namespace, + listeners, + attributes, + children, + dom_id: empty_cell(), - // todo: wire up more constization - static_listeners: false, - static_attrs: false, - static_children: false, - })), - } + // todo: wire up more constization + static_listeners: false, + static_attrs: false, + static_children: false, + })) } pub fn suspended() -> VNode<'static> { - VNode { - key: None, - kind: VNodeKind::Suspended(VSuspended { - node: Rc::new(empty_cell()), - }), - } + VNode::Suspended(VSuspended { + node: Rc::new(empty_cell()), + }) } pub fn attr( @@ -422,20 +415,18 @@ impl<'a> NodeFactory<'a> { let key = key.map(|f| self.raw_text(f).0); - VNode { + VNode::Component(bump.alloc_with(|| VComponent { + user_fc, + comparator, + raw_props, + children, + caller: NodeFactory::create_component_caller(component, raw_props), + is_static, + drop_props: RefCell::new(Some(drop_props)), + can_memoize: P::IS_STATIC, + ass_scope: Cell::new(None), key, - kind: VNodeKind::Component(bump.alloc_with(|| VComponent { - user_fc, - comparator, - raw_props, - children, - caller: NodeFactory::create_component_caller(component, raw_props), - is_static, - drop_props: RefCell::new(Some(drop_props)), - can_memoize: P::IS_STATIC, - ass_scope: Cell::new(None), - })), - } + })) } pub fn create_component_caller<'g, P: 'g>( @@ -463,13 +454,11 @@ impl<'a> NodeFactory<'a> { pub fn fragment_from_iter(self, node_iter: impl IntoVNodeList<'a>) -> VNode<'a> { let children = node_iter.into_vnode_list(self); - VNode { + VNode::Fragment(VFragment { + children, key: None, - kind: VNodeKind::Fragment(VFragment { - children, - is_static: false, - }), - } + is_static: false, + }) } } @@ -525,12 +514,9 @@ where } if nodes.len() == 0 { - nodes.push(VNode { - kind: VNodeKind::Anchor(VAnchor { - dom_id: empty_cell(), - }), - key: None, - }); + nodes.push(VNode::Anchor(VAnchor { + dom_id: empty_cell(), + })); } nodes.into_bump_slice() @@ -666,14 +652,14 @@ impl Debug for NodeFactory<'_> { impl Debug for VNode<'_> { fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - match &self.kind { - VNodeKind::Element(el) => write!(s, "VElement {{ name: {} }}", el.tag_name), - VNodeKind::Text(t) => write!(s, "VText {{ text: {} }}", t.text), - VNodeKind::Anchor(a) => write!(s, "VAnchor"), + match &self { + VNode::Element(el) => write!(s, "VElement {{ name: {} }}", el.tag_name), + VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text), + VNode::Anchor(a) => write!(s, "VAnchor"), - VNodeKind::Fragment(_) => write!(s, "fragment"), - VNodeKind::Suspended { .. } => write!(s, "suspended"), - VNodeKind::Component(_) => write!(s, "component"), + VNode::Fragment(_) => write!(s, "fragment"), + VNode::Suspended { .. } => write!(s, "suspended"), + VNode::Component(_) => write!(s, "component"), } } } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 85ad7a8b1..22e55a994 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -354,31 +354,31 @@ impl Scheduler { } while let Some(node) = garbage_list.pop() { - match &node.kind { - VNodeKind::Text(_) => { + match &node { + VNode::Text(_) => { self.shared.collect_garbage(node.direct_id()); } - VNodeKind::Anchor(_) => { + VNode::Anchor(_) => { self.shared.collect_garbage(node.direct_id()); } - VNodeKind::Suspended(_) => { + VNode::Suspended(_) => { self.shared.collect_garbage(node.direct_id()); } - VNodeKind::Element(el) => { + VNode::Element(el) => { self.shared.collect_garbage(node.direct_id()); for child in el.children { garbage_list.push(child); } } - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { for child in frag.children { garbage_list.push(child); } } - VNodeKind::Component(comp) => { + VNode::Component(comp) => { // TODO: run the hook destructors and then even delete the scope let scope_id = comp.ass_scope.get().unwrap(); diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index bd323e77d..80a4c17ef 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -75,7 +75,7 @@ impl<'a> TextRenderer<'a> { fn html_render(&self, node: &VNode, f: &mut std::fmt::Formatter, il: u16) -> std::fmt::Result { match &node.kind { - VNodeKind::Text(text) => { + VNode::Text(text) => { if self.cfg.indent { for _ in 0..il { write!(f, " ")?; @@ -83,7 +83,7 @@ impl<'a> TextRenderer<'a> { } write!(f, "{}", text.text)? } - VNodeKind::Anchor(anchor) => { + VNode::Anchor(anchor) => { // if self.cfg.indent { for _ in 0..il { @@ -92,7 +92,7 @@ impl<'a> TextRenderer<'a> { } write!(f, "")?; } - VNodeKind::Element(el) => { + VNode::Element(el) => { if self.cfg.indent { for _ in 0..il { write!(f, " ")?; @@ -163,12 +163,12 @@ impl<'a> TextRenderer<'a> { write!(f, "\n")?; } } - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { for child in frag.children { self.html_render(child, f, il + 1)?; } } - VNodeKind::Component(vcomp) => { + VNode::Component(vcomp) => { let idx = vcomp.ass_scope.get().unwrap(); match (self.vdom, self.cfg.skip_components) { (Some(vdom), false) => { @@ -180,7 +180,7 @@ impl<'a> TextRenderer<'a> { } } } - VNodeKind::Suspended { .. } => { + VNode::Suspended { .. } => { // we can't do anything with suspended nodes } } From 7b068202cea3e513cf885826f303df7e4f707afd Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 20 Aug 2021 10:44:58 -0400 Subject: [PATCH 11/53] wip: remove old create --- packages/core/src/diff.rs | 424 +++++++++++++------------------------- 1 file changed, 144 insertions(+), 280 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 9760e758f..07f313020 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -103,6 +103,11 @@ pub struct DiffMachine<'bump> { pub seen_scopes: FxHashSet, } + +/// The stack instructions we use to diff and create new nodes. +/// +/// Right now, we insert an instruction for every child node we want to create and diff. This can be less efficient than +/// a custom iterator type - but this is current easier to implement. In the future, let's try interact with the stack less. pub enum DiffInstruction<'a> { DiffNode { old: &'a VNode<'a>, @@ -111,6 +116,7 @@ pub enum DiffInstruction<'a> { }, Append {}, + Replace { with: usize, }, @@ -189,76 +195,14 @@ impl<'bump> DiffMachine<'bump> { } DiffInstruction::Create { node, .. } => { + // defer to individual functions so the compiler produces better code + // large functions tend to be difficult for the compiler to work with match &node { - VNode::Text(text) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_text_node(text.text, real_id); - text.dom_id.set(Some(real_id)); - *self.nodes_created_stack.last_mut().unwrap() += 1; - } - VNode::Suspended(suspended) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_placeholder(real_id); - suspended.node.set(Some(real_id)); - *self.nodes_created_stack.last_mut().unwrap() += 1; - } - VNode::Anchor(anchor) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_placeholder(real_id); - anchor.dom_id.set(Some(real_id)); - *self.nodes_created_stack.last_mut().unwrap() += 1; - } - - VNode::Element(el) => { - let VElement { - tag_name, - listeners, - attributes, - children, - namespace, - static_attrs: _, - static_children: _, - static_listeners: _, - dom_id, - key, - } = el; - - let real_id = self.vdom.reserve_node(); - self.edit_create_element(tag_name, *namespace, real_id); - dom_id.set(Some(real_id)); - - let cur_scope = self.current_scope().unwrap(); - - listeners.iter().for_each(|listener| { - self.fix_listener(listener); - listener.mounted_node.set(Some(real_id)); - self.edit_new_event_listener(listener, cur_scope.clone()); - - // if the node has an event listener, then it must be visited ? - }); - - for attr in *attributes { - self.edit_set_attribute(attr); - } - - // TODO: the append child edit - - *self.nodes_created_stack.last_mut().unwrap() += 1; - - // push every child onto the stack - self.nodes_created_stack.push(0); - for child in *children { - self.node_stack - .push(DiffInstruction::Create { node: child }) - } - } - - VNode::Fragment(frag) => { - for node in frag.children { - self.node_stack.push(DiffInstruction::Create { node }) - } - } - + VNode::Text(vtext) => self.create_text_node(vtext), + VNode::Suspended(suspended) => self.create_suspended_node(suspended), + VNode::Anchor(anchor) => self.create_anchor_node(anchor), + VNode::Element(element) => self.create_element_node(element), + VNode::Fragment(frag) => self.create_fragment_node(frag), VNode::Component(_) => { // } @@ -270,44 +214,142 @@ impl<'bump> DiffMachine<'bump> { Ok(()) } - fn create_text_node(&mut self) {} + fn create_text_node(&mut self, vtext: &'bump VText<'bump>) { + let real_id = self.vdom.reserve_node(); + self.edit_create_text_node(vtext.text, real_id); + vtext.dom_id.set(Some(real_id)); + *self.nodes_created_stack.last_mut().unwrap() += 1; + } - /// Create the new node, pushing instructions on our instruction stack to create any further children - /// - /// - pub fn create_iterative(&mut self, node: &'bump VNode<'bump>) { - match &node { - // singles - // update the parent - VNode::Text(text) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_text_node(text.text, real_id); - text.dom_id.set(Some(real_id)); - *self.nodes_created_stack.last_mut().unwrap() += 1; - } - VNode::Suspended(suspended) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_placeholder(real_id); - suspended.node.set(Some(real_id)); - *self.nodes_created_stack.last_mut().unwrap() += 1; - } - VNode::Anchor(anchor) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_placeholder(real_id); - anchor.dom_id.set(Some(real_id)); - *self.nodes_created_stack.last_mut().unwrap() += 1; - } + fn create_suspended_node(&mut self, suspended: &'bump VSuspended) { + let real_id = self.vdom.reserve_node(); + self.edit_create_placeholder(real_id); + suspended.node.set(Some(real_id)); + *self.nodes_created_stack.last_mut().unwrap() += 1; + } - VNode::Element(el) => { - // + fn create_anchor_node(&mut self, anchor: &'bump VAnchor) { + let real_id = self.vdom.reserve_node(); + self.edit_create_placeholder(real_id); + anchor.dom_id.set(Some(real_id)); + *self.nodes_created_stack.last_mut().unwrap() += 1; + } + + fn create_element_node(&mut self, element: &'bump VElement<'bump>) { + let VElement { + tag_name, + listeners, + attributes, + children, + namespace, + static_attrs: _, + static_children: _, + static_listeners: _, + dom_id, + key, + } = element; + + let real_id = self.vdom.reserve_node(); + self.edit_create_element(tag_name, *namespace, real_id); + dom_id.set(Some(real_id)); + + let cur_scope = self.current_scope().unwrap(); + + listeners.iter().for_each(|listener| { + self.fix_listener(listener); + listener.mounted_node.set(Some(real_id)); + self.edit_new_event_listener(listener, cur_scope.clone()); + + // if the node has an event listener, then it must be visited ? + }); + + for attr in *attributes { + self.edit_set_attribute(attr); + } + + // TODO: the append child edit + + *self.nodes_created_stack.last_mut().unwrap() += 1; + + // push every child onto the stack + self.nodes_created_stack.push(0); + for child in *children { + self.node_stack + .push(DiffInstruction::Create { node: child }) + } + } + + fn create_fragment_node(&mut self, frag: &'bump VFragment<'bump>) { + for node in frag.children { + self.node_stack.push(DiffInstruction::Create { node }) + } + } + + fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { + let caller = vcomponent.caller.clone(); + + let parent_idx = self.scope_stack.last().unwrap().clone(); + + // Insert a new scope into our component list + let new_idx = self.vdom.insert_scope_with_key(|new_idx| { + let parent_scope = self.get_scope(&parent_idx).unwrap(); + let height = parent_scope.height + 1; + Scope::new( + caller, + new_idx, + Some(parent_idx), + height, + ScopeChildren(vcomponent.children), + self.vdom.clone(), + ) + }); + + // Actually initialize the caller's slot with the right address + vcomponent.ass_scope.set(Some(new_idx)); + + if !vcomponent.can_memoize { + let cur_scope = self.get_scope_mut(&parent_idx).unwrap(); + let extended = vcomponent as *const VComponent; + let extended: *const VComponent<'static> = unsafe { std::mem::transmute(extended) }; + cur_scope.borrowed_props.borrow_mut().push(extended); + } + + // TODO: + // add noderefs to current noderef list Noderefs + // add effects to current effect list Effects + + let new_component = self.get_scope_mut(&new_idx).unwrap(); + + // Run the scope for one iteration to initialize it + match new_component.run_scope() { + Ok(_) => { + // all good, new nodes exist } - VNode::Fragment(frag) => { - // - } - VNode::Component(comp) => { - // + Err(err) => { + // failed to run. this is the first time the component ran, and it failed + // we manually set its head node to an empty fragment + panic!("failing components not yet implemented"); } } + + // 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); + + // Run the creation algorithm with this scope on the stack + 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. + self.seen_scopes.insert(new_idx); } pub fn diff_iterative(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { @@ -734,184 +776,6 @@ impl<'bump> DiffMachine<'bump> { } } - // Emit instructions to create the given virtual node. - // - // The change list stack may have any shape upon entering this function: - // - // [...] - // - // When this function returns, the new node is on top of the change list stack: - // - // [... node] - pub fn create_vnode(&mut self, node: &'bump VNode<'bump>) -> CreateMeta { - match &node { - VNode::Text(text) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_text_node(text.text, real_id); - text.dom_id.set(Some(real_id)); - CreateMeta::new(text.is_static, 1) - } - - VNode::Anchor(anchor) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_placeholder(real_id); - anchor.dom_id.set(Some(real_id)); - CreateMeta::new(false, 1) - } - - VNode::Element(el) => { - // we have the potential to completely eliminate working on this node in the future(!) - // - // This can only be done if all of the elements properties (attrs, children, listeners, etc) are static - // While creating these things, keep track if we can memoize this element. - // At the end, we'll set this flag on the element to skip it - let mut is_static: bool = true; - - let VElement { - tag_name, - listeners, - attributes, - children, - namespace, - static_attrs: _, - static_children: _, - static_listeners: _, - dom_id, - key, - } = el; - - let real_id = self.vdom.reserve_node(); - self.edit_create_element(tag_name, *namespace, real_id); - dom_id.set(Some(real_id)); - - let cur_scope = self.current_scope().unwrap(); - - listeners.iter().for_each(|listener| { - self.fix_listener(listener); - listener.mounted_node.set(Some(real_id)); - self.edit_new_event_listener(listener, cur_scope.clone()); - - // if the node has an event listener, then it must be visited ? - is_static = false; - }); - - for attr in *attributes { - is_static = is_static && attr.is_static; - self.edit_set_attribute(attr); - } - - // Fast path: if there is a single text child, it is faster to - // create-and-append the text node all at once via setting the - // parent's `textContent` in a single change list instruction than - // to emit three instructions to (1) create a text node, (2) set its - // text content, and finally (3) append the text node to this - // parent. - // - // Notice: this is a web-specific optimization and may be changed in the future - // - // TODO move over - // if children.len() == 1 { - // if let VNodeKind::Text(text) = &children[0] { - // self.set_text(text.text); - // return CreateMeta::new(is_static, 1); - // } - // } - - for child in *children { - let child_meta = self.create_vnode(child); - is_static = is_static && child_meta.is_static; - - // append whatever children were generated by this call - self.edit_append_children(child_meta.added_to_stack); - } - - CreateMeta::new(is_static, 1) - } - - VNode::Component(vcomponent) => { - let caller = vcomponent.caller.clone(); - - let parent_idx = self.scope_stack.last().unwrap().clone(); - - // Insert a new scope into our component list - let new_idx = self.vdom.insert_scope_with_key(|new_idx| { - let parent_scope = self.get_scope(&parent_idx).unwrap(); - let height = parent_scope.height + 1; - Scope::new( - caller, - new_idx, - Some(parent_idx), - height, - ScopeChildren(vcomponent.children), - self.vdom.clone(), - ) - }); - - // Actually initialize the caller's slot with the right address - vcomponent.ass_scope.set(Some(new_idx)); - - if !vcomponent.can_memoize { - let cur_scope = self.get_scope_mut(&parent_idx).unwrap(); - let extended = *vcomponent as *const VComponent; - let extended: *const VComponent<'static> = - unsafe { std::mem::transmute(extended) }; - cur_scope.borrowed_props.borrow_mut().push(extended); - } - - // TODO: - // add noderefs to current noderef list Noderefs - // add effects to current effect list Effects - - let new_component = self.get_scope_mut(&new_idx).unwrap(); - - // Run the scope for one iteration to initialize it - match new_component.run_scope() { - Ok(_) => { - // all good, new nodes exist - } - Err(err) => { - // failed to run. this is the first time the component ran, and it failed - // we manually set its head node to an empty fragment - panic!("failing components not yet implemented"); - } - } - - // 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); - - // Run the creation algorithm with this scope on the stack - 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. - self.seen_scopes.insert(new_idx); - - CreateMeta::new(vcomponent.is_static, meta.added_to_stack) - } - - // Fragments are the only nodes that can contain dynamic content (IE through curlies or iterators). - // We can never ignore their contents, so the prescence of a fragment indicates that we need always diff them. - // Fragments will just put all their nodes onto the stack after creation - VNode::Fragment(frag) => self.create_children(frag.children), - - VNode::Suspended(VSuspended { node: real_node }) => { - let id = self.vdom.reserve_node(); - self.edit_create_placeholder(id); - real_node.set(Some(id)); - CreateMeta::new(false, 1) - } - } - } - fn create_children(&mut self, children: &'bump [VNode<'bump>]) -> CreateMeta { let mut is_static = true; let mut added_to_stack = 0; From 098776095844013469f68c0b585f75587c0edd75 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 20 Aug 2021 11:39:13 -0400 Subject: [PATCH 12/53] wip: moving over instructions --- packages/core/.vscode/settings.json | 2 +- packages/core/src/diff.rs | 700 +++++++++------------------- packages/core/src/virtual_dom.rs | 24 +- packages/core/src/yield_now.rs | 4 + packages/core/tests/iterative.rs | 10 + 5 files changed, 255 insertions(+), 485 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 07f313020..bfc8f31c8 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,9 +1,10 @@ -//! This module contains the stateful PriorityFiber and all methods to diff VNodes, their properties, and their children. +//! This module contains the stateful DiffMachine 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 +//! The [`DiffMachine`] 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, incremental diffing, cancelation, NodeRefs, and additional //! batching operations. @@ -23,12 +24,13 @@ //! and brick the user's page. //! //! ### Fragment Support -//! +//! -------------------- //! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments -//! can be particularly challenging when they are empty, so the placeholder node lets us "reserve" a spot for the empty +//! can be particularly challenging when they are empty, so the anchor node lets us "reserve" a spot for the empty //! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the NodeFactory - it is -//! 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. +//! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. Adding +//! "dummy" nodes _is_ inefficient, but it makes our diffing algorithm faster and the implementation is completely up to +//! the platform. //! //! 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. @@ -44,7 +46,7 @@ //! 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 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. +//! a way of short-circuiting the most mem-cmp expensive checks. //! //! ## Bloom Filter and Heuristics //! ------------------------------ @@ -62,7 +64,7 @@ //! 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. //! -//! Further Reading and Thoughts +//! ## Further Reading and Thoughts //! ---------------------------- //! There are more ways of increasing diff performance here that are currently not implemented. //! More info on how to improve this diffing algorithm: @@ -81,13 +83,16 @@ use DomEdit::*; /// Our DiffMachine is an iterative tree differ. /// -/// It uses techniques of a register-based Turing Machines to allow pausing and restarting of the diff algorithm. This +/// It uses techniques of a stack machine to allow pausing and restarting of the diff algorithm. This /// was origially implemented using recursive techniques, but Rust lacks the abilty to call async functions recursively, -/// meaning we could not "pause" the diffing algorithm. +/// meaning we could not "pause" the original diffing algorithm. /// /// Instead, we use a traditional stack machine approach to diff and create new nodes. The diff algorithm periodically /// calls "yield_now" which allows the machine to pause and return control to the caller. The caller can then wait for -/// the next period of idle time, preventing our diff algorithm from blocking the maint thread. +/// the next period of idle time, preventing our diff algorithm from blocking the main thread. +/// +/// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's +/// stack machines all the way down! pub struct DiffMachine<'bump> { vdom: &'bump SharedResources, @@ -112,7 +117,6 @@ pub enum DiffInstruction<'a> { DiffNode { old: &'a VNode<'a>, new: &'a VNode<'a>, - progress: usize, }, Append {}, @@ -143,22 +147,6 @@ impl<'bump> DiffMachine<'bump> { } } - /// Allows the creation of a diff machine without the concept of scopes or a virtualdom - /// this is mostly useful for testing - /// - /// This will PANIC if any component elements are passed in. - pub fn new_headless(shared: &'bump SharedResources) -> Self { - todo!() - // Self { - // node_stack: smallvec![], - // mutations: Mutations::new(), - // scope_stack: smallvec![ScopeId(0)], - // vdom: shared, - // diffed: FxHashSet::default(), - // seen_scopes: FxHashSet::default(), - // } - } - // pub async fn diff_scope(&mut self, id: ScopeId) -> Result<()> { let component = self.get_scope_mut(&id).ok_or_else(|| Error::NotMounted)?; @@ -173,15 +161,14 @@ impl<'bump> DiffMachine<'bump> { /// /// 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 - /// - /// - /// 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(mut make) = self.node_stack.pop() { match &mut make { - DiffInstruction::DiffNode { old, new, progress } => { - // + DiffInstruction::DiffNode { old, new, .. } => { + self.diff_node(old, new); } DiffInstruction::Append {} => { @@ -195,18 +182,7 @@ impl<'bump> DiffMachine<'bump> { } DiffInstruction::Create { node, .. } => { - // defer to individual functions so the compiler produces better code - // large functions tend to be difficult for the compiler to work with - match &node { - VNode::Text(vtext) => self.create_text_node(vtext), - VNode::Suspended(suspended) => self.create_suspended_node(suspended), - VNode::Anchor(anchor) => self.create_anchor_node(anchor), - VNode::Element(element) => self.create_element_node(element), - VNode::Fragment(frag) => self.create_fragment_node(frag), - VNode::Component(_) => { - // - } - } + self.create_node(node); } } } @@ -214,6 +190,21 @@ impl<'bump> DiffMachine<'bump> { Ok(()) } + // ================================= + // Tools for creating new nodes + // ================================= + + fn create_node(&mut self, node: &'bump VNode<'bump>) { + match node { + VNode::Text(vtext) => self.create_text_node(vtext), + VNode::Suspended(suspended) => self.create_suspended_node(suspended), + VNode::Anchor(anchor) => self.create_anchor_node(anchor), + VNode::Element(element) => self.create_element_node(element), + VNode::Fragment(frag) => self.create_fragment_node(frag), + VNode::Component(component) => self.create_component_node(component), + } + } + fn create_text_node(&mut self, vtext: &'bump VText<'bump>) { let real_id = self.vdom.reserve_node(); self.edit_create_text_node(vtext.text, real_id); @@ -338,459 +329,222 @@ impl<'bump> DiffMachine<'bump> { // Push the new scope onto the stack self.scope_stack.push(new_idx); - // Run the creation algorithm with this scope on the stack - let meta = self.create_vnode(nextnode); + todo!(); + // // Run the creation algorithm with this scope on the stack + // let meta = self.create_vnode(nextnode); - // pop the scope off the stack - self.scope_stack.pop(); + // // 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"); - } + // if meta.added_to_stack == 0 { + // panic!("Components should *always* generate nodes - even if they fail"); + // } - // Finally, insert this scope as a seen node. - self.seen_scopes.insert(new_idx); + // // Finally, insert this scope as a seen node. + // self.seen_scopes.insert(new_idx); } - pub fn diff_iterative(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { - match (&old_node, &new_node) { - // Handle the "sane" cases first. - // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment". - // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. - (VNode::Text(old), VNode::Text(new)) => { - let root = old_node.direct_id(); + // ================================= + // Tools for diffing nodes + // ================================= - if old.text != new.text { - self.edit_push_root(root); - self.edit_set_text(new.text); - self.edit_pop(); - } - - new.dom_id.set(Some(root)); - } - - (VNode::Element(old), VNode::Element(new)) => { - let root = old_node.direct_id(); - - // 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_name != old.tag_name || new.namespace != old.namespace { - self.replace_node_with_node(root, old_node, new_node); - return; - } - - new.dom_id.set(Some(root)); - - // Don't push the root if we don't have to - let mut has_comitted = false; - let mut please_commit = |edits: &mut Vec| { - if !has_comitted { - has_comitted = true; - edits.push(PushRoot { id: root.as_u64() }); - } - }; - - // 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 { - please_commit(&mut self.mutations.edits); - self.edit_set_attribute(new_attr); - } - } - } else { - // TODO: provide some sort of report on how "good" the diffing was - please_commit(&mut self.mutations.edits); - for attribute in old.attributes { - self.edit_remove_attribute(attribute); - } - for attribute in new.attributes { - self.edit_set_attribute(attribute) - } - } - - // 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 - let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); - 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 { - please_commit(&mut self.mutations.edits); - self.edit_remove_event_listener(old_l.event); - self.edit_new_event_listener(new_l, cur_scope); - } - new_l.mounted_node.set(old_l.mounted_node.get()); - self.fix_listener(new_l); - } - } else { - please_commit(&mut self.mutations.edits); - for listener in old.listeners { - self.edit_remove_event_listener(listener.event); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.edit_new_event_listener(listener, cur_scope); - - // Make sure the listener gets attached to the scope list - self.fix_listener(listener); - } - } - - if has_comitted { - self.edit_pop(); - } - - self.diff_children(old.children, new.children); - } - - (VNode::Component(old), VNode::Component(new)) => { - let scope_addr = old.ass_scope.get().unwrap(); - - // Make sure we're dealing with the same component (by function pointer) - if old.user_fc == new.user_fc { - // - self.scope_stack.push(scope_addr); - - // Make sure the new component vnode is referencing the right scope id - new.ass_scope.set(Some(scope_addr)); - - // make sure the component's caller function is up to date - let scope = self.get_scope_mut(&scope_addr).unwrap(); - - scope - .update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); - - // React doesn't automatically memoize, but we do. - let compare = old.comparator.unwrap(); - - match compare(new) { - true => { - // the props are the same... - } - false => { - // the props are different... - scope.run_scope().unwrap(); - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); - } - } - - self.scope_stack.pop(); - - self.seen_scopes.insert(scope_addr); - } else { - let mut old_iter = RealChildIterator::new(old_node, &self.vdom); - let first = old_iter - .next() - .expect("Components should generate a placeholder root"); - - // remove any leftovers - for to_remove in old_iter { - self.edit_push_root(to_remove.direct_id()); - self.edit_remove(); - } - - // seems like we could combine this into a single instruction.... - self.replace_node_with_node(first.direct_id(), old_node, new_node); - - // Wipe the old one and plant the new one - let old_scope = old.ass_scope.get().unwrap(); - self.destroy_scopes(old_scope); - } - } - - (VNode::Fragment(old), VNode::Fragment(new)) => { - // 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; - } - - self.diff_children(old.children, new.children); - } - - (VNode::Anchor(old), VNode::Anchor(new)) => { - new.dom_id.set(old.dom_id.get()); - } - - // The strategy here is to pick the first possible node from the previous set and use that as our replace with root - // - // We also walk the "real node" list to make sure all latent roots are claened up - // This covers the case any time a fragment or component shows up with pretty much anything else - // - // This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases - // are pretty rare. IE replacing a list (component or fragment) with a single node. - ( - VNode::Component(_) - | VNode::Fragment(_) - | VNode::Text(_) - | VNode::Element(_) - | VNode::Anchor(_), - VNode::Component(_) - | VNode::Fragment(_) - | VNode::Text(_) - | VNode::Element(_) - | VNode::Anchor(_), - ) => { - self.replace_and_create_many_with_many([old_node], [new_node]); - } - - // TODO - (VNode::Suspended(old), new) => { - // - self.replace_and_create_many_with_many([old_node], [new_node]); - } - // a node that was once real is now suspended - (old, VNode::Suspended(_)) => { - // - self.replace_and_create_many_with_many([old_node], [new_node]); - } - } - } - - // Diff the `old` node with the `new` node. Emits instructions to modify a - // physical DOM node that reflects `old` into something that reflects `new`. - // - // 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>) { - match (&old_node, &new_node) { - // Handle the "sane" cases first. - // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment". - // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. - (VNode::Text(old), VNode::Text(new)) => { - let root = old_node.direct_id(); + use VNode::*; + match (old_node, new_node) { + // Check the most common cases first + (Text(old), Text(new)) => self.diff_text_nodes(old, new), + (Element(old), Element(new)) => self.diff_element_nodes(old, new), + (Component(old), Component(new)) => self.diff_component_nodes(old, new), + (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), + (Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()), - if old.text != new.text { - self.edit_push_root(root); - self.edit_set_text(new.text); - self.edit_pop(); - } - - new.dom_id.set(Some(root)); - } - - (VNode::Element(old), VNode::Element(new)) => { - let root = old_node.direct_id(); - - // 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_name != old.tag_name || new.namespace != old.namespace { - self.replace_node_with_node(root, old_node, new_node); - return; - } - - new.dom_id.set(Some(root)); - - // Don't push the root if we don't have to - let mut has_comitted = false; - let mut please_commit = |edits: &mut Vec| { - if !has_comitted { - has_comitted = true; - edits.push(PushRoot { id: root.as_u64() }); - } - }; - - // 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 { - please_commit(&mut self.mutations.edits); - self.edit_set_attribute(new_attr); - } - } - } else { - // TODO: provide some sort of report on how "good" the diffing was - please_commit(&mut self.mutations.edits); - for attribute in old.attributes { - self.edit_remove_attribute(attribute); - } - for attribute in new.attributes { - self.edit_set_attribute(attribute) - } - } - - // 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 - let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); - 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 { - please_commit(&mut self.mutations.edits); - self.edit_remove_event_listener(old_l.event); - self.edit_new_event_listener(new_l, cur_scope); - } - new_l.mounted_node.set(old_l.mounted_node.get()); - self.fix_listener(new_l); - } - } else { - please_commit(&mut self.mutations.edits); - for listener in old.listeners { - self.edit_remove_event_listener(listener.event); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.edit_new_event_listener(listener, cur_scope); - - // Make sure the listener gets attached to the scope list - self.fix_listener(listener); - } - } - - if has_comitted { - self.edit_pop(); - } - - self.diff_children(old.children, new.children); - } - - (VNode::Component(old), VNode::Component(new)) => { - let scope_addr = old.ass_scope.get().unwrap(); - - // Make sure we're dealing with the same component (by function pointer) - if old.user_fc == new.user_fc { - // - self.scope_stack.push(scope_addr); - - // Make sure the new component vnode is referencing the right scope id - new.ass_scope.set(Some(scope_addr)); - - // make sure the component's caller function is up to date - let scope = self.get_scope_mut(&scope_addr).unwrap(); - - scope - .update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); - - // React doesn't automatically memoize, but we do. - let compare = old.comparator.unwrap(); - - match compare(new) { - true => { - // the props are the same... - } - false => { - // the props are different... - scope.run_scope().unwrap(); - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); - } - } - - self.scope_stack.pop(); - - self.seen_scopes.insert(scope_addr); - } else { - let mut old_iter = RealChildIterator::new(old_node, &self.vdom); - let first = old_iter - .next() - .expect("Components should generate a placeholder root"); - - // remove any leftovers - for to_remove in old_iter { - self.edit_push_root(to_remove.direct_id()); - self.edit_remove(); - } - - // seems like we could combine this into a single instruction.... - self.replace_node_with_node(first.direct_id(), old_node, new_node); - - // Wipe the old one and plant the new one - let old_scope = old.ass_scope.get().unwrap(); - self.destroy_scopes(old_scope); - } - } - - (VNode::Fragment(old), VNode::Fragment(new)) => { - // 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; - } - - self.diff_children(old.children, new.children); - } - - (VNode::Anchor(old), VNode::Anchor(new)) => { - new.dom_id.set(old.dom_id.get()); - } - - // The strategy here is to pick the first possible node from the previous set and use that as our replace with root - // - // We also walk the "real node" list to make sure all latent roots are claened up - // This covers the case any time a fragment or component shows up with pretty much anything else - // - // This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases - // are pretty rare. IE replacing a list (component or fragment) with a single node. ( - VNode::Component(_) - | VNode::Fragment(_) - | VNode::Text(_) - | VNode::Element(_) - | VNode::Anchor(_), - VNode::Component(_) - | VNode::Fragment(_) - | VNode::Text(_) - | VNode::Element(_) - | VNode::Anchor(_), + Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_), + Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_), ) => { self.replace_and_create_many_with_many([old_node], [new_node]); } - // TODO - (VNode::Suspended(old), new) => { - // + // TODO: these don't properly clean up any data + (Suspended(old), new) => { self.replace_and_create_many_with_many([old_node], [new_node]); } + // a node that was once real is now suspended - (old, VNode::Suspended(_)) => { - // + (old, Suspended(_)) => { self.replace_and_create_many_with_many([old_node], [new_node]); } } } - fn create_children(&mut self, children: &'bump [VNode<'bump>]) -> CreateMeta { - let mut is_static = true; - let mut added_to_stack = 0; + fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) { + let root = old.dom_id.get().unwrap(); - // add them backwards - for child in children.iter().rev() { - let child_meta = self.create_vnode(child); - is_static = is_static && child_meta.is_static; - added_to_stack += child_meta.added_to_stack; + if old.text != new.text { + self.edit_push_root(root); + self.edit_set_text(new.text); + self.edit_pop(); } - CreateMeta { - is_static, - added_to_stack, + new.dom_id.set(Some(root)); + } + + fn diff_element_nodes(&mut self, old: &'bump VElement<'bump>, new: &'bump VElement<'bump>) { + let root = old.dom_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_name != old.tag_name || new.namespace != old.namespace { + self.replace_node_with_node(root, old_node, new_node); + return; } + + new.dom_id.set(Some(root)); + + // Don't push the root if we don't have to + let mut has_comitted = false; + let mut please_commit = |edits: &mut Vec| { + if !has_comitted { + has_comitted = true; + edits.push(PushRoot { id: root.as_u64() }); + } + }; + + // 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 { + please_commit(&mut self.mutations.edits); + self.edit_set_attribute(new_attr); + } + } + } else { + // TODO: provide some sort of report on how "good" the diffing was + please_commit(&mut self.mutations.edits); + for attribute in old.attributes { + self.edit_remove_attribute(attribute); + } + for attribute in new.attributes { + self.edit_set_attribute(attribute) + } + } + + // 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 + let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); + 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 { + please_commit(&mut self.mutations.edits); + self.edit_remove_event_listener(old_l.event); + self.edit_new_event_listener(new_l, cur_scope); + } + new_l.mounted_node.set(old_l.mounted_node.get()); + self.fix_listener(new_l); + } + } else { + please_commit(&mut self.mutations.edits); + for listener in old.listeners { + self.edit_remove_event_listener(listener.event); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.edit_new_event_listener(listener, cur_scope); + + // Make sure the listener gets attached to the scope list + self.fix_listener(listener); + } + } + + if has_comitted { + self.edit_pop(); + } + + self.diff_children(old.children, new.children); + } + + fn diff_component_nodes( + &mut self, + old: &'bump VComponent<'bump>, + new: &'bump VComponent<'bump>, + ) { + let scope_addr = old.ass_scope.get().unwrap(); + + // Make sure we're dealing with the same component (by function pointer) + if old.user_fc == new.user_fc { + // + self.scope_stack.push(scope_addr); + + // Make sure the new component vnode is referencing the right scope id + new.ass_scope.set(Some(scope_addr)); + + // make sure the component's caller function is up to date + let scope = self.get_scope_mut(&scope_addr).unwrap(); + + scope.update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); + + // React doesn't automatically memoize, but we do. + let compare = old.comparator.unwrap(); + + match compare(new) { + true => { + // the props are the same... + } + false => { + // the props are different... + scope.run_scope().unwrap(); + self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); + } + } + + self.scope_stack.pop(); + + self.seen_scopes.insert(scope_addr); + } else { + todo!(); + + // let mut old_iter = RealChildIterator::new(old_node, &self.vdom); + // let first = old_iter + // .next() + // .expect("Components should generate a placeholder root"); + + // // remove any leftovers + // for to_remove in old_iter { + // self.edit_push_root(to_remove.direct_id()); + // self.edit_remove(); + // } + + // // seems like we could combine this into a single instruction.... + // self.replace_node_with_node(first.direct_id(), old_node, new_node); + + // // Wipe the old one and plant the new one + // let old_scope = old.ass_scope.get().unwrap(); + // self.destroy_scopes(old_scope); + } + } + + fn diff_fragment_nodes(&mut self, old: &'bump VFragment<'bump>, new: &'bump VFragment<'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]); + return; + } + + self.diff_children(old.children, new.children); } /// Destroy a scope and all of its descendents. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 78b4cbde3..195dafa8b 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -159,17 +159,19 @@ impl VirtualDom { .get_scope_mut(&self.base_scope) .expect("The base scope should never be moved"); - // 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() { - let meta = diff_machine.create_vnode(cur_component.frames.fin_head()); - diff_machine.edit_append_children(meta.added_to_stack); - } else { - // todo: should this be a hard error? - log::warn!( - "Component failed to run succesfully during rebuild. - This does not result in a failed rebuild, but indicates a logic failure within your app." - ); - } + todo!(); + + // // 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() { + // let meta = diff_machine.create_vnode(cur_component.frames.fin_head()); + // diff_machine.edit_append_children(meta.added_to_stack); + // } else { + // // todo: should this be a hard error? + // log::warn!( + // "Component failed to run succesfully during rebuild. + // This does not result in a failed rebuild, but indicates a logic failure within your app." + // ); + // } Ok(diff_machine.mutations) } diff --git a/packages/core/src/yield_now.rs b/packages/core/src/yield_now.rs index 2a985483e..8fef0848e 100644 --- a/packages/core/src/yield_now.rs +++ b/packages/core/src/yield_now.rs @@ -1,3 +1,7 @@ +//! Utility Code taken from async_std to immediately yield the current task to the executor. +//! +//! + use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; diff --git a/packages/core/tests/iterative.rs b/packages/core/tests/iterative.rs index 295abbe81..6ec318e61 100644 --- a/packages/core/tests/iterative.rs +++ b/packages/core/tests/iterative.rs @@ -45,3 +45,13 @@ async fn test_iterative_diff() { let mut machine = DiffMachine::new_headless(&shared); let a = machine.work().await.unwrap(); } + +#[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); + ///} +} From 2ce0752a9cd253fee1c8b205400aa191b09c9dcb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 21 Aug 2021 11:29:29 -0400 Subject: [PATCH 13/53] feat: fix compiling --- packages/core/.vscode/settings.json | 2 +- packages/core/src/diff.rs | 112 ++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 33 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 79505b018..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index bfc8f31c8..828c210c4 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -14,7 +14,7 @@ //! ### IDs for elements //! -------------------- //! 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 +//! We don't necessarily require that DOM changes happen instantly during the diffing process, so the implementor may choose //! 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. @@ -46,7 +46,7 @@ //! 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 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 mem-cmp expensive checks. +//! a way of short-circuiting the most expensive checks. //! //! ## Bloom Filter and Heuristics //! ------------------------------ @@ -119,15 +119,26 @@ pub enum DiffInstruction<'a> { new: &'a VNode<'a>, }, - Append {}, - - Replace { - with: usize, + DiffChildren { + progress: usize, + children: &'a [VNode<'a>], }, Create { node: &'a VNode<'a>, }, + CreateChildren { + progress: usize, + children: &'a [VNode<'a>], + }, + + // todo: merge this into the create instruction? + Append, + InsertAfter, + InsertBefore, + Replace { + with: usize, + }, } impl<'bump> DiffMachine<'bump> { @@ -165,25 +176,49 @@ impl<'bump> DiffMachine<'bump> { // 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(mut make) = self.node_stack.pop() { - match &mut make { + let mut should_pop = false; + while let Some(make) = self.node_stack.last_mut() { + should_pop = false; + match make { DiffInstruction::DiffNode { old, new, .. } => { + let (old, new) = (*old, *new); self.diff_node(old, new); } + DiffInstruction::DiffChildren { progress, children } => { + // + } + + DiffInstruction::Create { node, .. } => { + let node = *node; + self.create_node(node); + } + DiffInstruction::CreateChildren { progress, children } => { + if let Some(child) = (children).get(*progress) { + *progress += 1; + self.create_node(child); + } else { + should_pop = true; + } + } + DiffInstruction::Append {} => { let many = self.nodes_created_stack.pop().unwrap(); self.edit_append_children(many as u32); } DiffInstruction::Replace { with } => { + let with = *with; let many = self.nodes_created_stack.pop().unwrap(); - self.edit_replace_with(*with as u32, many as u32); + + self.edit_replace_with(with as u32, many as u32); } - DiffInstruction::Create { node, .. } => { - self.create_node(node); - } + DiffInstruction::InsertAfter => todo!(), + DiffInstruction::InsertBefore => todo!(), + } + if should_pop { + self.node_stack.pop(); } } @@ -397,7 +432,8 @@ impl<'bump> DiffMachine<'bump> { // // This case is rather rare (typically only in non-keyed lists) if new.tag_name != old.tag_name || new.namespace != old.namespace { - self.replace_node_with_node(root, old_node, new_node); + todo!(); + // self.replace_node_with_node(root, old_node, new_node); return; } @@ -600,8 +636,11 @@ impl<'bump> DiffMachine<'bump> { // Completely adding new nodes, removing any placeholder if it exists (IS_EMPTY, IS_NOT_EMPTY) => { - let meta = self.create_children(new); - self.edit_append_children(meta.added_to_stack); + todo!(); + // let meta = todo!(); + // let meta = self.create_children(new); + // let meta = self.create_children(new); + // self.edit_append_children(meta.added_to_stack); } // Completely removing old nodes and putting an anchor in its place @@ -628,8 +667,9 @@ impl<'bump> DiffMachine<'bump> { self.edit_push_root(anchor.dom_id.get().unwrap()); let mut added = 0; for el in new { - let meta = self.create_vnode(el); - added += meta.added_to_stack; + todo!(); + // let meta = self.create_vnode(el); + // added += meta.added_to_stack; } self.edit_replace_with(1, added); } @@ -780,8 +820,10 @@ impl<'bump> DiffMachine<'bump> { self.edit_push_root(last_node); // Create the new children and insert them after - let meta = self.create_children(&new[shared_prefix_count..]); - self.edit_insert_after(meta.added_to_stack); + // + todo!(); + // let meta = self.create_children(&new[shared_prefix_count..]); + // self.edit_insert_after(meta.added_to_stack); return KeyedPrefixResult::Finished; } @@ -805,8 +847,9 @@ impl<'bump> DiffMachine<'bump> { // When this function returns, the change list stack is in the same state. pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { for child in new { - let meta = self.create_vnode(child); - self.edit_append_children(meta.added_to_stack); + todo!(); + // let meta = self.create_vnode(child); + // self.edit_append_children(meta.added_to_stack); } } @@ -990,8 +1033,9 @@ impl<'bump> DiffMachine<'bump> { eprintln!("key is not contained {:?}", key); // new node needs to be created // insert it before the current milestone - let meta = self.create_vnode(next_new); - self.edit_insert_before(meta.added_to_stack); + todo!(); + // let meta = self.create_vnode(next_new); + // self.edit_insert_before(meta.added_to_stack); } } } @@ -1034,8 +1078,9 @@ impl<'bump> DiffMachine<'bump> { eprintln!("key is not contained {:?}", key); // new node needs to be created // insert it before the current milestone - let meta = self.create_vnode(last_node); - self.edit_insert_after(meta.added_to_stack); + todo!(); + // let meta = self.create_vnode(last_node); + // self.edit_insert_after(meta.added_to_stack); } } self.edit_pop(); @@ -1108,8 +1153,9 @@ impl<'bump> DiffMachine<'bump> { self.edit_push_root(last.direct_id()); // create the rest and insert them - let meta = self.create_children(&new[old.len()..]); - self.edit_insert_after(meta.added_to_stack); + todo!(); + // let meta = self.create_children(&new[old.len()..]); + // self.edit_insert_after(meta.added_to_stack); self.edit_pop(); @@ -1259,8 +1305,9 @@ impl<'bump> DiffMachine<'bump> { let mut nodes_created = 0; for node in new_nodes { - let meta = self.create_vnode(node); - nodes_created += meta.added_to_stack; + todo!(); + // let meta = self.create_vnode(node); + // nodes_created += meta.added_to_stack; } // if 0 nodes are created, then it gets interperted as a deletion @@ -1295,9 +1342,10 @@ impl<'bump> DiffMachine<'bump> { new_node: &'bump VNode<'bump>, ) { self.edit_push_root(anchor); - let meta = self.create_vnode(new_node); - self.edit_replace_with(1, meta.added_to_stack); - self.create_garbage(old_node); + todo!(); + // let meta = self.create_vnode(new_node); + // self.edit_replace_with(1, meta.added_to_stack); + // self.create_garbage(old_node); self.edit_pop(); } From 24572b63deec590f810df0b95a12cbd5b0bd61f5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 21 Aug 2021 13:24:47 -0400 Subject: [PATCH 14/53] wip: tests added for new iterative --- packages/core/Cargo.toml | 2 + packages/core/src/diff.rs | 96 +++++++++++++++++------------ packages/core/src/nodes.rs | 12 ++-- packages/core/src/virtual_dom.rs | 52 ++++++++++++---- packages/core/tests/iterative.rs | 49 +++++++++++---- packages/core/tests/test_logging.rs | 49 +++++++++++++++ 6 files changed, 191 insertions(+), 69 deletions(-) create mode 100644 packages/core/tests/test_logging.rs diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index d0da2ff07..3a0c66c02 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -50,6 +50,8 @@ indexmap = "1.7.0" anyhow = "1.0.42" async-std = { version = "1.9.0", features = ["attributes"] } dioxus-html = { path = "../html" } +fern = { version = "0.6.0", features = ["colored"] } +simple_logger = "1.13.0" [features] diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 828c210c4..1c193d7e4 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -100,7 +100,7 @@ pub struct DiffMachine<'bump> { pub nodes_created_stack: SmallVec<[usize; 10]>, - pub node_stack: SmallVec<[DiffInstruction<'bump>; 10]>, + pub instructions: SmallVec<[DiffInstruction<'bump>; 10]>, pub scope_stack: SmallVec<[ScopeId; 5]>, @@ -113,6 +113,7 @@ pub struct DiffMachine<'bump> { /// /// Right now, we insert an instruction for every child node we want to create and diff. This can be less efficient than /// a custom iterator type - but this is current easier to implement. In the future, let's try interact with the stack less. +#[derive(Debug)] pub enum DiffInstruction<'a> { DiffNode { old: &'a VNode<'a>, @@ -121,7 +122,8 @@ pub enum DiffInstruction<'a> { DiffChildren { progress: usize, - children: &'a [VNode<'a>], + old: &'a [VNode<'a>], + new: &'a [VNode<'a>], }, Create { @@ -148,7 +150,7 @@ impl<'bump> DiffMachine<'bump> { shared: &'bump SharedResources, ) -> Self { Self { - node_stack: smallvec![], + instructions: smallvec![], nodes_created_stack: smallvec![], mutations: edits, scope_stack: smallvec![cur_scope], @@ -158,6 +160,12 @@ impl<'bump> DiffMachine<'bump> { } } + pub fn new_headless(shared: &'bump SharedResources) -> Self { + let edits = Mutations::new(); + let cur_scope = ScopeId(0); + Self::new(edits, cur_scope, shared) + } + // pub async fn diff_scope(&mut self, id: ScopeId) -> Result<()> { let component = self.get_scope_mut(&id).ok_or_else(|| Error::NotMounted)?; @@ -176,50 +184,60 @@ impl<'bump> DiffMachine<'bump> { // 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 - let mut should_pop = false; - while let Some(make) = self.node_stack.last_mut() { - should_pop = false; - match make { + while let Some(instruction) = self.instructions.last_mut() { + log::debug!("Handling diff instruction: {:?}", instruction); + match instruction { DiffInstruction::DiffNode { old, new, .. } => { let (old, new) = (*old, *new); + self.instructions.pop(); self.diff_node(old, new); } - DiffInstruction::DiffChildren { progress, children } => { - // + // this is slightly more complicated, we need to find a way to pause our LIS code + DiffInstruction::DiffChildren { progress, old, new } => { + todo!() } 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(child) = children.get(*progress) { *progress += 1; self.create_node(child); } else { - should_pop = true; + self.instructions.pop(); } } - DiffInstruction::Append {} => { + DiffInstruction::Append => { let many = self.nodes_created_stack.pop().unwrap(); self.edit_append_children(many as u32); + self.instructions.pop(); } DiffInstruction::Replace { with } => { let with = *with; let many = self.nodes_created_stack.pop().unwrap(); - self.edit_replace_with(with as u32, many as u32); + self.instructions.pop(); } - DiffInstruction::InsertAfter => todo!(), - DiffInstruction::InsertBefore => todo!(), - } - if should_pop { - self.node_stack.pop(); - } + DiffInstruction::InsertAfter => { + let n = self.nodes_created_stack.pop().unwrap(); + self.edit_insert_after(n as u32); + self.instructions.pop(); + } + + DiffInstruction::InsertBefore => { + let n = self.nodes_created_stack.pop().unwrap(); + self.edit_insert_before(n as u32); + self.instructions.pop(); + } + }; } Ok(()) @@ -268,15 +286,13 @@ impl<'bump> DiffMachine<'bump> { attributes, children, namespace, - static_attrs: _, - static_children: _, - static_listeners: _, dom_id, - key, + .. } = element; 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(); @@ -285,30 +301,26 @@ impl<'bump> DiffMachine<'bump> { self.fix_listener(listener); listener.mounted_node.set(Some(real_id)); self.edit_new_event_listener(listener, cur_scope.clone()); - - // if the node has an event listener, then it must be visited ? }); for attr in *attributes { self.edit_set_attribute(attr); } - // TODO: the append child edit + self.instructions.push(DiffInstruction::Append); - *self.nodes_created_stack.last_mut().unwrap() += 1; - - // push every child onto the stack self.nodes_created_stack.push(0); - for child in *children { - self.node_stack - .push(DiffInstruction::Create { node: child }) - } + self.instructions.push(DiffInstruction::CreateChildren { + children, + progress: 0, + }); } fn create_fragment_node(&mut self, frag: &'bump VFragment<'bump>) { - for node in frag.children { - self.node_stack.push(DiffInstruction::Create { node }) - } + self.instructions.push(DiffInstruction::CreateChildren { + children: frag.children, + progress: 0, + }); } fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { @@ -348,7 +360,7 @@ impl<'bump> DiffMachine<'bump> { // Run the scope for one iteration to initialize it match new_component.run_scope() { - Ok(_) => { + Ok(_g) => { // all good, new nodes exist } Err(err) => { @@ -361,11 +373,13 @@ 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); - todo!(); // // Run the creation algorithm with this scope on the stack + self.instructions + .push(DiffInstruction::Create { node: nextnode }); + // let meta = self.create_vnode(nextnode); // // pop the scope off the stack @@ -376,7 +390,7 @@ impl<'bump> DiffMachine<'bump> { // } // // Finally, insert this scope as a seen node. - // self.seen_scopes.insert(new_idx); + self.seen_scopes.insert(new_idx); } // ================================= diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 865284794..05798f1ac 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -38,12 +38,12 @@ pub enum VNode<'src> { impl<'src> VNode<'src> { pub fn key(&self) -> Option<&'src str> { match &self { - VNode::Text(_) => todo!(), - VNode::Element(_) => todo!(), - VNode::Fragment(_) => todo!(), - VNode::Component(_) => todo!(), - VNode::Suspended(_) => todo!(), - VNode::Anchor(_) => todo!(), + VNode::Element(el) => el.key, + VNode::Component(c) => c.key, + VNode::Fragment(f) => f.key, + VNode::Text(_t) => None, + VNode::Suspended(s) => None, + VNode::Anchor(f) => None, } } pub fn direct_id(&self) -> ElementId { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 195dafa8b..23b43c210 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -159,23 +159,53 @@ impl VirtualDom { .get_scope_mut(&self.base_scope) .expect("The base scope should never be moved"); - todo!(); + // todo!(); // // 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() { - // let meta = diff_machine.create_vnode(cur_component.frames.fin_head()); - // diff_machine.edit_append_children(meta.added_to_stack); - // } else { - // // todo: should this be a hard error? - // log::warn!( - // "Component failed to run succesfully during rebuild. - // This does not result in a failed rebuild, but indicates a logic failure within your app." - // ); - // } + 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(), + }); + } else { + // todo: should this be a hard error? + log::warn!( + "Component failed to run succesfully during rebuild. + This does not result in a failed rebuild, but indicates a logic failure within your app." + ); + } Ok(diff_machine.mutations) } + pub async fn rebuild_async<'s>(&'s mut self) -> Result> { + let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); + + let cur_component = diff_machine + .get_scope_mut(&self.base_scope) + .expect("The base scope should never be moved"); + + // todo!(); + + // // 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(), + }); + 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!( + "Component failed to run succesfully during rebuild. + This does not result in a failed rebuild, but indicates a logic failure within your app." + ); + } + + Ok(diff_machine.mutations) + } /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. /// /// This method will not wait for any suspended nodes to complete. diff --git a/packages/core/tests/iterative.rs b/packages/core/tests/iterative.rs index 6ec318e61..27fe91e92 100644 --- a/packages/core/tests/iterative.rs +++ b/packages/core/tests/iterative.rs @@ -8,6 +8,7 @@ use dioxus::{ scheduler::Mutations, DomEdit, }; +mod test_logging; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; @@ -29,29 +30,55 @@ fn test_original_diff() { } #[async_std::test] -async fn test_iterative_diff() { +async fn test_iterative_create() { static App: FC<()> = |cx| { cx.render(rsx! { div { div { "Hello, world!" + div { + div { + Fragment { + "hello" + "world" + } + } + } } } }) }; - let shared = SharedResources::new(); + test_logging::set_up_logging(); - let mut machine = DiffMachine::new_headless(&shared); - let a = machine.work().await.unwrap(); + let mut dom = VirtualDom::new(App); + let mutations = dom.rebuild_async().await.unwrap(); + 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 fn test_iterative_create_list() { + static App: FC<()> = |cx| { + cx.render(rsx! { + {(0..10).map(|f| rsx!{ div { + "hello" + }})} + }) + }; + + 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 websys_loop() { +// ///loop { +// /// let deadline = request_idle_callback().await; +// /// let edits = dom.work(deadline); +// /// request_animation_frame().await; +// /// apply(edits); +// ///} +// } diff --git a/packages/core/tests/test_logging.rs b/packages/core/tests/test_logging.rs new file mode 100644 index 000000000..a8cf6926b --- /dev/null +++ b/packages/core/tests/test_logging.rs @@ -0,0 +1,49 @@ +pub fn set_up_logging() { + use fern::colors::{Color, ColoredLevelConfig}; + use log::debug; + + // configure colors for the whole line + let colors_line = ColoredLevelConfig::new() + .error(Color::Red) + .warn(Color::Yellow) + // we actually don't need to specify the color for debug and info, they are white by default + .info(Color::White) + .debug(Color::White) + // depending on the terminals color scheme, this is the same as the background color + .trace(Color::BrightBlack); + + // configure colors for the name of the level. + // since almost all of them are the same as the color for the whole line, we + // just clone `colors_line` and overwrite our changes + let colors_level = colors_line.clone().info(Color::Green); + // here we set up our fern Dispatch + fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "{color_line}[{level}{color_line}] {message}\x1B[0m", + color_line = format_args!( + "\x1B[{}m", + colors_line.get_color(&record.level()).to_fg_str() + ), + level = colors_level.color(record.level()), + message = message, + )); + }) + // set the default log level. to filter out verbose log messages from dependencies, set + // this to Warn and overwrite the log level for your crate. + .level(log::LevelFilter::Debug) + // .level(log::LevelFilter::Warn) + // change log levels for individual modules. Note: This looks for the record's target + // field which defaults to the module path but can be overwritten with the `target` + // parameter: + // `info!(target="special_target", "This log message is about special_target");` + // .level_for("dioxus", log::LevelFilter::Debug) + // .level_for("dioxus", log::LevelFilter::Info) + // .level_for("pretty_colored", log::LevelFilter::Trace) + // output to stdout + .chain(std::io::stdout()) + .apply() + .unwrap(); + + debug!("finished setting up logging! yay!"); +} From 1622f484b34412110723645cffc4c8c286b6ca80 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 21 Aug 2021 23:04:34 -0400 Subject: [PATCH 15/53] wip: basic creates working properly --- packages/core/src/diff.rs | 322 ++++++++++++++++------------ packages/core/src/virtual_dom.rs | 7 +- packages/core/tests/iterative.rs | 55 ++++- packages/core/tests/test_logging.rs | 2 - 4 files changed, 237 insertions(+), 149 deletions(-) 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!"); } From db6d0184aa5aa612bd7229a06f72b4c2ec5f6409 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 22 Aug 2021 17:08:25 -0400 Subject: [PATCH 16/53] feat: dedicated mutations module --- packages/core/Cargo.toml | 14 +- packages/core/benches/create.rs | 22 + packages/core/src/diff.rs | 572 +++++++----------- packages/core/src/editor.rs | 2 +- packages/core/src/hooklist.rs | 28 +- packages/core/src/lib.rs | 8 +- packages/core/src/mutations.rs | 154 +++++ packages/core/src/nodes.rs | 50 +- packages/core/src/scheduler.rs | 50 +- packages/core/src/scope.rs | 8 +- packages/core/src/util.rs | 4 + packages/core/src/virtual_dom.rs | 59 +- .../{iterative.rs => create_iterative.rs} | 4 +- packages/core/tests/debugdiff.rs | 2 + packages/core/tests/diff_iterative.rs | 46 ++ packages/core/tests/diffing.rs | 67 +- packages/core/tests/work_sync.rs | 35 ++ 17 files changed, 610 insertions(+), 515 deletions(-) create mode 100644 packages/core/benches/create.rs create mode 100644 packages/core/src/mutations.rs rename packages/core/tests/{iterative.rs => create_iterative.rs} (97%) create mode 100644 packages/core/tests/debugdiff.rs create mode 100644 packages/core/tests/diff_iterative.rs create mode 100644 packages/core/tests/work_sync.rs diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 3a0c66c02..264a6c56d 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -28,11 +28,6 @@ longest-increasing-subsequence = "0.1.0" # internall used log = "0.4" -# # Serialize the Edits for use in Webview/Liveview instances -serde = { version = "1", features = ["derive"], optional = true } - -appendlist = "1.4.0" - futures-util = "0.3.15" smallvec = "1.6.1" @@ -43,12 +38,15 @@ futures-channel = "0.3.16" # used for noderefs once_cell = "1.8.0" -indexmap = "1.7.0" + +# # Serialize the Edits for use in Webview/Liveview instances +serde = { version = "1", features = ["derive"], optional = true } [dev-dependencies] anyhow = "1.0.42" async-std = { version = "1.9.0", features = ["attributes"] } +criterion = "0.3.5" dioxus-html = { path = "../html" } fern = { version = "0.6.0", features = ["colored"] } simple_logger = "1.13.0" @@ -57,3 +55,7 @@ simple_logger = "1.13.0" [features] default = ["serialize"] serialize = ["serde"] + +[[bench]] +name = "create" +harness = false diff --git a/packages/core/benches/create.rs b/packages/core/benches/create.rs new file mode 100644 index 000000000..2b0c26ef5 --- /dev/null +++ b/packages/core/benches/create.rs @@ -0,0 +1,22 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +fn fibonacci(n: u64) -> u64 { + match n { + 0 => 1, + 1 => 1, + n => fibonacci(n - 1) + fibonacci(n - 2), + } +} + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20)))); +} + +criterion_group!(benches, criterion_benchmark); + +fn main() { + benches(); + Criterion::default().configure_from_args().final_summary(); + // $crate::__warn_about_html_reports_feature(); + // $crate::__warn_about_cargo_bench_support_feature(); +} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f350da8b9..87282013c 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -84,9 +84,8 @@ //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ use crate::{arena::SharedResources, innerlude::*}; -use futures_util::Future; +use futures_util::{Future, FutureExt}; use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; -use indexmap::IndexSet; use smallvec::{smallvec, SmallVec}; use std::{ @@ -113,7 +112,7 @@ pub struct DiffMachine<'bump> { pub nodes_created_stack: SmallVec<[usize; 10]>, - pub instructions: SmallVec<[DiffInstruction<'bump>; 10]>, + pub instructions: Vec>, pub scope_stack: SmallVec<[ScopeId; 5]>, @@ -124,8 +123,7 @@ pub struct DiffMachine<'bump> { /// The stack instructions we use to diff and create new nodes. /// -/// Right now, we insert an instruction for every child node we want to create and diff. This can be less efficient than -/// a custom iterator type - but this is current easier to implement. In the future, let's try interact with the stack less. +/// These instructions are essentially mini state machines that stay on top of the stack until they are finished. #[derive(Debug)] pub enum DiffInstruction<'a> { DiffNode { @@ -138,28 +136,25 @@ pub enum DiffInstruction<'a> { new: &'a [VNode<'a>], }, - // diff two lists of equally sized children - DiffEqual { - progress: usize, - old: &'a [VNode<'a>], - new: &'a [VNode<'a>], - }, - Create { node: &'a VNode<'a>, and: MountType<'a>, }, - CreateChildren { - progress: usize, + Remove { + child: &'a VNode<'a>, + }, + + RemoveChildren { children: &'a [VNode<'a>], - and: MountType<'a>, }, Mount { and: MountType<'a>, }, + PopElement, + PopScope, } @@ -168,8 +163,8 @@ pub enum MountType<'a> { Absorb, Append, Replace { old: &'a VNode<'a> }, - InsertAfter { other_node: &'a VNode<'a> }, - InsertBefore { other_node: &'a VNode<'a> }, + InsertAfter { other_node: Option<&'a VNode<'a>> }, + InsertBefore { other_node: Option<&'a VNode<'a>> }, } impl<'bump> DiffMachine<'bump> { @@ -179,7 +174,7 @@ impl<'bump> DiffMachine<'bump> { shared: &'bump SharedResources, ) -> Self { Self { - instructions: smallvec![], + instructions: Vec::with_capacity(1000), nodes_created_stack: smallvec![], mutations: edits, scope_stack: smallvec![cur_scope], @@ -209,10 +204,9 @@ impl<'bump> DiffMachine<'bump> { /// /// 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() { + while let Some(instruction) = self.instructions.pop() { log::debug!("Handling diff instruction: {:?}", instruction); // todo: call this less frequently, there is a bit of overhead involved @@ -220,97 +214,42 @@ impl<'bump> DiffMachine<'bump> { match instruction { DiffInstruction::PopScope => { - self.instructions.pop(); self.scope_stack.pop(); } + DiffInstruction::PopElement => { + self.mutations.pop(); + } DiffInstruction::DiffNode { old, new, .. } => { - let (old, new) = (*old, *new); - self.instructions.pop(); - self.diff_node(old, new); } - DiffInstruction::DiffEqual { progress, old, new } => { - debug_assert_eq!(old.len(), new.len()); - - if let (Some(old_child), Some(new_child)) = - (old.get(*progress), new.get(*progress)) - { - *progress += 1; - self.diff_node(old_child, new_child); - } else { - self.instructions.pop(); - } - } - - // 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::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::CreateChildren { - progress, - children, - and, - } => { - let and = *and; - - if *progress == 0 { - self.nodes_created_stack.push(0); + DiffInstruction::Remove { child } => { + for child in RealChildIterator::new(child, self.vdom) { + self.mutations.push_root(child.direct_id()); + self.mutations.remove(); } + } - 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::RemoveChildren { children } => { + for child in RealChildIterator::new_from_slice(children, self.vdom) { + self.mutations.push_root(child.direct_id()); + self.mutations.remove(); } } 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(); + self.mount(and); } }; } @@ -318,6 +257,33 @@ impl<'bump> DiffMachine<'bump> { Ok(()) } + fn mount(&mut self, and: MountType) { + 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.mutations.edits.push(AppendChildren { + many: nodes_created as u32, + }); + } + MountType::Replace { old } => { + todo!() + // self.mutations.replace_with(with as u32, many as u32); + } + MountType::InsertAfter { other_node } => { + self.mutations.insert_after(nodes_created as u32); + } + + MountType::InsertBefore { other_node } => { + self.mutations.insert_before(nodes_created as u32); + } + } + } + // ================================= // Tools for creating new nodes // ================================= @@ -335,21 +301,21 @@ impl<'bump> DiffMachine<'bump> { fn create_text_node(&mut self, vtext: &'bump VText<'bump>) { let real_id = self.vdom.reserve_node(); - self.edit_create_text_node(vtext.text, real_id); + self.mutations.create_text_node(vtext.text, real_id); vtext.dom_id.set(Some(real_id)); *self.nodes_created_stack.last_mut().unwrap() += 1; } fn create_suspended_node(&mut self, suspended: &'bump VSuspended) { let real_id = self.vdom.reserve_node(); - self.edit_create_placeholder(real_id); + self.mutations.create_placeholder(real_id); suspended.node.set(Some(real_id)); *self.nodes_created_stack.last_mut().unwrap() += 1; } fn create_anchor_node(&mut self, anchor: &'bump VAnchor) { let real_id = self.vdom.reserve_node(); - self.edit_create_placeholder(real_id); + self.mutations.create_placeholder(real_id); anchor.dom_id.set(Some(real_id)); *self.nodes_created_stack.last_mut().unwrap() += 1; } @@ -366,7 +332,7 @@ impl<'bump> DiffMachine<'bump> { } = element; let real_id = self.vdom.reserve_node(); - self.edit_create_element(tag_name, *namespace, real_id); + self.mutations.create_element(tag_name, *namespace, real_id); *self.nodes_created_stack.last_mut().unwrap() += 1; @@ -377,28 +343,21 @@ impl<'bump> DiffMachine<'bump> { listeners.iter().for_each(|listener| { self.fix_listener(listener); listener.mounted_node.set(Some(real_id)); - self.edit_new_event_listener(listener, cur_scope.clone()); + self.mutations + .new_event_listener(listener, cur_scope.clone()); }); for attr in *attributes { - self.edit_set_attribute(attr); + self.mutations.set_attribute(attr); } if children.len() > 0 { - self.instructions.push(DiffInstruction::CreateChildren { - children, - progress: 0, - and: MountType::Append, - }); + self.create_children_instructions(children, 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, - }); + self.create_children_instructions(frag.children, MountType::Absorb); } fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { @@ -417,6 +376,7 @@ impl<'bump> DiffMachine<'bump> { height, ScopeChildren(vcomponent.children), self.vdom.clone(), + vcomponent.name, ) }); @@ -479,23 +439,13 @@ impl<'bump> DiffMachine<'bump> { (Component(old), Component(new)) => self.diff_component_nodes(old, new), (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), (Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()), + (Suspended(old), Suspended(new)) => new.node.set(old.node.get()), + // Anything else is just a basic replace and create ( - Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_), - Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_), - ) => { - self.replace_and_create_many_with_many([old_node], [new_node]); - } - - // TODO: these don't properly clean up any data - (Suspended(old), new) => { - self.replace_and_create_many_with_many([old_node], [new_node]); - } - - // a node that was once real is now suspended - (old, Suspended(_)) => { - self.replace_and_create_many_with_many([old_node], [new_node]); - } + Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), + Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), + ) => self.replace_and_create_many_with_many(old_node, new_node), } } @@ -503,9 +453,9 @@ impl<'bump> DiffMachine<'bump> { let root = old.dom_id.get().unwrap(); if old.text != new.text { - self.edit_push_root(root); - self.edit_set_text(new.text); - self.edit_pop(); + self.mutations.push_root(root); + self.mutations.set_text(new.text); + self.mutations.pop(); } new.dom_id.set(Some(root)); @@ -545,17 +495,17 @@ impl<'bump> DiffMachine<'bump> { for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { if old_attr.value != new_attr.value { please_commit(&mut self.mutations.edits); - self.edit_set_attribute(new_attr); + self.mutations.set_attribute(new_attr); } } } else { // TODO: provide some sort of report on how "good" the diffing was please_commit(&mut self.mutations.edits); for attribute in old.attributes { - self.edit_remove_attribute(attribute); + self.mutations.remove_attribute(attribute); } for attribute in new.attributes { - self.edit_set_attribute(attribute) + self.mutations.set_attribute(attribute) } } @@ -572,8 +522,8 @@ impl<'bump> DiffMachine<'bump> { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { if old_l.event != new_l.event { please_commit(&mut self.mutations.edits); - self.edit_remove_event_listener(old_l.event); - self.edit_new_event_listener(new_l, cur_scope); + self.mutations.remove_event_listener(old_l.event); + self.mutations.new_event_listener(new_l, cur_scope); } new_l.mounted_node.set(old_l.mounted_node.get()); self.fix_listener(new_l); @@ -581,11 +531,11 @@ impl<'bump> DiffMachine<'bump> { } else { please_commit(&mut self.mutations.edits); for listener in old.listeners { - self.edit_remove_event_listener(listener.event); + self.mutations.remove_event_listener(listener.event); } for listener in new.listeners { listener.mounted_node.set(Some(root)); - self.edit_new_event_listener(listener, cur_scope); + self.mutations.new_event_listener(listener, cur_scope); // Make sure the listener gets attached to the scope list self.fix_listener(listener); @@ -593,7 +543,7 @@ impl<'bump> DiffMachine<'bump> { } if has_comitted { - self.edit_pop(); + self.mutations.pop(); } self.diff_children(old.children, new.children); @@ -646,8 +596,8 @@ impl<'bump> DiffMachine<'bump> { // // remove any leftovers // for to_remove in old_iter { - // self.edit_push_root(to_remove.direct_id()); - // self.edit_remove(); + // self.mutations.push_root(to_remove.direct_id()); + // self.mutations.remove(); // } // // seems like we could combine this into a single instruction.... @@ -670,6 +620,26 @@ impl<'bump> DiffMachine<'bump> { self.diff_children(old.children, new.children); } + // ============================================= + // Utilites for creating new diff instructions + // ============================================= + + fn create_children_instructions( + &mut self, + children: &'bump [VNode<'bump>], + and: MountType<'bump>, + ) { + self.nodes_created_stack.push(0); + self.instructions.push(DiffInstruction::Mount { and }); + + for child in children.into_iter().rev() { + self.instructions.push(DiffInstruction::Create { + and: MountType::Absorb, + node: child, + }); + } + } + /// Destroy a scope and all of its descendents. /// /// Calling this will run the destuctors on all hooks in the tree. @@ -727,11 +697,7 @@ impl<'bump> DiffMachine<'bump> { // Completely adding new nodes, removing any placeholder if it exists (IS_EMPTY, IS_NOT_EMPTY) => { - self.instructions.push(DiffInstruction::CreateChildren { - children: new, - progress: 0, - and: MountType::Append, - }); + self.create_children_instructions(new, MountType::Append); } // Completely removing old nodes and putting an anchor in its place @@ -755,11 +721,10 @@ impl<'bump> DiffMachine<'bump> { // Replace the anchor with whatever new nodes are coming down the pipe (VNode::Anchor(anchor), _) => { - self.instructions.push(DiffInstruction::CreateChildren { - children: new, - progress: 0, - and: MountType::Replace { old: first_old }, - }); + self.create_children_instructions( + new, + MountType::Replace { old: first_old }, + ); } // Replace whatever nodes are sitting there with the anchor @@ -905,13 +870,13 @@ impl<'bump> DiffMachine<'bump> { if shared_prefix_count == old.len() { // Load the last element let last_node = self.find_last_element(new.last().unwrap()).direct_id(); - self.edit_push_root(last_node); + self.mutations.push_root(last_node); // Create the new children and insert them after // todo!(); // let meta = self.create_children(&new[shared_prefix_count..]); - // self.edit_insert_after(meta.added_to_stack); + // self.mutations.insert_after(meta.added_to_stack); return KeyedPrefixResult::Finished; } @@ -937,7 +902,7 @@ impl<'bump> DiffMachine<'bump> { for child in new { todo!(); // let meta = self.create_vnode(child); - // self.edit_append_children(meta.added_to_stack); + // self.mutations.append_children(meta.added_to_stack); } } @@ -1077,7 +1042,7 @@ impl<'bump> DiffMachine<'bump> { root = Some(new_anchor); // let anchor_el = self.find_first_element(new_anchor); - // self.edit_push_root(anchor_el.direct_id()); + // self.mutations.push_root(anchor_el.direct_id()); // // let mut pushed = false; 'inner: loop { @@ -1100,27 +1065,27 @@ impl<'bump> DiffMachine<'bump> { // now move all the nodes into the right spot for child in RealChildIterator::new(next_new, self.vdom) { let el = child.direct_id(); - self.edit_push_root(el); - self.edit_insert_before(1); + self.mutations.push_root(el); + self.mutations.insert_before(1); } } else { self.instructions.push(DiffInstruction::Create { node: next_new, and: MountType::InsertBefore { - other_node: new_anchor, + other_node: Some(new_anchor), }, }); } } } - self.edit_pop(); + self.mutations.pop(); } let final_lis_node = root.unwrap(); let final_el_node = self.find_last_element(final_lis_node); let final_el = final_el_node.direct_id(); - self.edit_push_root(final_el); + self.mutations.push_root(final_el); let mut last_iter = new.iter().rev().enumerate(); let last_key = final_lis_node.key().unwrap(); @@ -1145,8 +1110,8 @@ impl<'bump> DiffMachine<'bump> { // now move all the nodes into the right spot for child in RealChildIterator::new(last_node, self.vdom) { let el = child.direct_id(); - self.edit_push_root(el); - self.edit_insert_after(1); + self.mutations.push_root(el); + self.mutations.insert_after(1); } } else { eprintln!("key is not contained {:?}", key); @@ -1154,10 +1119,10 @@ impl<'bump> DiffMachine<'bump> { // insert it before the current milestone todo!(); // let meta = self.create_vnode(last_node); - // self.edit_insert_after(meta.added_to_stack); + // self.mutations.insert_after(meta.added_to_stack); } } - self.edit_pop(); + self.mutations.pop(); } // Diff the suffix of keyed children that share the same keys in the same order. @@ -1198,19 +1163,17 @@ impl<'bump> DiffMachine<'bump> { match old.len().cmp(&new.len()) { // old.len > new.len -> removing some nodes Ordering::Greater => { - // diff them together - for (new_child, old_child) in new.iter().zip(old.iter()) { - self.diff_node(old_child, new_child); + // Generate instructions to diff the existing elements + for (new_child, old_child) in new.iter().zip(old.iter()).rev() { + self.instructions.push(DiffInstruction::DiffNode { + new: new_child, + old: old_child, + }); } - // todo: we would emit fewer instructions if we just did a replace many - // remove whatever is still dangling - for item in &old[new.len()..] { - for i in RealChildIterator::new(item, self.vdom) { - self.edit_push_root(i.direct_id()); - self.edit_remove(); - } - } + self.instructions.push(DiffInstruction::RemoveChildren { + children: &old[new.len()..], + }); } // old.len < new.len -> adding some nodes @@ -1218,27 +1181,30 @@ impl<'bump> DiffMachine<'bump> { // // we need to save the last old element and then replace it with all the new ones Ordering::Less => { - // Add the new elements to the last old element while it still exists - let last = self.find_last_element(old.last().unwrap()); - self.edit_push_root(last.direct_id()); - - // create the rest and insert them - todo!(); - // let meta = self.create_children(&new[old.len()..]); - // self.edit_insert_after(meta.added_to_stack); - - self.edit_pop(); - - // diff the rest - for (new_child, old_child) in new.iter().zip(old.iter()) { - self.diff_node(old_child, new_child) + // Generate instructions to diff the existing elements + for (new_child, old_child) in new.iter().zip(old.iter()).rev() { + self.instructions.push(DiffInstruction::DiffNode { + new: new_child, + old: old_child, + }); } + + // Generate instructions to add in the new elements + self.create_children_instructions( + &new[old.len()..], + MountType::InsertAfter { + other_node: old.last(), + }, + ); } // 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); + for (new_child, old_child) in new.iter().zip(old.iter()).rev() { + self.instructions.push(DiffInstruction::DiffNode { + new: new_child, + old: old_child, + }); } } } @@ -1247,24 +1213,24 @@ impl<'bump> DiffMachine<'bump> { // ====================== // Support methods // ====================== - // Remove all of a node's children. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { - // debug_assert!(self.traversal_is_committed()); - log::debug!("REMOVING CHILDREN"); - for _child in old { - // registry.remove_subtree(child); - } - // Fast way to remove all children: set the node's textContent to an empty - // string. - todo!() - // self.set_inner_text(""); - } + // // Remove all of a node's children. + // // + // // The change list stack must have this shape upon entry to this function: + // // + // // [... parent] + // // + // // When this function returns, the change list stack is in the same state. + // fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { + // // debug_assert!(self.traversal_is_committed()); + // log::debug!("REMOVING CHILDREN"); + // for _child in old { + // // registry.remove_subtree(child); + // } + // // Fast way to remove all children: set the node's textContent to an empty + // // string. + // todo!() + // // self.set_inner_text(""); + // } // Remove the current child and all of its following siblings. // // The change list stack must have this shape upon entry to this function: @@ -1324,8 +1290,16 @@ impl<'bump> DiffMachine<'bump> { } } - fn remove_child(&mut self, node: &'bump VNode<'bump>) { - self.replace_and_create_many_with_many(Some(node), None); + // fn remove_child(&mut self, node: &'bump VNode<'bump>) { + // self.replace_and_create_many_with_many(Some(node), None); + // } + + fn replace_many_with_many(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + // + } + + fn replace_one_with_many(&mut self, old: &'bump VNode<'bump>, new: &'bump [VNode<'bump>]) { + // } /// Remove all the old nodes and replace them with newly created new nodes. @@ -1333,11 +1307,13 @@ impl<'bump> DiffMachine<'bump> { /// The new nodes *will* be created - don't create them yourself! fn replace_and_create_many_with_many( &mut self, - old_nodes: impl IntoIterator>, - new_nodes: impl IntoIterator>, + old_node: &'bump VNode<'bump>, + new_node: &'bump VNode<'bump>, + // old_nodes: impl IntoIterator>, + // new_nodes: impl IntoIterator>, ) { let mut nodes_to_replace = Vec::new(); - let mut nodes_to_search = old_nodes.into_iter().collect::>(); + let mut nodes_to_search = vec![old_node]; let mut scopes_obliterated = Vec::new(); while let Some(node) = nodes_to_search.pop() { match &node { @@ -1368,20 +1344,25 @@ impl<'bump> DiffMachine<'bump> { // self.create_garbage(node); } - let n = nodes_to_replace.len(); - for node in nodes_to_replace { - self.edit_push_root(node); - } + // let n = nodes_to_replace.len(); + // for node in nodes_to_replace { + // self.mutations.push_root(node); + // } - let mut nodes_created = 0; - for node in new_nodes { - todo!(); - // let meta = self.create_vnode(node); - // nodes_created += meta.added_to_stack; - } + // let mut nodes_created = 0; + // for node in new_nodes { + // todo!(); + // let meta = self.create_vnode(node); + // nodes_created += meta.added_to_stack; + // } // if 0 nodes are created, then it gets interperted as a deletion - self.edit_replace_with(n as u32, nodes_created); + // self.mutations.replace_with(n as u32, nodes_created); + + // self.instructions.push(DiffInstruction::CreateChildren { + // and: MountType::Replace { old: None }, + // children: + // }); // obliterate! for scope in scopes_obliterated { @@ -1411,12 +1392,12 @@ impl<'bump> DiffMachine<'bump> { old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>, ) { - self.edit_push_root(anchor); + self.mutations.push_root(anchor); todo!(); // let meta = self.create_vnode(new_node); - // self.edit_replace_with(1, meta.added_to_stack); + // self.mutations.replace_with(1, meta.added_to_stack); // self.create_garbage(old_node); - self.edit_pop(); + self.mutations.pop(); } fn remove_vnode(&mut self, node: &'bump VNode<'bump>) { @@ -1470,138 +1451,6 @@ impl<'bump> DiffMachine<'bump> { // if we have, then we're trying to alias it, which is not allowed unsafe { self.vdom.get_scope(*id) } } - - // Navigation - pub(crate) fn edit_push_root(&mut self, root: ElementId) { - let id = root.as_u64(); - self.mutations.edits.push(PushRoot { id }); - } - - pub(crate) fn edit_pop(&mut self) { - self.mutations.edits.push(PopRoot {}); - } - - // Add Nodes to the dom - // add m nodes from the stack - pub(crate) fn edit_append_children(&mut self, many: u32) { - self.mutations.edits.push(AppendChildren { many }); - } - - // replace the n-m node on the stack with the m nodes - // ends with the last element of the chain on the top of the stack - pub(crate) fn edit_replace_with(&mut self, n: u32, m: u32) { - self.mutations.edits.push(ReplaceWith { n, m }); - } - - pub(crate) fn edit_insert_after(&mut self, n: u32) { - self.mutations.edits.push(InsertAfter { n }); - } - - pub(crate) fn edit_insert_before(&mut self, n: u32) { - self.mutations.edits.push(InsertBefore { n }); - } - - // Remove Nodesfrom the dom - pub(crate) fn edit_remove(&mut self) { - self.mutations.edits.push(Remove); - } - - // Create - pub(crate) fn edit_create_text_node(&mut self, text: &'bump str, id: ElementId) { - let id = id.as_u64(); - self.mutations.edits.push(CreateTextNode { text, id }); - } - - pub(crate) fn edit_create_element( - &mut self, - tag: &'static str, - ns: Option<&'static str>, - id: ElementId, - ) { - let id = id.as_u64(); - match ns { - Some(ns) => self.mutations.edits.push(CreateElementNs { id, ns, tag }), - None => self.mutations.edits.push(CreateElement { id, tag }), - } - } - - // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom - pub(crate) fn edit_create_placeholder(&mut self, id: ElementId) { - let id = id.as_u64(); - self.mutations.edits.push(CreatePlaceholder { id }); - } - - // events - pub(crate) fn edit_new_event_listener(&mut self, listener: &Listener, scope: ScopeId) { - let Listener { - event, - mounted_node, - .. - } = listener; - - let element_id = mounted_node.get().unwrap().as_u64(); - - self.mutations.edits.push(NewEventListener { - scope, - event_name: event, - mounted_node_id: element_id, - }); - } - - pub(crate) fn edit_remove_event_listener(&mut self, event: &'static str) { - self.mutations.edits.push(RemoveEventListener { event }); - } - - // modify - pub(crate) fn edit_set_text(&mut self, text: &'bump str) { - self.mutations.edits.push(SetText { text }); - } - - pub(crate) fn edit_set_attribute(&mut self, attribute: &'bump Attribute) { - let Attribute { - name, - value, - is_static, - is_volatile, - namespace, - } = attribute; - // field: &'static str, - // value: &'bump str, - // ns: Option<&'static str>, - self.mutations.edits.push(SetAttribute { - field: name, - value, - ns: *namespace, - }); - } - - pub(crate) fn edit_set_attribute_ns( - &mut self, - attribute: &'bump Attribute, - namespace: &'bump str, - ) { - let Attribute { - name, - value, - is_static, - is_volatile, - // namespace, - .. - } = attribute; - // field: &'static str, - // value: &'bump str, - // ns: Option<&'static str>, - self.mutations.edits.push(SetAttribute { - field: name, - value, - ns: Some(namespace), - }); - } - - pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { - let name = attribute.name; - self.mutations.edits.push(RemoveAttribute { name }); - } } // When we create new nodes, we need to propagate some information back up the call chain. @@ -1664,6 +1513,14 @@ impl<'a> RealChildIterator<'a> { stack: smallvec::smallvec![(0, starter)], } } + + pub fn new_from_slice(nodes: &'a [VNode<'a>], scopes: &'a SharedResources) -> Self { + let mut stack = smallvec::smallvec![]; + for node in nodes { + stack.push((0, node)); + } + Self { scopes, stack } + } // keep the memory around pub fn reset_with(&mut self, node: &'a VNode<'a>) { self.stack.clear(); @@ -1774,16 +1631,3 @@ fn compare_strs(a: &str, b: &str) -> bool { true } } - -struct DfsIterator<'a> { - idx: usize, - node: Option<(&'a VNode<'a>, &'a VNode<'a>)>, - nodes: Option<(&'a [VNode<'a>], &'a [VNode<'a>])>, -} -impl<'a> Iterator for DfsIterator<'a> { - type Item = (&'a VNode<'a>, &'a VNode<'a>); - - fn next(&mut self) -> Option { - todo!() - } -} diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs index 3bc959b4a..b6cc620d8 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/editor.rs @@ -9,7 +9,7 @@ use crate::innerlude::ScopeId; /// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the /// network or through FFI boundaries. -#[derive(Debug)] +#[derive(Debug, PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), diff --git a/packages/core/src/hooklist.rs b/packages/core/src/hooklist.rs index 06ed54d32..3781fc2ed 100644 --- a/packages/core/src/hooklist.rs +++ b/packages/core/src/hooklist.rs @@ -1,10 +1,10 @@ use std::{ any::Any, - cell::{Cell, UnsafeCell}, + cell::{Cell, RefCell, UnsafeCell}, }; pub struct HookList { - vals: appendlist::AppendList>>, + vals: RefCell>>>, idx: Cell, } @@ -27,35 +27,17 @@ impl InnerHook { } } } - impl HookList { - /// Unsafely get a mutable reference to any of the hooks - /// - /// This is unsafe because an &mut T might be aliased if the hook data is already borrowed/in use in the component - /// - /// This method should be reserved for internal methods that are guaranteed that this hook is not aliased anyhwere - /// inside the component body, or outside into children components. - /// - /// This method is currently used only by the suspense system whose hook implementation guarantees that all &T is dropped - /// before the suspense handler is ran. - pub(crate) unsafe fn get_mut(&self, idx: usize) -> Option<&mut T> { - self.vals.get(idx).and_then(|inn| { - let raw_box = unsafe { &mut *inn.cell.get() }; - raw_box.downcast_mut::() - }) - } - pub(crate) fn next(&self) -> Option<&mut T> { - self.vals.get(self.idx.get()).and_then(|inn| { + self.vals.borrow().get(self.idx.get()).and_then(|inn| { self.idx.set(self.idx.get() + 1); let raw_box = unsafe { &mut *inn.cell.get() }; raw_box.downcast_mut::() }) } - #[inline] pub(crate) fn push(&self, new: T) { - self.vals.push(InnerHook::new(Box::new(new))) + self.vals.borrow_mut().push(InnerHook::new(Box::new(new))) } /// This resets the internal iterator count @@ -70,7 +52,7 @@ impl HookList { #[inline] pub(crate) fn len(&self) -> usize { - self.vals.len() + self.vals.borrow().len() } #[inline] diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index d26d4c161..09d287d90 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,15 +12,15 @@ pub use crate::innerlude::{ format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, - EventTrigger, LazyNodes, NodeFactory, Properties, ScopeId, SuspendedContext, VNode, VirtualDom, - VirtualEvent, FC, + EventTrigger, LazyNodes, Mutations, NodeFactory, Properties, ScopeId, SuspendedContext, VNode, + VirtualDom, VirtualEvent, FC, }; pub mod prelude { pub use crate::component::{fc_to_builder, Fragment, Properties}; pub use crate::context::Context; pub use crate::hooks::*; - pub use crate::innerlude::{DioxusElement, DomTree, LazyNodes, NodeFactory, FC}; + pub use crate::innerlude::{DioxusElement, DomTree, LazyNodes, Mutations, NodeFactory, FC}; pub use crate::nodes::VNode; pub use crate::VirtualDom; pub use dioxus_core_macro::{format_args_f, html, rsx, Props}; @@ -39,6 +39,7 @@ pub(crate) mod innerlude { pub use crate::heuristics::*; pub use crate::hooklist::*; pub use crate::hooks::*; + pub use crate::mutations::*; pub use crate::nodes::*; pub use crate::scheduler::*; pub use crate::scope::*; @@ -68,6 +69,7 @@ pub mod events; pub mod heuristics; pub mod hooklist; pub mod hooks; +pub mod mutations; pub mod nodes; pub mod scheduler; pub mod scope; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs new file mode 100644 index 000000000..ba43f9b4f --- /dev/null +++ b/packages/core/src/mutations.rs @@ -0,0 +1,154 @@ +use std::any::Any; + +use crate::innerlude::*; + +/// The "Mutations" object holds the changes that need to be made to the DOM. +/// +#[derive(Debug)] +pub struct Mutations<'s> { + pub edits: Vec>, + pub noderefs: Vec>, +} +use DomEdit::*; + +impl<'bump> Mutations<'bump> { + pub fn new() -> Self { + let edits = Vec::new(); + let noderefs = Vec::new(); + Self { edits, noderefs } + } + + pub fn extend(&mut self, other: &mut Mutations) {} + + // Navigation + pub(crate) fn push_root(&mut self, root: ElementId) { + let id = root.as_u64(); + self.edits.push(PushRoot { id }); + } + pub(crate) fn pop(&mut self) { + self.edits.push(PopRoot {}); + } + // replace the n-m node on the stack with the m nodes + // ends with the last element of the chain on the top of the stack + pub(crate) fn replace_with(&mut self, n: u32, m: u32) { + self.edits.push(ReplaceWith { n, m }); + } + pub(crate) fn insert_after(&mut self, n: u32) { + self.edits.push(InsertAfter { n }); + } + pub(crate) fn insert_before(&mut self, n: u32) { + self.edits.push(InsertBefore { n }); + } + // Remove Nodesfrom the dom + pub(crate) fn remove(&mut self) { + self.edits.push(Remove); + } + // Create + pub(crate) fn create_text_node(&mut self, text: &'bump str, id: ElementId) { + let id = id.as_u64(); + self.edits.push(CreateTextNode { text, id }); + } + pub(crate) fn create_element( + &mut self, + tag: &'static str, + ns: Option<&'static str>, + id: ElementId, + ) { + let id = id.as_u64(); + match ns { + Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }), + None => self.edits.push(CreateElement { id, tag }), + } + } + // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom + pub(crate) fn create_placeholder(&mut self, id: ElementId) { + let id = id.as_u64(); + self.edits.push(CreatePlaceholder { id }); + } + // events + pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) { + let Listener { + event, + mounted_node, + .. + } = listener; + + let element_id = mounted_node.get().unwrap().as_u64(); + + self.edits.push(NewEventListener { + scope, + event_name: event, + mounted_node_id: element_id, + }); + } + pub(crate) fn remove_event_listener(&mut self, event: &'static str) { + self.edits.push(RemoveEventListener { event }); + } + // modify + pub(crate) fn set_text(&mut self, text: &'bump str) { + self.edits.push(SetText { text }); + } + pub(crate) fn set_attribute(&mut self, attribute: &'bump Attribute) { + let Attribute { + name, + value, + is_static, + is_volatile, + namespace, + } = attribute; + + self.edits.push(SetAttribute { + field: name, + value, + ns: *namespace, + }); + } + pub(crate) fn set_attribute_ns(&mut self, attribute: &'bump Attribute, namespace: &'bump str) { + let Attribute { + name, + value, + is_static, + is_volatile, + .. + } = attribute; + + self.edits.push(SetAttribute { + field: name, + value, + ns: Some(namespace), + }); + } + pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) { + let name = attribute.name; + self.edits.push(RemoveAttribute { name }); + } +} + +// refs are only assigned once +pub struct NodeRefMutation<'a> { + element: &'a mut Option>>, + element_id: ElementId, +} + +impl<'a> std::fmt::Debug for NodeRefMutation<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NodeRefMutation") + .field("element_id", &self.element_id) + .finish() + } +} + +impl<'a> NodeRefMutation<'a> { + pub fn downcast_ref(&self) -> Option<&T> { + self.element + .as_ref() + .and_then(|f| f.get()) + .and_then(|f| f.downcast_ref::()) + } + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.element + .as_mut() + .and_then(|f| f.get_mut()) + .and_then(|f| f.downcast_mut::()) + } +} diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 05798f1ac..c9b1c3db8 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -164,6 +164,8 @@ pub struct VComponent<'src> { pub can_memoize: bool, + pub name: &'static str, + // a pointer into the bump arena (given by the 'src lifetime) pub(crate) raw_props: *const (), @@ -361,6 +363,8 @@ impl<'a> NodeFactory<'a> { { let bump = self.bump(); + let name = crate::util::type_name_of(component); + // We don't want the fat part of the fat pointer // This function does static dispatch so we don't need any VTable stuff let children: &'a V = bump.alloc(children); @@ -426,6 +430,7 @@ impl<'a> NodeFactory<'a> { can_memoize: P::IS_STATIC, ass_scope: Cell::new(None), key, + name, })) } @@ -454,6 +459,25 @@ impl<'a> NodeFactory<'a> { pub fn fragment_from_iter(self, node_iter: impl IntoVNodeList<'a>) -> VNode<'a> { let children = node_iter.into_vnode_list(self); + // TODO + // We need a dedicated path in the rsx! macro that will trigger the "you need keys" warning + // + // if cfg!(debug_assertions) { + // if children.len() > 1 { + // if children.last().unwrap().key().is_none() { + // log::error!( + // r#" + // Warning: Each child in an array or iterator should have a unique "key" prop. + // Not providing a key will lead to poor performance with lists. + // See docs.rs/dioxus for more information. + // --- + // To help you identify where this error is coming from, we've generated a backtrace. + // "#, + // ); + // } + // } + // } + VNode::Fragment(VFragment { children, key: None, @@ -497,22 +521,6 @@ where nodes.push(node.into_vnode(cx)); } - if cfg!(debug_assertions) { - if nodes.len() > 1 { - if nodes.last().unwrap().key().is_none() { - log::error!( - r#" - Warning: Each child in an array or iterator should have a unique "key" prop. - Not providing a key will lead to poor performance with lists. - See docs.rs/dioxus for more information. - --- - To help you identify where this error is coming from, we've generated a backtrace. - "#, - ); - } - } - } - if nodes.len() == 0 { nodes.push(VNode::Anchor(VAnchor { dom_id: empty_cell(), @@ -657,9 +665,13 @@ impl Debug for VNode<'_> { VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text), VNode::Anchor(a) => write!(s, "VAnchor"), - VNode::Fragment(_) => write!(s, "fragment"), - VNode::Suspended { .. } => write!(s, "suspended"), - VNode::Component(_) => write!(s, "component"), + VNode::Fragment(frag) => write!(s, "VFragment {{ children: {:?} }}", frag.children), + VNode::Suspended { .. } => write!(s, "VSuspended"), + VNode::Component(comp) => write!( + s, + "VComponent {{ fc: {:?}, children: {:?} }}", + comp.name, comp.children + ), } } } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 22e55a994..b8fc07f15 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -33,58 +33,10 @@ use futures_util::pin_mut; use futures_util::Future; use futures_util::FutureExt; use futures_util::StreamExt; -use indexmap::IndexSet; use smallvec::SmallVec; use crate::innerlude::*; -/// The "Mutations" object holds the changes that need to be made to the DOM. -/// -#[derive(Debug)] -pub struct Mutations<'s> { - pub edits: Vec>, - pub noderefs: Vec>, -} - -impl<'s> Mutations<'s> { - pub fn new() -> Self { - let edits = Vec::new(); - let noderefs = Vec::new(); - Self { edits, noderefs } - } - - pub fn extend(&mut self, other: &mut Mutations) {} -} - -// refs are only assigned once -pub struct NodeRefMutation<'a> { - element: &'a mut Option>>, - element_id: ElementId, -} - -impl<'a> std::fmt::Debug for NodeRefMutation<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NodeRefMutation") - .field("element_id", &self.element_id) - .finish() - } -} - -impl<'a> NodeRefMutation<'a> { - pub fn downcast_ref(&self) -> Option<&T> { - self.element - .as_ref() - .and_then(|f| f.get()) - .and_then(|f| f.downcast_ref::()) - } - pub fn downcast_mut(&mut self) -> Option<&mut T> { - self.element - .as_mut() - .and_then(|f| f.get_mut()) - .and_then(|f| f.downcast_mut::()) - } -} - pub struct Scheduler { current_priority: EventPriority, @@ -432,7 +384,7 @@ pub struct Waypoint { pub struct PriortySystem { pub pending_scopes: Vec, - pub dirty_scopes: IndexSet, + pub dirty_scopes: HashSet, } impl PriortySystem { diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 673944df2..00b4f251f 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -43,6 +43,9 @@ pub struct Scope { pub(crate) hooks: HookList, pub(crate) shared_contexts: RefCell>>, + // meta + pub(crate) function_name: &'static str, + // A reference to the resources shared by all the comonents pub(crate) vdom: SharedResources, } @@ -73,6 +76,8 @@ impl Scope { child_nodes: ScopeChildren, vdom: SharedResources, + + function_name: &'static str, ) -> Self { let child_nodes = unsafe { child_nodes.extend_lifetime() }; @@ -84,6 +89,7 @@ impl Scope { } Self { + function_name, child_nodes, caller, parent_idx: parent, @@ -141,7 +147,7 @@ impl Scope { // the user's component succeeded. We can safely cycle to the next frame self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; self.frames.cycle_frame(); - log::debug!("Cycle okay"); + log::debug!("Successfully rendered component"); Ok(()) } } diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 0add0f456..c7309fe68 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -8,6 +8,10 @@ pub fn empty_cell() -> Cell> { Cell::new(None) } +pub fn type_name_of(_: T) -> &'static str { + std::any::type_name::() +} + // /// A helper type that lets scopes be ordered by their height // #[derive(Debug, Clone, Copy, PartialEq, Eq)] // pub struct HeightMarker { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index ca355f986..d6ab528a8 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -40,7 +40,7 @@ pub struct VirtualDom { /// /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas /// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually. - shared: SharedResources, + pub shared: SharedResources, /// The index of the root component /// Should always be the first (gen=0, id=0) @@ -111,7 +111,8 @@ impl VirtualDom { let base_scope = components.insert_scope_with_key(move |myidx| { let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); - Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), link) + let name = type_name_of(root); + Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), link, name) }); Self { @@ -150,35 +151,18 @@ impl VirtualDom { /// The diff machine expects the RealDom's stack to be the root of the application /// /// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed - /// through "run" - /// + /// through "run". We completely avoid the task scheduler infrastructure. pub fn rebuild<'s>(&'s mut self) -> Result> { - let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); + let mut fut = self.rebuild_async().boxed_local(); - let cur_component = diff_machine - .get_scope_mut(&self.base_scope) - .expect("The base scope should never be moved"); - - // todo!(); - - // // 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.instructions.push(DiffInstruction::Create { - node: cur_component.frames.fin_head(), - and: MountType::Append, - }); - } else { - // todo: should this be a hard error? - log::warn!( - "Component failed to run succesfully during rebuild. - This does not result in a failed rebuild, but indicates a logic failure within your app." - ); + loop { + if let Some(edits) = (&mut fut).now_or_never() { + break edits; + } } - - Ok(diff_machine.mutations) } - /// Rebuild the dom + /// Rebuild the dom from the ground up pub async fn rebuild_async<'s>(&'s mut self) -> Result> { let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); @@ -186,8 +170,6 @@ impl VirtualDom { .get_scope_mut(&self.base_scope) .expect("The base scope should never be moved"); - // todo!(); - // // 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.instructions.push(DiffInstruction::Create { @@ -205,6 +187,27 @@ impl VirtualDom { Ok(diff_machine.mutations) } + + // diff the dom with itself + pub async fn diff_async<'s>(&'s mut self) -> Result> { + let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); + + let cur_component = diff_machine + .get_scope_mut(&self.base_scope) + .expect("The base scope should never be moved"); + + cur_component.run_scope().unwrap(); + + diff_machine.instructions.push(DiffInstruction::DiffNode { + old: cur_component.frames.wip_head(), + new: cur_component.frames.fin_head(), + }); + + diff_machine.work().await.unwrap(); + + Ok(diff_machine.mutations) + } + /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. /// /// This method will not wait for any suspended nodes to complete. diff --git a/packages/core/tests/iterative.rs b/packages/core/tests/create_iterative.rs similarity index 97% rename from packages/core/tests/iterative.rs rename to packages/core/tests/create_iterative.rs index 573bd4459..e797fff05 100644 --- a/packages/core/tests/iterative.rs +++ b/packages/core/tests/create_iterative.rs @@ -104,9 +104,7 @@ async fn test_iterative_create_components() { static Child: FC<()> = |cx| { cx.render(rsx! { h1 {} - div { - {cx.children()} - } + div { {cx.children()} } p {} }) }; diff --git a/packages/core/tests/debugdiff.rs b/packages/core/tests/debugdiff.rs new file mode 100644 index 000000000..9f6aac6c1 --- /dev/null +++ b/packages/core/tests/debugdiff.rs @@ -0,0 +1,2 @@ +/// A virtualdom wrapper used for testing purposes. +pub struct DebugDiff {} diff --git a/packages/core/tests/diff_iterative.rs b/packages/core/tests/diff_iterative.rs new file mode 100644 index 000000000..f5fba7852 --- /dev/null +++ b/packages/core/tests/diff_iterative.rs @@ -0,0 +1,46 @@ +//! tests to prove that the iterative implementation works + +use dioxus::prelude::*; + +mod test_logging; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; + +#[async_std::test] +async fn test_iterative_create_components() { + static App: FC<()> = |cx| { + // test root fragments + cx.render(rsx! { + Child { "abc1" } + Child { "abc2" } + Child { "abc3" } + }) + }; + + fn Child(cx: Context<()>) -> DomTree { + // test root fragments, anchors, and ChildNode type + cx.render(rsx! { + h1 {} + div { {cx.children()} } + Fragment { + Fragment { + Fragment { + "wozza" + } + } + } + {(0..0).map(|f| rsx!{ div { "walalla"}})} + p {} + }) + } + + test_logging::set_up_logging(); + + let mut dom = VirtualDom::new(App); + + let mutations = dom.rebuild_async().await.unwrap(); + dbg!(mutations); + + let mutations = dom.diff_async().await.unwrap(); + dbg!(mutations); +} diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 9f74901ce..84b69120d 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -9,12 +9,13 @@ use bumpalo::Bump; use anyhow::{Context, Result}; use dioxus::{ arena::SharedResources, - diff::{CreateMeta, DiffMachine}, + diff::{CreateMeta, DiffInstruction, DiffMachine}, prelude::*, DomEdit, }; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; +use futures_util::FutureExt; struct TestDom { bump: Bump, @@ -38,30 +39,41 @@ impl TestDom { lazy_nodes.into_vnode(NodeFactory::new(&self.bump)) } - fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Vec> { - let mut edits = Vec::new(); + fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { + // let mut edits = Vec::new(); let mut machine = DiffMachine::new_headless(&self.resources); - machine.diff_node(old, new); - edits + + machine + .instructions + .push(dioxus::diff::DiffInstruction::DiffNode { new, old }); + + machine.mutations } - fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> (CreateMeta, Vec>) + fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> Mutations<'a> where F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, { let old = self.bump.alloc(self.render(left)); - let mut edits = Vec::new(); let mut machine = DiffMachine::new_headless(&self.resources); - let meta = machine.create_vnode(old); - (meta, edits) + + machine + .instructions + .push(dioxus::diff::DiffInstruction::Create { + node: old, + and: dioxus::diff::MountType::Append, + }); + work_sync(&mut machine); + + machine.mutations } fn lazy_diff<'a, F1, F2>( &'a self, left: LazyNodes<'a, F1>, right: LazyNodes<'a, F2>, - ) -> (Vec>, Vec>) + ) -> (Mutations<'a>, Mutations<'a>) where F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, F2: FnOnce(NodeFactory<'a>) -> VNode<'a>, @@ -70,18 +82,37 @@ impl TestDom { let new = self.bump.alloc(self.render(right)); - let mut create_edits = Vec::new(); + // let mut create_edits = Vec::new(); let mut machine = DiffMachine::new_headless(&self.resources); - machine.create_vnode(old); + machine + .instructions + .push(dioxus::diff::DiffInstruction::Create { + and: dioxus::diff::MountType::Append, + node: old, + }); + work_sync(&mut machine); + let create_edits = machine.mutations; - let mut edits = Vec::new(); let mut machine = DiffMachine::new_headless(&self.resources); - machine.diff_node(old, new); + machine + .instructions + .push(DiffInstruction::DiffNode { old, new }); + work_sync(&mut machine); + let edits = machine.mutations; + (create_edits, edits) } } +fn work_sync(machine: &mut DiffMachine) { + let mut fut = machine.work().boxed_local(); + + while let None = (&mut fut).now_or_never() { + // + } +} + #[test] fn diffing_works() {} @@ -100,7 +131,7 @@ fn html_and_rsx_generate_the_same_output() { #[test] fn fragments_create_properly() { let dom = TestDom::new(); - let (meta, edits) = dom.create(rsx! { + let Mutations { edits, noderefs } = dom.create(rsx! { div { "Hello a" } div { "Hello b" } div { "Hello c" } @@ -109,7 +140,7 @@ fn fragments_create_properly() { assert!(&edits[3].is("CreateElement")); assert!(&edits[6].is("CreateElement")); - assert_eq!(meta.added_to_stack, 3); + assert_eq!(*edits.last().unwrap(), DomEdit::AppendChildren { many: 3 }); dbg!(edits); } @@ -152,7 +183,7 @@ fn empty_fragments_create_anchors_with_many_children() { let edits = dom.lazy_diff(left, right); dbg!(&edits); - let last_edit = edits.1.last().unwrap(); + let last_edit = edits.1.edits.last().unwrap(); assert!(last_edit.is("ReplaceWith")); } @@ -190,7 +221,7 @@ fn two_equal_fragments_are_equal() { let edits = dom.lazy_diff(left, right); dbg!(&edits); - assert!(edits.1.is_empty()); + assert!(edits.1.edits.is_empty()); } /// Should result the creation of more nodes appended after the old last node diff --git a/packages/core/tests/work_sync.rs b/packages/core/tests/work_sync.rs new file mode 100644 index 000000000..187a4443b --- /dev/null +++ b/packages/core/tests/work_sync.rs @@ -0,0 +1,35 @@ +//! Diffing is interruptible, but uses yield_now which is loop-pollable +//! +//! This means you can actually call it synchronously if you want. + +use anyhow::{Context, Result}; +use dioxus::{ + arena::SharedResources, + diff::{CreateMeta, DiffInstruction, DiffMachine}, + prelude::*, + scope::Scope, +}; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; +use futures_util::FutureExt; + +#[test] +fn worksync() { + static App: FC<()> = |cx| { + cx.render(rsx! { + div {"hello"} + }) + }; + let mut dom = VirtualDom::new(App); + + let mut fut = dom.rebuild_async().boxed_local(); + + let mutations = loop { + let g = (&mut fut).now_or_never(); + if g.is_some() { + break g.unwrap().unwrap(); + } + }; + + dbg!(mutations); +} From 16bfc6d2488f7ec0266ff5b174142232fb04849f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 23 Aug 2021 10:43:49 -0400 Subject: [PATCH 17/53] wip: compiles again --- packages/core/src/childiter.rs | 130 +++++++ packages/core/src/diff.rs | 614 ++++++++----------------------- packages/core/src/diff_stack.rs | 115 ++++++ packages/core/src/diffsupport.rs | 72 ++++ packages/core/src/editor.rs | 11 +- packages/core/src/lib.rs | 4 + packages/core/src/mutations.rs | 44 ++- packages/core/src/nodes.rs | 10 - packages/core/src/virtual_dom.rs | 14 +- packages/core/tests/diffing.rs | 28 +- 10 files changed, 534 insertions(+), 508 deletions(-) create mode 100644 packages/core/src/childiter.rs create mode 100644 packages/core/src/diff_stack.rs create mode 100644 packages/core/src/diffsupport.rs diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs new file mode 100644 index 000000000..e3c520d1c --- /dev/null +++ b/packages/core/src/childiter.rs @@ -0,0 +1,130 @@ +use crate::innerlude::*; + +/// This iterator iterates through a list of virtual children and only returns real children (Elements, Text, Anchors). +/// +/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like +/// "InsertBefore". +pub struct RealChildIterator<'a> { + scopes: &'a SharedResources, + + // Heuristcally we should never bleed into 4 completely nested fragments/components + // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane + // TODO: use const generics instead of the 4 estimation + stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 4]>, +} + +impl<'a> RealChildIterator<'a> { + pub fn new(starter: &'a VNode<'a>, scopes: &'a SharedResources) -> Self { + Self { + scopes, + stack: smallvec::smallvec![(0, starter)], + } + } + + pub fn new_from_slice(nodes: &'a [VNode<'a>], scopes: &'a SharedResources) -> Self { + let mut stack = smallvec::smallvec![]; + for node in nodes { + stack.push((0, node)); + } + Self { scopes, stack } + } + // keep the memory around + pub fn reset_with(&mut self, node: &'a VNode<'a>) { + self.stack.clear(); + self.stack.push((0, node)); + } +} + +impl<'a> Iterator for RealChildIterator<'a> { + type Item = &'a VNode<'a>; + + fn next(&mut self) -> Option<&'a VNode<'a>> { + let mut should_pop = false; + let mut returned_node: Option<&'a VNode<'a>> = None; + let mut should_push = None; + + while returned_node.is_none() { + if let Some((count, node)) = self.stack.last_mut() { + match &node { + // We can only exit our looping when we get "real" nodes + // This includes fragments and components when they're empty (have a single root) + VNode::Element(_) | VNode::Text(_) => { + // We've recursed INTO an element/text + // We need to recurse *out* of it and move forward to the next + should_pop = true; + returned_node = Some(&*node); + } + + // If we get a fragment we push the next child + VNode::Fragment(frag) => { + let subcount = *count as usize; + + if frag.children.len() == 0 { + should_pop = true; + returned_node = Some(&*node); + } + + if subcount >= frag.children.len() { + should_pop = true; + } else { + should_push = Some(&frag.children[subcount]); + } + } + // // If we get a fragment we push the next child + // VNodeKind::Fragment(frag) => { + // let subcount = *count as usize; + + // if frag.children.len() == 0 { + // should_pop = true; + // returned_node = Some(&*node); + // } + + // if subcount >= frag.children.len() { + // should_pop = true; + // } else { + // should_push = Some(&frag.children[subcount]); + // } + // } + + // Immediately abort suspended nodes - can't do anything with them yet + VNode::Suspended(node) => { + // VNodeKind::Suspended => should_pop = true, + todo!() + } + + VNode::Anchor(a) => { + todo!() + } + + // For components, we load their root and push them onto the stack + VNode::Component(sc) => { + let scope = + unsafe { self.scopes.get_scope(sc.ass_scope.get().unwrap()) }.unwrap(); + // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); + + // Simply swap the current node on the stack with the root of the component + *node = scope.frames.fin_head(); + } + } + } else { + // If there's no more items on the stack, we're done! + return None; + } + + if should_pop { + self.stack.pop(); + if let Some((id, _)) = self.stack.last_mut() { + *id += 1; + } + should_pop = false; + } + + if let Some(push) = should_push { + self.stack.push((0, push)); + should_push = None; + } + } + + returned_node + } +} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 87282013c..b9dffde21 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -110,63 +110,13 @@ pub struct DiffMachine<'bump> { pub mutations: Mutations<'bump>, - pub nodes_created_stack: SmallVec<[usize; 10]>, - - pub instructions: Vec>, - - pub scope_stack: SmallVec<[ScopeId; 5]>, + pub stack: DiffStack<'bump>, pub diffed: FxHashSet, pub seen_scopes: FxHashSet, } -/// The stack instructions we use to diff and create new nodes. -/// -/// These instructions are essentially mini state machines that stay on top of the stack until they are finished. -#[derive(Debug)] -pub enum DiffInstruction<'a> { - DiffNode { - old: &'a VNode<'a>, - new: &'a VNode<'a>, - }, - - DiffChildren { - old: &'a [VNode<'a>], - new: &'a [VNode<'a>], - }, - - Create { - node: &'a VNode<'a>, - and: MountType<'a>, - }, - - Remove { - child: &'a VNode<'a>, - }, - - RemoveChildren { - children: &'a [VNode<'a>], - }, - - Mount { - and: MountType<'a>, - }, - - PopElement, - - PopScope, -} - -#[derive(Debug, Clone, Copy)] -pub enum MountType<'a> { - Absorb, - Append, - Replace { old: &'a VNode<'a> }, - InsertAfter { other_node: Option<&'a VNode<'a>> }, - InsertBefore { other_node: Option<&'a VNode<'a>> }, -} - impl<'bump> DiffMachine<'bump> { pub(crate) fn new( edits: Mutations<'bump>, @@ -174,10 +124,8 @@ impl<'bump> DiffMachine<'bump> { shared: &'bump SharedResources, ) -> Self { Self { - instructions: Vec::with_capacity(1000), - nodes_created_stack: smallvec![], + stack: DiffStack::new(cur_scope), mutations: edits, - scope_stack: smallvec![cur_scope], vdom: shared, diffed: FxHashSet::default(), seen_scopes: FxHashSet::default(), @@ -192,7 +140,10 @@ impl<'bump> DiffMachine<'bump> { // pub async fn diff_scope(&mut self, id: ScopeId) -> Result<()> { - let component = self.get_scope_mut(&id).ok_or_else(|| Error::NotMounted)?; + let component = self + .vdom + .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); Ok(()) @@ -206,7 +157,7 @@ impl<'bump> DiffMachine<'bump> { pub async fn work(&mut self) -> Result<()> { // 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.pop() { + while let Some(instruction) = self.stack.pop() { log::debug!("Handling diff instruction: {:?}", instruction); // todo: call this less frequently, there is a bit of overhead involved @@ -214,7 +165,7 @@ impl<'bump> DiffMachine<'bump> { match instruction { DiffInstruction::PopScope => { - self.scope_stack.pop(); + self.stack.pop_scope(); } DiffInstruction::PopElement => { self.mutations.pop(); @@ -229,22 +180,18 @@ impl<'bump> DiffMachine<'bump> { } DiffInstruction::Create { node, and } => { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { and }); self.create_node(node); } DiffInstruction::Remove { child } => { for child in RealChildIterator::new(child, self.vdom) { - self.mutations.push_root(child.direct_id()); - self.mutations.remove(); + self.mutations.remove(child.direct_id().as_u64()); } } DiffInstruction::RemoveChildren { children } => { for child in RealChildIterator::new_from_slice(children, self.vdom) { - self.mutations.push_root(child.direct_id()); - self.mutations.remove(); + self.mutations.remove(child.direct_id().as_u64()); } } @@ -258,12 +205,12 @@ impl<'bump> DiffMachine<'bump> { } fn mount(&mut self, and: MountType) { - let nodes_created = self.nodes_created_stack.pop().unwrap(); + let nodes_created = self.stack.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; + *self.stack.nodes_created_stack.last_mut().unwrap() += nodes_created; } MountType::Append => { self.mutations.edits.push(AppendChildren { @@ -303,21 +250,21 @@ impl<'bump> DiffMachine<'bump> { let real_id = self.vdom.reserve_node(); self.mutations.create_text_node(vtext.text, real_id); vtext.dom_id.set(Some(real_id)); - *self.nodes_created_stack.last_mut().unwrap() += 1; + self.stack.add_child_count(1); } fn create_suspended_node(&mut self, suspended: &'bump VSuspended) { let real_id = self.vdom.reserve_node(); self.mutations.create_placeholder(real_id); suspended.node.set(Some(real_id)); - *self.nodes_created_stack.last_mut().unwrap() += 1; + self.stack.add_child_count(1); } fn create_anchor_node(&mut self, anchor: &'bump VAnchor) { let real_id = self.vdom.reserve_node(); self.mutations.create_placeholder(real_id); anchor.dom_id.set(Some(real_id)); - *self.nodes_created_stack.last_mut().unwrap() += 1; + self.stack.add_child_count(1); } fn create_element_node(&mut self, element: &'bump VElement<'bump>) { @@ -334,11 +281,11 @@ impl<'bump> DiffMachine<'bump> { let real_id = self.vdom.reserve_node(); self.mutations.create_element(tag_name, *namespace, real_id); - *self.nodes_created_stack.last_mut().unwrap() += 1; + self.stack.add_child_count(1); dom_id.set(Some(real_id)); - let cur_scope = self.current_scope().unwrap(); + let cur_scope = self.stack.current_scope().unwrap(); listeners.iter().for_each(|listener| { self.fix_listener(listener); @@ -352,22 +299,22 @@ impl<'bump> DiffMachine<'bump> { } if children.len() > 0 { - self.create_children_instructions(children, MountType::Append); + self.stack.create_children(children, MountType::Append); } } fn create_fragment_node(&mut self, frag: &'bump VFragment<'bump>) { - self.create_children_instructions(frag.children, MountType::Absorb); + self.stack.create_children(frag.children, MountType::Absorb); } fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { let caller = vcomponent.caller.clone(); - let parent_idx = self.scope_stack.last().unwrap().clone(); + let parent_idx = self.stack.current_scope().unwrap(); // Insert a new scope into our component list let new_idx = self.vdom.insert_scope_with_key(|new_idx| { - let parent_scope = self.get_scope(&parent_idx).unwrap(); + let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); let height = parent_scope.height + 1; Scope::new( caller, @@ -384,7 +331,7 @@ impl<'bump> DiffMachine<'bump> { vcomponent.ass_scope.set(Some(new_idx)); if !vcomponent.can_memoize { - let cur_scope = self.get_scope_mut(&parent_idx).unwrap(); + let cur_scope = self.vdom.get_scope_mut(parent_idx).unwrap(); let extended = vcomponent as *const VComponent; let extended: *const VComponent<'static> = unsafe { std::mem::transmute(extended) }; cur_scope.borrowed_props.borrow_mut().push(extended); @@ -394,7 +341,7 @@ impl<'bump> DiffMachine<'bump> { // add noderefs to current noderef list Noderefs // add effects to current effect list Effects - let new_component = self.get_scope_mut(&new_idx).unwrap(); + let new_component = self.vdom.get_scope_mut(new_idx).unwrap(); // Run the scope for one iteration to initialize it match new_component.run_scope() { @@ -411,16 +358,7 @@ 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); - self.instructions.push(DiffInstruction::PopScope); - - // 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, - }); + self.stack.create_component(new_idx, nextnode); // Finally, insert this scope as a seen node. self.seen_scopes.insert(new_idx); @@ -445,7 +383,7 @@ impl<'bump> DiffMachine<'bump> { ( Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), - ) => self.replace_and_create_many_with_many(old_node, new_node), + ) => self.replace_and_create_one_with_one(old_node, new_node), } } @@ -517,7 +455,7 @@ impl<'bump> DiffMachine<'bump> { // 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 - let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); + let cur_scope = self.stack.current_scope().unwrap(); 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 { @@ -559,13 +497,13 @@ impl<'bump> DiffMachine<'bump> { // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { // - self.scope_stack.push(scope_addr); + self.stack.scope_stack.push(scope_addr); // Make sure the new component vnode is referencing the right scope id new.ass_scope.set(Some(scope_addr)); // make sure the component's caller function is up to date - let scope = self.get_scope_mut(&scope_addr).unwrap(); + let scope = self.vdom.get_scope_mut(scope_addr).unwrap(); scope.update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); @@ -583,7 +521,7 @@ impl<'bump> DiffMachine<'bump> { } } - self.scope_stack.pop(); + self.stack.scope_stack.pop(); self.seen_scopes.insert(scope_addr); } else { @@ -624,54 +562,6 @@ impl<'bump> DiffMachine<'bump> { // Utilites for creating new diff instructions // ============================================= - fn create_children_instructions( - &mut self, - children: &'bump [VNode<'bump>], - and: MountType<'bump>, - ) { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { and }); - - for child in children.into_iter().rev() { - self.instructions.push(DiffInstruction::Create { - and: MountType::Absorb, - node: child, - }); - } - } - - /// Destroy a scope and all of its descendents. - /// - /// Calling this will run the destuctors on all hooks in the tree. - /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. - fn destroy_scopes(&mut self, old_scope: ScopeId) { - let mut nodes_to_delete = vec![old_scope]; - let mut scopes_to_explore = vec![old_scope]; - - // explore the scope tree breadth first - while let Some(scope_id) = scopes_to_explore.pop() { - // If we're planning on deleting this node, then we don't need to both rendering it - self.seen_scopes.insert(scope_id); - let scope = self.get_scope(&scope_id).unwrap(); - for child in scope.descendents.borrow().iter() { - // Add this node to be explored - scopes_to_explore.push(child.clone()); - - // Also add it for deletion - nodes_to_delete.push(child.clone()); - } - } - - // Delete all scopes that we found as part of this subtree - for node in nodes_to_delete { - log::debug!("Removing scope {:#?}", node); - let _scope = self.vdom.try_remove(node).unwrap(); - // do anything we need to do to delete the scope - // I think we need to run the destructors on the hooks - // TODO - } - } - // Diff the given set of old and new children. // // The parent must be on top of the change list stack when this function is @@ -697,7 +587,7 @@ impl<'bump> DiffMachine<'bump> { // Completely adding new nodes, removing any placeholder if it exists (IS_EMPTY, IS_NOT_EMPTY) => { - self.create_children_instructions(new, MountType::Append); + self.stack.create_children(new, MountType::Append); } // Completely removing old nodes and putting an anchor in its place @@ -705,7 +595,7 @@ impl<'bump> DiffMachine<'bump> { // remove all the old nodes (IS_NOT_EMPTY, IS_EMPTY) => { for node in old { - self.remove_vnode(node); + self.remove_nodes(Some(node)); } } @@ -721,15 +611,13 @@ impl<'bump> DiffMachine<'bump> { // Replace the anchor with whatever new nodes are coming down the pipe (VNode::Anchor(anchor), _) => { - self.create_children_instructions( - new, - MountType::Replace { old: first_old }, - ); + self.stack + .create_children(new, MountType::Replace { old: first_old }); } // Replace whatever nodes are sitting there with the anchor (_, VNode::Anchor(anchor)) => { - self.replace_and_create_many_with_many(old, [first_new]); + self.replace_and_create_many_with_one(old, first_new); } // Use the complex diff algorithm to diff the nodes @@ -884,7 +772,7 @@ impl<'bump> DiffMachine<'bump> { // And if that was all of the new children, then remove all of the remaining // old children and we're finished. if shared_prefix_count == new.len() { - self.remove_children(&old[shared_prefix_count..]); + self.remove_nodes(&old[shared_prefix_count..]); return KeyedPrefixResult::Finished; } @@ -898,7 +786,7 @@ impl<'bump> DiffMachine<'bump> { // [... parent] // // When this function returns, the change list stack is in the same state. - pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { + fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { for child in new { todo!(); // let meta = self.create_vnode(child); @@ -1069,7 +957,7 @@ impl<'bump> DiffMachine<'bump> { self.mutations.insert_before(1); } } else { - self.instructions.push(DiffInstruction::Create { + self.stack.push(DiffInstruction::Create { node: next_new, and: MountType::InsertBefore { other_node: Some(new_anchor), @@ -1165,13 +1053,13 @@ impl<'bump> DiffMachine<'bump> { Ordering::Greater => { // Generate instructions to diff the existing elements for (new_child, old_child) in new.iter().zip(old.iter()).rev() { - self.instructions.push(DiffInstruction::DiffNode { + self.stack.push(DiffInstruction::DiffNode { new: new_child, old: old_child, }); } - self.instructions.push(DiffInstruction::RemoveChildren { + self.stack.push(DiffInstruction::RemoveChildren { children: &old[new.len()..], }); } @@ -1183,14 +1071,14 @@ impl<'bump> DiffMachine<'bump> { Ordering::Less => { // Generate instructions to diff the existing elements for (new_child, old_child) in new.iter().zip(old.iter()).rev() { - self.instructions.push(DiffInstruction::DiffNode { + self.stack.push(DiffInstruction::DiffNode { new: new_child, old: old_child, }); } // Generate instructions to add in the new elements - self.create_children_instructions( + self.stack.create_children( &new[old.len()..], MountType::InsertAfter { other_node: old.last(), @@ -1201,7 +1089,7 @@ impl<'bump> DiffMachine<'bump> { // old.len == new.len -> no nodes added/removed, but perhaps changed Ordering::Equal => { for (new_child, old_child) in new.iter().zip(old.iter()).rev() { - self.instructions.push(DiffInstruction::DiffNode { + self.stack.push(DiffInstruction::DiffNode { new: new_child, old: old_child, }); @@ -1210,40 +1098,6 @@ impl<'bump> DiffMachine<'bump> { } } - // ====================== - // Support methods - // ====================== - // // Remove all of a node's children. - // // - // // The change list stack must have this shape upon entry to this function: - // // - // // [... parent] - // // - // // When this function returns, the change list stack is in the same state. - // fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { - // // debug_assert!(self.traversal_is_committed()); - // log::debug!("REMOVING CHILDREN"); - // for _child in old { - // // registry.remove_subtree(child); - // } - // // Fast way to remove all children: set the node's textContent to an empty - // // string. - // todo!() - // // self.set_inner_text(""); - // } - // Remove the current child and all of its following siblings. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent child] - // - // After the function returns, the child is no longer on the change list stack: - // - // [... parent] - fn remove_children(&mut self, old: &'bump [VNode<'bump>]) { - self.replace_and_create_many_with_many(old, None) - } - fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { let mut search_node = Some(vnode); @@ -1260,7 +1114,7 @@ impl<'bump> DiffMachine<'bump> { } VNode::Component(el) => { let scope_id = el.ass_scope.get().unwrap(); - let scope = self.get_scope(&scope_id).unwrap(); + let scope = self.vdom.get_scope(scope_id).unwrap(); search_node = Some(scope.root()); } } @@ -1283,7 +1137,7 @@ impl<'bump> DiffMachine<'bump> { } VNode::Component(el) => { let scope_id = el.ass_scope.get().unwrap(); - let scope = self.get_scope(&scope_id).unwrap(); + let scope = self.vdom.get_scope(scope_id).unwrap(); search_node = Some(scope.root()); } } @@ -1294,56 +1148,107 @@ impl<'bump> DiffMachine<'bump> { // self.replace_and_create_many_with_many(Some(node), None); // } + fn replace_and_create_one_with_one( + &mut self, + old: &'bump VNode<'bump>, + new: &'bump VNode<'bump>, + ) { + self.stack.create_node(new, MountType::Replace { old }); + } + fn replace_many_with_many(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { // } fn replace_one_with_many(&mut self, old: &'bump VNode<'bump>, new: &'bump [VNode<'bump>]) { - // + self.stack.create_children(new, MountType::Replace { old }); } - /// Remove all the old nodes and replace them with newly created new nodes. - /// - /// The new nodes *will* be created - don't create them yourself! - fn replace_and_create_many_with_many( + fn replace_and_create_many_with_one( &mut self, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, - // old_nodes: impl IntoIterator>, - // new_nodes: impl IntoIterator>, + old: &'bump [VNode<'bump>], + new: &'bump VNode<'bump>, ) { - let mut nodes_to_replace = Vec::new(); - let mut nodes_to_search = vec![old_node]; - let mut scopes_obliterated = Vec::new(); - while let Some(node) = nodes_to_search.pop() { - match &node { - // the ones that have a direct id return immediately - VNode::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNode::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNode::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNode::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), + if let Some(first_old) = old.get(0) { + self.remove_nodes(&old[1..]); + self.stack + .create_node(new, MountType::Replace { old: first_old }); + } else { + self.stack.create_node(new, MountType::Append {}); + } + } - // Fragments will either have a single anchor or a list of children - VNode::Fragment(frag) => { - for child in frag.children { - nodes_to_search.push(child); - } + /// schedules nodes for garbage collection and pushes "remove" to the mutation stack + /// remove can happen whenever + fn remove_nodes(&mut self, nodes: impl IntoIterator>) { + // or cache the vec on the diff machine + for node in nodes { + match node { + VNode::Text(t) => { + t.dom_id.get().map(|id| { + self.mutations.remove(id.as_u64()); + self.vdom.collect_garbage(id); + }); } - - // Components can be any of the nodes above - // However, we do need to track which components need to be removed - VNode::Component(el) => { - let scope_id = el.ass_scope.get().unwrap(); - let scope = self.get_scope(&scope_id).unwrap(); + VNode::Suspended(s) => { + s.node.get().map(|id| { + self.mutations.remove(id.as_u64()); + self.vdom.collect_garbage(id); + }); + } + VNode::Anchor(a) => { + a.dom_id.get().map(|id| { + self.mutations.remove(id.as_u64()); + self.vdom.collect_garbage(id); + }); + } + VNode::Element(e) => { + e.dom_id.get().map(|id| self.mutations.remove(id.as_u64())); + } + VNode::Fragment(f) => { + self.remove_nodes(f.children); + } + VNode::Component(c) => { + let scope_id = c.ass_scope.get().unwrap(); + let scope = self.vdom.get_scope(scope_id).unwrap(); let root = scope.root(); - nodes_to_search.push(root); - scopes_obliterated.push(scope_id); + self.remove_nodes(Some(root)); } } - // TODO: enable internal garabge collection - // self.create_garbage(node); } + // let mut nodes_to_replace = Vec::new(); + // let mut nodes_to_search = vec![old_node]; + // let mut scopes_obliterated = Vec::new(); + // while let Some(node) = nodes_to_search.pop() { + // match &node { + // // the ones that have a direct id return immediately + // VNode::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + // VNode::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + // VNode::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + // VNode::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), + + // // Fragments will either have a single anchor or a list of children + // VNode::Fragment(frag) => { + // for child in frag.children { + // nodes_to_search.push(child); + // } + // } + + // // Components can be any of the nodes above + // // However, we do need to track which components need to be removed + // VNode::Component(el) => { + // let scope_id = el.ass_scope.get().unwrap(); + // let scope = self.vdom.get_scope(scope_id).unwrap(); + // let root = scope.root(); + // nodes_to_search.push(root); + // scopes_obliterated.push(scope_id); + // } + // } + // // TODO: enable internal garabge collection + // // self.create_garbage(node); + // } + // let n = nodes_to_replace.len(); // for node in nodes_to_replace { // self.mutations.push_root(node); @@ -1365,13 +1270,37 @@ impl<'bump> DiffMachine<'bump> { // }); // obliterate! - for scope in scopes_obliterated { - self.destroy_scopes(scope); + // for scope in scopes_obliterated { + + // todo: mark as garbage + + // self.destroy_scopes(scope); + // } + } + + /// Remove all the old nodes and replace them with newly created new nodes. + /// + /// The new nodes *will* be created - don't create them yourself! + fn replace_and_create_many_with_many( + &mut self, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], + ) { + if let Some(first_old) = old.get(0) { + self.remove_nodes(&old[1..]); + self.stack + .create_children(new, MountType::Replace { old: first_old }) + } else { + self.stack.create_children(new, MountType::Append {}); } } fn create_garbage(&mut self, node: &'bump VNode<'bump>) { - match self.current_scope().and_then(|id| self.get_scope(&id)) { + match self + .stack + .current_scope() + .and_then(|id| self.vdom.get_scope(id)) + { Some(scope) => { let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) }; scope.pending_garbage.borrow_mut().push(garbage); @@ -1382,92 +1311,26 @@ impl<'bump> DiffMachine<'bump> { } } - fn immediately_dispose_garabage(&mut self, node: ElementId) { - self.vdom.collect_garbage(node) - } - fn replace_node_with_node( &mut self, - anchor: ElementId, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>, ) { - self.mutations.push_root(anchor); - todo!(); - // let meta = self.create_vnode(new_node); - // self.mutations.replace_with(1, meta.added_to_stack); - // self.create_garbage(old_node); - self.mutations.pop(); - } - - fn remove_vnode(&mut self, node: &'bump VNode<'bump>) { - match &node { - VNode::Text(el) => self.immediately_dispose_garabage(node.direct_id()), - VNode::Element(el) => { - self.immediately_dispose_garabage(node.direct_id()); - for child in el.children { - self.remove_vnode(&child); - } - } - VNode::Anchor(a) => { - // - } - VNode::Fragment(frag) => { - for child in frag.children { - self.remove_vnode(&child); - } - } - VNode::Component(el) => { - // - // self.destroy_scopes(old_scope) - } - VNode::Suspended(_) => todo!(), - } - } - - fn current_scope(&self) -> Option { - self.scope_stack.last().map(|f| f.clone()) + self.stack.instructions.push(DiffInstruction::Create { + and: MountType::Replace { old: old_node }, + node: new_node, + }); } fn fix_listener<'a>(&mut self, listener: &'a Listener<'a>) { - let scope_id = self.current_scope(); + let scope_id = self.stack.current_scope(); if let Some(scope_id) = scope_id { - let scope = self.get_scope(&scope_id).unwrap(); + let scope = self.vdom.get_scope(scope_id).unwrap(); let mut queue = scope.listeners.borrow_mut(); let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; queue.push(long_listener as *const _) } } - - pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'bump mut Scope> { - // ensure we haven't seen this scope before - // if we have, then we're trying to alias it, which is not allowed - debug_assert!(!self.seen_scopes.contains(id)); - - unsafe { self.vdom.get_scope_mut(*id) } - } - pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'bump Scope> { - // ensure we haven't seen this scope before - // if we have, then we're trying to alias it, which is not allowed - unsafe { self.vdom.get_scope(*id) } - } -} - -// When we create new nodes, we need to propagate some information back up the call chain. -// This gives the caller some information on how to handle things like insertins, appending, and subtree discarding. -#[derive(Debug)] -pub struct CreateMeta { - pub is_static: bool, - pub added_to_stack: u32, -} - -impl CreateMeta { - fn new(is_static: bool, added_to_tack: u32) -> Self { - Self { - is_static, - added_to_stack: added_to_tack, - } - } } enum KeyedPrefixResult { @@ -1478,156 +1341,3 @@ enum KeyedPrefixResult { // the beginning of `new` and `old` we already processed. MoreWorkToDo(usize), } - -fn find_first_real_node<'a>( - nodes: impl IntoIterator>, - scopes: &'a SharedResources, -) -> Option<&'a VNode<'a>> { - for node in nodes { - let mut iter = RealChildIterator::new(node, scopes); - if let Some(node) = iter.next() { - return Some(node); - } - } - - None -} - -/// This iterator iterates through a list of virtual children and only returns real children (Elements, Text, Anchors). -/// -/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like -/// "InsertBefore". -pub struct RealChildIterator<'a> { - scopes: &'a SharedResources, - - // Heuristcally we should never bleed into 4 completely nested fragments/components - // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane - // TODO: use const generics instead of the 4 estimation - stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 4]>, -} - -impl<'a> RealChildIterator<'a> { - pub fn new(starter: &'a VNode<'a>, scopes: &'a SharedResources) -> Self { - Self { - scopes, - stack: smallvec::smallvec![(0, starter)], - } - } - - pub fn new_from_slice(nodes: &'a [VNode<'a>], scopes: &'a SharedResources) -> Self { - let mut stack = smallvec::smallvec![]; - for node in nodes { - stack.push((0, node)); - } - Self { scopes, stack } - } - // keep the memory around - pub fn reset_with(&mut self, node: &'a VNode<'a>) { - self.stack.clear(); - self.stack.push((0, node)); - } -} - -impl<'a> Iterator for RealChildIterator<'a> { - type Item = &'a VNode<'a>; - - fn next(&mut self) -> Option<&'a VNode<'a>> { - let mut should_pop = false; - let mut returned_node: Option<&'a VNode<'a>> = None; - let mut should_push = None; - - while returned_node.is_none() { - if let Some((count, node)) = self.stack.last_mut() { - match &node { - // We can only exit our looping when we get "real" nodes - // This includes fragments and components when they're empty (have a single root) - VNode::Element(_) | VNode::Text(_) => { - // We've recursed INTO an element/text - // We need to recurse *out* of it and move forward to the next - should_pop = true; - returned_node = Some(&*node); - } - - // If we get a fragment we push the next child - VNode::Fragment(frag) => { - let subcount = *count as usize; - - if frag.children.len() == 0 { - should_pop = true; - returned_node = Some(&*node); - } - - if subcount >= frag.children.len() { - should_pop = true; - } else { - should_push = Some(&frag.children[subcount]); - } - } - // // If we get a fragment we push the next child - // VNodeKind::Fragment(frag) => { - // let subcount = *count as usize; - - // if frag.children.len() == 0 { - // should_pop = true; - // returned_node = Some(&*node); - // } - - // if subcount >= frag.children.len() { - // should_pop = true; - // } else { - // should_push = Some(&frag.children[subcount]); - // } - // } - - // Immediately abort suspended nodes - can't do anything with them yet - VNode::Suspended(node) => { - // VNodeKind::Suspended => should_pop = true, - todo!() - } - - VNode::Anchor(a) => { - todo!() - } - - // For components, we load their root and push them onto the stack - VNode::Component(sc) => { - let scope = - unsafe { self.scopes.get_scope(sc.ass_scope.get().unwrap()) }.unwrap(); - // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); - - // Simply swap the current node on the stack with the root of the component - *node = scope.frames.fin_head(); - } - } - } else { - // If there's no more items on the stack, we're done! - return None; - } - - if should_pop { - self.stack.pop(); - if let Some((id, _)) = self.stack.last_mut() { - *id += 1; - } - should_pop = false; - } - - if let Some(push) = should_push { - self.stack.push((0, push)); - should_push = None; - } - } - - returned_node - } -} - -fn compare_strs(a: &str, b: &str) -> bool { - // Check by pointer, optimizing for static strs - if !std::ptr::eq(a, b) { - // If the pointers are different then check by value - a == b - } else { - true - } -} diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs new file mode 100644 index 000000000..296bdd1d0 --- /dev/null +++ b/packages/core/src/diff_stack.rs @@ -0,0 +1,115 @@ +use crate::innerlude::*; +use smallvec::{smallvec, SmallVec}; + +/// The stack instructions we use to diff and create new nodes. +#[derive(Debug)] +pub enum DiffInstruction<'a> { + DiffNode { + old: &'a VNode<'a>, + new: &'a VNode<'a>, + }, + + DiffChildren { + old: &'a [VNode<'a>], + new: &'a [VNode<'a>], + }, + + Create { + node: &'a VNode<'a>, + and: MountType<'a>, + }, + + Remove { + child: &'a VNode<'a>, + }, + + RemoveChildren { + children: &'a [VNode<'a>], + }, + + Mount { + and: MountType<'a>, + }, + + PopElement, + + PopScope, +} + +#[derive(Debug, Clone, Copy)] +pub enum MountType<'a> { + Absorb, + Append, + Replace { old: &'a VNode<'a> }, + InsertAfter { other_node: Option<&'a VNode<'a>> }, + InsertBefore { other_node: Option<&'a VNode<'a>> }, +} + +pub struct DiffStack<'bump> { + pub instructions: Vec>, + pub nodes_created_stack: SmallVec<[usize; 10]>, + pub scope_stack: SmallVec<[ScopeId; 5]>, +} + +impl<'bump> DiffStack<'bump> { + pub fn new(cur_scope: ScopeId) -> Self { + Self { + instructions: Vec::with_capacity(1000), + nodes_created_stack: smallvec![], + scope_stack: smallvec![cur_scope], + } + } + + pub fn pop(&mut self) -> Option> { + self.instructions.pop() + } + + pub fn pop_scope(&mut self) -> Option { + self.scope_stack.pop() + } + + pub fn push(&mut self, instruction: DiffInstruction<'bump>) { + self.instructions.push(instruction) + } + + pub fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) { + self.nodes_created_stack.push(0); + self.instructions.push(DiffInstruction::Mount { and }); + + for child in children.into_iter().rev() { + self.instructions.push(DiffInstruction::Create { + and: MountType::Absorb, + node: child, + }); + } + } + + pub fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) { + self.nodes_created_stack.push(0); + self.instructions.push(DiffInstruction::Mount { and }); + self.instructions + .push(DiffInstruction::Create { and, node }); + } + + pub fn add_child_count(&mut self, count: usize) { + *self.nodes_created_stack.last_mut().unwrap() += count; + } + + pub fn current_scope(&self) -> Option { + self.scope_stack.last().map(|f| f.clone()) + } + + pub fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) { + // Push the new scope onto the stack + self.scope_stack.push(idx); + + self.instructions.push(DiffInstruction::PopScope); + + // Run the creation algorithm with this scope on the stack + // ?? I think we treat components as framgnets?? + self.instructions.push(DiffInstruction::Create { + node, + and: MountType::Absorb, + }); + } +} diff --git a/packages/core/src/diffsupport.rs b/packages/core/src/diffsupport.rs new file mode 100644 index 000000000..8b01182e2 --- /dev/null +++ b/packages/core/src/diffsupport.rs @@ -0,0 +1,72 @@ +/// Destroy a scope and all of its descendents. +/// +/// Calling this will run the destuctors on all hooks in the tree. +/// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. +fn destroy_scopes(&mut self, old_scope: ScopeId) { + let mut nodes_to_delete = vec![old_scope]; + let mut scopes_to_explore = vec![old_scope]; + + // explore the scope tree breadth first + while let Some(scope_id) = scopes_to_explore.pop() { + // If we're planning on deleting this node, then we don't need to both rendering it + self.seen_scopes.insert(scope_id); + let scope = self.get_scope(&scope_id).unwrap(); + for child in scope.descendents.borrow().iter() { + // Add this node to be explored + scopes_to_explore.push(child.clone()); + + // Also add it for deletion + nodes_to_delete.push(child.clone()); + } + } + + // Delete all scopes that we found as part of this subtree + for node in nodes_to_delete { + log::debug!("Removing scope {:#?}", node); + let _scope = self.vdom.try_remove(node).unwrap(); + // do anything we need to do to delete the scope + // I think we need to run the destructors on the hooks + // TODO + } +} + +pub(crate) fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'bump mut Scope> { + // ensure we haven't seen this scope before + // if we have, then we're trying to alias it, which is not allowed + debug_assert!(!self.seen_scopes.contains(id)); + + unsafe { self.vdom.get_scope_mut(*id) } +} +pub(crate) fn get_scope(&mut self, id: &ScopeId) -> Option<&'bump Scope> { + // ensure we haven't seen this scope before + // if we have, then we're trying to alias it, which is not allowed + unsafe { self.vdom.get_scope(*id) } +} + +fn compare_strs(a: &str, b: &str) -> bool { + // Check by pointer, optimizing for static strs + if !std::ptr::eq(a, b) { + // If the pointers are different then check by value + a == b + } else { + true + } +} + +fn find_first_real_node<'a>( + nodes: impl IntoIterator>, + scopes: &'a SharedResources, +) -> Option<&'a VNode<'a>> { + for node in nodes { + let mut iter = RealChildIterator::new(node, scopes); + if let Some(node) = iter.next() { + return Some(node); + } + } + + None +} + +fn remove_children(&mut self, old: &'bump [VNode<'bump>]) { + self.replace_and_create_many_with_many(old, None) +} diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs index b6cc620d8..9aa887832 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/editor.rs @@ -24,10 +24,6 @@ pub enum DomEdit<'bump> { many: u32, }, ReplaceWith { - // the first n elements - n: u32, - - // the last m elements m: u32, }, InsertAfter { @@ -36,7 +32,10 @@ pub enum DomEdit<'bump> { InsertBefore { n: u32, }, - Remove, + // remove roots directly + Remove { + root: u64, + }, RemoveAllChildren, CreateTextNode { text: &'bump str, @@ -83,7 +82,7 @@ impl DomEdit<'_> { DomEdit::PopRoot => id == "PopRoot", DomEdit::AppendChildren { .. } => id == "AppendChildren", DomEdit::ReplaceWith { .. } => id == "ReplaceWith", - DomEdit::Remove => id == "Remove", + DomEdit::Remove { .. } => id == "Remove", DomEdit::RemoveAllChildren => id == "RemoveAllChildren", DomEdit::CreateTextNode { .. } => id == "CreateTextNode", DomEdit::CreateElement { .. } => id == "CreateElement", diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 09d287d90..34f8d3ce9 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -30,9 +30,11 @@ pub mod prelude { pub(crate) mod innerlude { pub use crate::arena::*; pub use crate::bumpframe::*; + pub use crate::childiter::*; pub use crate::component::*; pub use crate::context::*; pub use crate::diff::*; + pub use crate::diff_stack::*; pub use crate::editor::*; pub use crate::error::*; pub use crate::events::*; @@ -60,9 +62,11 @@ pub mod exports { pub mod arena; pub mod bumpframe; +pub mod childiter; pub mod component; pub mod context; pub mod diff; +pub mod diff_stack; pub mod editor; pub mod error; pub mod events; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index ba43f9b4f..d45a9bba4 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,17 +1,18 @@ -use std::any::Any; +//! Instructions returned by the VirtualDOM on how to modify the Real DOM. +//! use crate::innerlude::*; +use std::any::Any; -/// The "Mutations" object holds the changes that need to be made to the DOM. -/// #[derive(Debug)] -pub struct Mutations<'s> { - pub edits: Vec>, - pub noderefs: Vec>, +pub struct Mutations<'a> { + pub edits: Vec>, + pub noderefs: Vec>, } + use DomEdit::*; -impl<'bump> Mutations<'bump> { +impl<'a> Mutations<'a> { pub fn new() -> Self { let edits = Vec::new(); let noderefs = Vec::new(); @@ -25,29 +26,34 @@ impl<'bump> Mutations<'bump> { let id = root.as_u64(); self.edits.push(PushRoot { id }); } + pub(crate) fn pop(&mut self) { self.edits.push(PopRoot {}); } - // replace the n-m node on the stack with the m nodes - // ends with the last element of the chain on the top of the stack - pub(crate) fn replace_with(&mut self, n: u32, m: u32) { - self.edits.push(ReplaceWith { n, m }); + + pub(crate) fn replace_with(&mut self, m: u32) { + self.edits.push(ReplaceWith { m }); } + pub(crate) fn insert_after(&mut self, n: u32) { self.edits.push(InsertAfter { n }); } + pub(crate) fn insert_before(&mut self, n: u32) { self.edits.push(InsertBefore { n }); } + // Remove Nodesfrom the dom - pub(crate) fn remove(&mut self) { - self.edits.push(Remove); + pub(crate) fn remove(&mut self, id: u64) { + self.edits.push(Remove { root: id }); } + // Create - pub(crate) fn create_text_node(&mut self, text: &'bump str, id: ElementId) { + pub(crate) fn create_text_node(&mut self, text: &'a str, id: ElementId) { let id = id.as_u64(); self.edits.push(CreateTextNode { text, id }); } + pub(crate) fn create_element( &mut self, tag: &'static str, @@ -65,6 +71,7 @@ impl<'bump> Mutations<'bump> { let id = id.as_u64(); self.edits.push(CreatePlaceholder { id }); } + // events pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) { let Listener { @@ -84,11 +91,13 @@ impl<'bump> Mutations<'bump> { pub(crate) fn remove_event_listener(&mut self, event: &'static str) { self.edits.push(RemoveEventListener { event }); } + // modify - pub(crate) fn set_text(&mut self, text: &'bump str) { + pub(crate) fn set_text(&mut self, text: &'a str) { self.edits.push(SetText { text }); } - pub(crate) fn set_attribute(&mut self, attribute: &'bump Attribute) { + + pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute) { let Attribute { name, value, @@ -103,7 +112,7 @@ impl<'bump> Mutations<'bump> { ns: *namespace, }); } - pub(crate) fn set_attribute_ns(&mut self, attribute: &'bump Attribute, namespace: &'bump str) { + pub(crate) fn set_attribute_ns(&mut self, attribute: &'a Attribute, namespace: &'a str) { let Attribute { name, value, @@ -118,6 +127,7 @@ impl<'bump> Mutations<'bump> { ns: Some(namespace), }); } + pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) { let name = attribute.name; self.edits.push(RemoveAttribute { name }); diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c9b1c3db8..14d21a5a7 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -98,13 +98,8 @@ pub struct VElement<'a> { pub namespace: Option<&'static str>, pub dom_id: Cell>, - pub static_listeners: bool, pub listeners: &'a [Listener<'a>], - - pub static_attrs: bool, pub attributes: &'a [Attribute<'a>], - - pub static_children: bool, pub children: &'a [VNode<'a>], } @@ -303,11 +298,6 @@ impl<'a> NodeFactory<'a> { attributes, children, dom_id: empty_cell(), - - // todo: wire up more constization - static_listeners: false, - static_attrs: false, - static_children: false, })) } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index d6ab528a8..5a68e3008 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -166,13 +166,14 @@ impl VirtualDom { pub async fn rebuild_async<'s>(&'s mut self) -> Result> { let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); - let cur_component = diff_machine - .get_scope_mut(&self.base_scope) + let cur_component = self + .shared + .get_scope_mut(self.base_scope) .expect("The base scope should never be moved"); // // 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.instructions.push(DiffInstruction::Create { + diff_machine.stack.push(DiffInstruction::Create { node: cur_component.frames.fin_head(), and: MountType::Append, }); @@ -192,13 +193,14 @@ impl VirtualDom { pub async fn diff_async<'s>(&'s mut self) -> Result> { let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); - let cur_component = diff_machine - .get_scope_mut(&self.base_scope) + let cur_component = self + .shared + .get_scope_mut(self.base_scope) .expect("The base scope should never be moved"); cur_component.run_scope().unwrap(); - diff_machine.instructions.push(DiffInstruction::DiffNode { + diff_machine.stack.push(DiffInstruction::DiffNode { old: cur_component.frames.wip_head(), new: cur_component.frames.fin_head(), }); diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 84b69120d..640aad610 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -9,7 +9,7 @@ use bumpalo::Bump; use anyhow::{Context, Result}; use dioxus::{ arena::SharedResources, - diff::{CreateMeta, DiffInstruction, DiffMachine}, + diff::{DiffInstruction, DiffMachine}, prelude::*, DomEdit, }; @@ -44,7 +44,7 @@ impl TestDom { let mut machine = DiffMachine::new_headless(&self.resources); machine - .instructions + .stack .push(dioxus::diff::DiffInstruction::DiffNode { new, old }); machine.mutations @@ -58,12 +58,10 @@ impl TestDom { let mut machine = DiffMachine::new_headless(&self.resources); - machine - .instructions - .push(dioxus::diff::DiffInstruction::Create { - node: old, - and: dioxus::diff::MountType::Append, - }); + machine.stack.push(dioxus::diff::DiffInstruction::Create { + node: old, + and: dioxus::diff::MountType::Append, + }); work_sync(&mut machine); machine.mutations @@ -85,19 +83,15 @@ impl TestDom { // let mut create_edits = Vec::new(); let mut machine = DiffMachine::new_headless(&self.resources); - machine - .instructions - .push(dioxus::diff::DiffInstruction::Create { - and: dioxus::diff::MountType::Append, - node: old, - }); + machine.stack.push(dioxus::diff::DiffInstruction::Create { + and: dioxus::diff::MountType::Append, + node: old, + }); work_sync(&mut machine); let create_edits = machine.mutations; let mut machine = DiffMachine::new_headless(&self.resources); - machine - .instructions - .push(DiffInstruction::DiffNode { old, new }); + machine.stack.push(DiffInstruction::DiffNode { old, new }); work_sync(&mut machine); let edits = machine.mutations; From 2d541eca640d15bb56bccb4191b1dc0fe02dfc22 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 23 Aug 2021 12:11:45 -0400 Subject: [PATCH 18/53] wip: structure coming together and tests --- packages/core/.vscode/settings.json | 2 +- packages/core/src/diff.rs | 93 ++++----- packages/core/src/diff_stack.rs | 28 ++- packages/core/src/{editor.rs => dom_edits.rs} | 9 +- packages/core/src/lib.rs | 10 +- packages/core/src/mutations.rs | 15 +- packages/core/src/virtual_dom.rs | 7 +- packages/core/tests/create_iterative.rs | 178 ++++++++++++++++-- packages/core/tests/diffing.rs | 22 +-- packages/core/tests/eventsystem.rs | 7 +- packages/core/tests/hooks.rs | 7 +- packages/core/tests/test_logging.rs | 7 +- packages/core/tests/work_sync.rs | 7 +- 13 files changed, 251 insertions(+), 141 deletions(-) rename packages/core/src/{editor.rs => dom_edits.rs} (93%) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index b9dffde21..4c4894161 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -167,8 +167,10 @@ impl<'bump> DiffMachine<'bump> { DiffInstruction::PopScope => { self.stack.pop_scope(); } + DiffInstruction::PopElement => { - self.mutations.pop(); + todo!(); + // self.mutations.pop(); } DiffInstruction::DiffNode { old, new, .. } => { @@ -179,20 +181,16 @@ impl<'bump> DiffMachine<'bump> { self.diff_children(old, new); } - DiffInstruction::Create { node, and } => { + DiffInstruction::Create { node } => { self.create_node(node); } DiffInstruction::Remove { child } => { - for child in RealChildIterator::new(child, self.vdom) { - self.mutations.remove(child.direct_id().as_u64()); - } + self.remove_nodes(Some(child)); } DiffInstruction::RemoveChildren { children } => { - for child in RealChildIterator::new_from_slice(children, self.vdom) { - self.mutations.remove(child.direct_id().as_u64()); - } + self.remove_nodes(children); } DiffInstruction::Mount { and } => { @@ -204,13 +202,13 @@ impl<'bump> DiffMachine<'bump> { Ok(()) } - fn mount(&mut self, and: MountType) { - let nodes_created = self.stack.nodes_created_stack.pop().unwrap(); + fn mount(&mut self, and: MountType<'bump>) { + let nodes_created = self.stack.pop_nodes_created(); match and { // add the nodes from this virtual list to the parent // used by fragments and components MountType::Absorb => { - *self.stack.nodes_created_stack.last_mut().unwrap() += nodes_created; + self.stack.add_child_count(nodes_created); } MountType::Append => { self.mutations.edits.push(AppendChildren { @@ -218,15 +216,21 @@ impl<'bump> DiffMachine<'bump> { }); } MountType::Replace { old } => { - todo!() - // self.mutations.replace_with(with as u32, many as u32); + let root = todo!(); + self.mutations.replace_with(root, nodes_created as u32); } + MountType::ReplaceByElementId { old } => { + self.mutations.replace_with(old, nodes_created as u32); + } + MountType::InsertAfter { other_node } => { - self.mutations.insert_after(nodes_created as u32); + let root = self.find_last_element(other_node).direct_id(); + self.mutations.insert_after(root, nodes_created as u32); } MountType::InsertBefore { other_node } => { - self.mutations.insert_before(nodes_created as u32); + let root = self.find_first_element(other_node).direct_id(); + self.mutations.insert_before(root, nodes_created as u32); } } } @@ -373,17 +377,19 @@ impl<'bump> DiffMachine<'bump> { match (old_node, new_node) { // Check the most common cases first (Text(old), Text(new)) => self.diff_text_nodes(old, new), - (Element(old), Element(new)) => self.diff_element_nodes(old, new), (Component(old), Component(new)) => self.diff_component_nodes(old, new), (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), (Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()), (Suspended(old), Suspended(new)) => new.node.set(old.node.get()), + (Element(old), Element(new)) => self.diff_element_nodes(old, new), // Anything else is just a basic replace and create ( Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), - ) => self.replace_and_create_one_with_one(old_node, new_node), + ) => self + .stack + .create_node(new_node, MountType::Replace { old: old_node }), } } @@ -407,8 +413,8 @@ impl<'bump> DiffMachine<'bump> { // // This case is rather rare (typically only in non-keyed lists) if new.tag_name != old.tag_name || new.namespace != old.namespace { - todo!(); - // self.replace_node_with_node(root, old_node, new_node); + todo!("element changed"); + // self.replace_node_with_node(old, new); return; } @@ -779,21 +785,6 @@ impl<'bump> DiffMachine<'bump> { KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } - // Create the given children and append them to the parent node. - // - // The parent node must currently be on top of the change list stack: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { - for child in new { - todo!(); - // let meta = self.create_vnode(child); - // self.mutations.append_children(meta.added_to_stack); - } - } - // The most-general, expensive code path for keyed children diffing. // // We find the longest subsequence within `old` of children that are relatively @@ -957,12 +948,12 @@ impl<'bump> DiffMachine<'bump> { self.mutations.insert_before(1); } } else { - self.stack.push(DiffInstruction::Create { - node: next_new, - and: MountType::InsertBefore { - other_node: Some(new_anchor), + self.stack.create_node( + next_new, + MountType::InsertBefore { + other_node: new_anchor, }, - }); + ); } } } @@ -1081,7 +1072,7 @@ impl<'bump> DiffMachine<'bump> { self.stack.create_children( &new[old.len()..], MountType::InsertAfter { - other_node: old.last(), + other_node: old.last().unwrap(), }, ); } @@ -1098,6 +1089,10 @@ impl<'bump> DiffMachine<'bump> { } } + // ===================== + // Utilities + // ===================== + fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { let mut search_node = Some(vnode); @@ -1144,18 +1139,6 @@ impl<'bump> DiffMachine<'bump> { } } - // fn remove_child(&mut self, node: &'bump VNode<'bump>) { - // self.replace_and_create_many_with_many(Some(node), None); - // } - - fn replace_and_create_one_with_one( - &mut self, - old: &'bump VNode<'bump>, - new: &'bump VNode<'bump>, - ) { - self.stack.create_node(new, MountType::Replace { old }); - } - fn replace_many_with_many(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { // } @@ -1316,10 +1299,8 @@ impl<'bump> DiffMachine<'bump> { old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>, ) { - self.stack.instructions.push(DiffInstruction::Create { - and: MountType::Replace { old: old_node }, - node: new_node, - }); + self.stack + .create_node(new_node, MountType::Replace { old: old_node }); } fn fix_listener<'a>(&mut self, listener: &'a Listener<'a>) { diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index 296bdd1d0..b82658e29 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -16,7 +16,6 @@ pub enum DiffInstruction<'a> { Create { node: &'a VNode<'a>, - and: MountType<'a>, }, Remove { @@ -41,13 +40,14 @@ pub enum MountType<'a> { Absorb, Append, Replace { old: &'a VNode<'a> }, - InsertAfter { other_node: Option<&'a VNode<'a>> }, - InsertBefore { other_node: Option<&'a VNode<'a>> }, + ReplaceByElementId { old: ElementId }, + InsertAfter { other_node: &'a VNode<'a> }, + InsertBefore { other_node: &'a VNode<'a> }, } pub struct DiffStack<'bump> { - pub instructions: Vec>, - pub nodes_created_stack: SmallVec<[usize; 10]>, + instructions: Vec>, + nodes_created_stack: SmallVec<[usize; 10]>, pub scope_stack: SmallVec<[ScopeId; 5]>, } @@ -77,24 +77,25 @@ impl<'bump> DiffStack<'bump> { self.instructions.push(DiffInstruction::Mount { and }); for child in children.into_iter().rev() { - self.instructions.push(DiffInstruction::Create { - and: MountType::Absorb, - node: child, - }); + self.instructions + .push(DiffInstruction::Create { node: child }); } } pub fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) { self.nodes_created_stack.push(0); self.instructions.push(DiffInstruction::Mount { and }); - self.instructions - .push(DiffInstruction::Create { and, node }); + self.instructions.push(DiffInstruction::Create { node }); } pub fn add_child_count(&mut self, count: usize) { *self.nodes_created_stack.last_mut().unwrap() += count; } + pub fn pop_nodes_created(&mut self) -> usize { + self.nodes_created_stack.pop().unwrap() + } + pub fn current_scope(&self) -> Option { self.scope_stack.last().map(|f| f.clone()) } @@ -107,9 +108,6 @@ impl<'bump> DiffStack<'bump> { // Run the creation algorithm with this scope on the stack // ?? I think we treat components as framgnets?? - self.instructions.push(DiffInstruction::Create { - node, - and: MountType::Absorb, - }); + self.instructions.push(DiffInstruction::Create { node }); } } diff --git a/packages/core/src/editor.rs b/packages/core/src/dom_edits.rs similarity index 93% rename from packages/core/src/editor.rs rename to packages/core/src/dom_edits.rs index 9aa887832..cbb70f248 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/dom_edits.rs @@ -20,22 +20,29 @@ pub enum DomEdit<'bump> { id: u64, }, PopRoot, + AppendChildren { many: u32, }, + + // "Root" refers to the item direclty + // it's a waste of an instruction to push the root directly ReplaceWith { + root: u64, m: u32, }, InsertAfter { + root: u64, n: u32, }, InsertBefore { + root: u64, n: u32, }, - // remove roots directly Remove { root: u64, }, + RemoveAllChildren, CreateTextNode { text: &'bump str, diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 34f8d3ce9..ed4e8ce9f 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -11,9 +11,9 @@ //! pub use crate::innerlude::{ - format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, - EventTrigger, LazyNodes, Mutations, NodeFactory, Properties, ScopeId, SuspendedContext, VNode, - VirtualDom, VirtualEvent, FC, + format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId, + EventPriority, EventTrigger, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, + SuspendedContext, VNode, VirtualDom, VirtualEvent, FC, }; pub mod prelude { @@ -35,7 +35,7 @@ pub(crate) mod innerlude { pub use crate::context::*; pub use crate::diff::*; pub use crate::diff_stack::*; - pub use crate::editor::*; + pub use crate::dom_edits::*; pub use crate::error::*; pub use crate::events::*; pub use crate::heuristics::*; @@ -67,7 +67,7 @@ pub mod component; pub mod context; pub mod diff; pub mod diff_stack; -pub mod editor; +pub mod dom_edits; pub mod error; pub mod events; pub mod heuristics; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index d45a9bba4..afefc1cf0 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -31,16 +31,19 @@ impl<'a> Mutations<'a> { self.edits.push(PopRoot {}); } - pub(crate) fn replace_with(&mut self, m: u32) { - self.edits.push(ReplaceWith { m }); + pub(crate) fn replace_with(&mut self, root: ElementId, m: u32) { + let root = root.as_u64(); + self.edits.push(ReplaceWith { m, root }); } - pub(crate) fn insert_after(&mut self, n: u32) { - self.edits.push(InsertAfter { n }); + pub(crate) fn insert_after(&mut self, root: ElementId, n: u32) { + let root = root.as_u64(); + self.edits.push(InsertAfter { n, root }); } - pub(crate) fn insert_before(&mut self, n: u32) { - self.edits.push(InsertBefore { n }); + pub(crate) fn insert_before(&mut self, root: ElementId, n: u32) { + let root = root.as_u64(); + self.edits.push(InsertBefore { n, root }); } // Remove Nodesfrom the dom diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 5a68e3008..2718aa222 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -173,10 +173,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.stack.push(DiffInstruction::Create { - node: cur_component.frames.fin_head(), - and: MountType::Append, - }); + diff_machine + .stack + .create_node(cur_component.frames.fin_head(), MountType::Append); diff_machine.work().await.unwrap(); } else { // todo: should this be a hard error? diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_iterative.rs index e797fff05..519e97141 100644 --- a/packages/core/tests/create_iterative.rs +++ b/packages/core/tests/create_iterative.rs @@ -1,16 +1,11 @@ //! tests to prove that the iterative implementation works use anyhow::{Context, Result}; -use dioxus::{ - arena::SharedResources, - diff::{CreateMeta, DiffMachine}, - prelude::*, - scheduler::Mutations, - DomEdit, -}; +use dioxus::{arena::SharedResources, diff::DiffMachine, prelude::*, DomEdit, Mutations}; mod test_logging; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; +use DomEdit::*; #[test] fn test_original_diff() { @@ -26,11 +21,24 @@ fn test_original_diff() { let mut dom = VirtualDom::new(App); let mutations = dom.rebuild().unwrap(); - dbg!(mutations); + assert_eq!( + mutations.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateElement { id: 1, tag: "div" }, + CreateTextNode { + id: 2, + text: "Hello, world!" + }, + AppendChildren { many: 1 }, + AppendChildren { many: 1 }, + AppendChildren { many: 1 }, + ] + ); } #[async_std::test] -async fn test_iterative_create() { +async fn create() { static App: FC<()> = |cx| { cx.render(rsx! { div { @@ -50,14 +58,38 @@ async fn test_iterative_create() { }; test_logging::set_up_logging(); - let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); - dbg!(mutations); + assert_eq!( + mutations.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateElement { id: 1, tag: "div" }, + CreateTextNode { + id: 2, + text: "Hello, world!" + }, + CreateElement { id: 3, tag: "div" }, + CreateElement { id: 4, tag: "div" }, + CreateTextNode { + id: 5, + text: "hello" + }, + CreateTextNode { + id: 6, + text: "world" + }, + AppendChildren { many: 2 }, + AppendChildren { many: 1 }, + AppendChildren { many: 2 }, + AppendChildren { many: 1 }, + AppendChildren { many: 1 }, + ] + ); } #[async_std::test] -async fn test_iterative_create_list() { +async fn create_list() { static App: FC<()> = |cx| { cx.render(rsx! { {(0..3).map(|f| rsx!{ div { @@ -70,11 +102,36 @@ async fn test_iterative_create_list() { let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); - dbg!(mutations); + + // copilot wrote this test :P + assert_eq!( + mutations.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateTextNode { + id: 1, + text: "hello" + }, + AppendChildren { many: 1 }, + CreateElement { id: 2, tag: "div" }, + CreateTextNode { + id: 3, + text: "hello" + }, + AppendChildren { many: 1 }, + CreateElement { id: 4, tag: "div" }, + CreateTextNode { + id: 5, + text: "hello" + }, + AppendChildren { many: 1 }, + AppendChildren { many: 3 }, + ] + ); } #[async_std::test] -async fn test_iterative_create_simple() { +async fn create_simple() { static App: FC<()> = |cx| { cx.render(rsx! { div {} @@ -88,11 +145,22 @@ async fn test_iterative_create_simple() { let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); - dbg!(mutations); + + // copilot wrote this test :P + assert_eq!( + mutations.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateElement { id: 1, tag: "div" }, + CreateElement { id: 2, tag: "div" }, + CreateElement { id: 3, tag: "div" }, + AppendChildren { many: 4 }, + ] + ); } #[async_std::test] -async fn test_iterative_create_components() { +async fn create_components() { static App: FC<()> = |cx| { cx.render(rsx! { Child { "abc1" } @@ -113,5 +181,81 @@ async fn test_iterative_create_components() { let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); - dbg!(mutations); + + assert_eq!( + mutations.edits, + [ + CreateElement { id: 0, tag: "h1" }, + CreateElement { id: 1, tag: "div" }, + CreateTextNode { + id: 2, + text: "abc1" + }, + AppendChildren { many: 1 }, + CreateElement { id: 3, tag: "p" }, + CreateElement { id: 4, tag: "h1" }, + CreateElement { id: 5, tag: "div" }, + CreateTextNode { + id: 6, + text: "abc2" + }, + AppendChildren { many: 1 }, + CreateElement { id: 7, tag: "p" }, + CreateElement { id: 8, tag: "h1" }, + CreateElement { id: 9, tag: "div" }, + CreateTextNode { + id: 10, + text: "abc3" + }, + AppendChildren { many: 1 }, + CreateElement { id: 11, tag: "p" }, + AppendChildren { many: 9 }, + ] + ); +} + +#[async_std::test] +async fn anchors() { + static App: FC<()> = |cx| { + cx.render(rsx! { + {true.then(|| rsx!{ div { "hello" } })} + {false.then(|| rsx!{ div { "goodbye" } })} + }) + }; + + test_logging::set_up_logging(); + + let mut dom = VirtualDom::new(App); + let mutations = dom.rebuild_async().await.unwrap(); + assert_eq!( + mutations.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateTextNode { + id: 1, + text: "hello" + }, + AppendChildren { many: 1 }, + CreatePlaceholder { id: 2 }, + AppendChildren { many: 2 }, + ] + ); +} + +#[async_std::test] +async fn suspended() { + static App: FC<()> = |cx| { + let val = use_suspense(cx, || async {}, |cx, _| cx.render(rsx! { "hi "})); + cx.render(rsx! { {val} }) + }; + + test_logging::set_up_logging(); + + let mut dom = VirtualDom::new(App); + let mutations = dom.rebuild_async().await.unwrap(); + + assert_eq!( + mutations.edits, + [CreatePlaceholder { id: 0 }, AppendChildren { many: 1 },] + ); } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 640aad610..a4ce42240 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -8,10 +8,7 @@ use bumpalo::Bump; use anyhow::{Context, Result}; use dioxus::{ - arena::SharedResources, - diff::{DiffInstruction, DiffMachine}, - prelude::*, - DomEdit, + arena::SharedResources, diff::DiffMachine, prelude::*, DiffInstruction, DomEdit, MountType, }; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; @@ -43,9 +40,7 @@ impl TestDom { // let mut edits = Vec::new(); let mut machine = DiffMachine::new_headless(&self.resources); - machine - .stack - .push(dioxus::diff::DiffInstruction::DiffNode { new, old }); + machine.stack.push(DiffInstruction::DiffNode { new, old }); machine.mutations } @@ -58,10 +53,8 @@ impl TestDom { let mut machine = DiffMachine::new_headless(&self.resources); - machine.stack.push(dioxus::diff::DiffInstruction::Create { - node: old, - and: dioxus::diff::MountType::Append, - }); + machine.stack.create_node(old, MountType::Append); + work_sync(&mut machine); machine.mutations @@ -83,10 +76,9 @@ impl TestDom { // let mut create_edits = Vec::new(); let mut machine = DiffMachine::new_headless(&self.resources); - machine.stack.push(dioxus::diff::DiffInstruction::Create { - and: dioxus::diff::MountType::Append, - node: old, - }); + + machine.stack.create_node(old, MountType::Append); + work_sync(&mut machine); let create_edits = machine.mutations; diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs index cb593dee7..919b0a447 100644 --- a/packages/core/tests/eventsystem.rs +++ b/packages/core/tests/eventsystem.rs @@ -1,12 +1,7 @@ use bumpalo::Bump; use anyhow::{Context, Result}; -use dioxus::{ - arena::SharedResources, - diff::{CreateMeta, DiffMachine}, - prelude::*, - DomEdit, -}; +use dioxus::{arena::SharedResources, diff::DiffMachine, prelude::*, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/hooks.rs b/packages/core/tests/hooks.rs index 2c434eabe..42f15db4b 100644 --- a/packages/core/tests/hooks.rs +++ b/packages/core/tests/hooks.rs @@ -1,10 +1,5 @@ use anyhow::{Context, Result}; -use dioxus::{ - arena::SharedResources, - diff::{CreateMeta, DiffMachine}, - prelude::*, - DomEdit, -}; +use dioxus::{arena::SharedResources, diff::DiffMachine, prelude::*, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/test_logging.rs b/packages/core/tests/test_logging.rs index 8783dd6d2..41498a9e9 100644 --- a/packages/core/tests/test_logging.rs +++ b/packages/core/tests/test_logging.rs @@ -17,7 +17,9 @@ pub fn set_up_logging() { // just clone `colors_line` and overwrite our changes let colors_level = colors_line.clone().info(Color::Green); // here we set up our fern Dispatch - fern::Dispatch::new() + + // when running tests in batch, the logger is re-used, so ignore the logger error + let _ = fern::Dispatch::new() .format(move |out, message, record| { out.finish(format_args!( "{color_line}[{level}{color_line}] {message}\x1B[0m", @@ -42,6 +44,5 @@ pub fn set_up_logging() { // .level_for("pretty_colored", log::LevelFilter::Trace) // output to stdout .chain(std::io::stdout()) - .apply() - .unwrap(); + .apply(); } diff --git a/packages/core/tests/work_sync.rs b/packages/core/tests/work_sync.rs index 187a4443b..ed9269bc7 100644 --- a/packages/core/tests/work_sync.rs +++ b/packages/core/tests/work_sync.rs @@ -3,12 +3,7 @@ //! This means you can actually call it synchronously if you want. use anyhow::{Context, Result}; -use dioxus::{ - arena::SharedResources, - diff::{CreateMeta, DiffInstruction, DiffMachine}, - prelude::*, - scope::Scope, -}; +use dioxus::{arena::SharedResources, diff::DiffMachine, prelude::*, scope::Scope}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; use futures_util::FutureExt; From e6f56563bc84516e017b3db06f11fab2549b9a50 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 23 Aug 2021 13:15:57 -0400 Subject: [PATCH 19/53] feat: tests and benchmarks --- packages/core/.vscode/settings.json | 2 +- packages/core/Cargo.toml | 5 + packages/core/benches/create.rs | 8 +- packages/core/benches/jsframework.rs | 131 ++++++++++++++++++++++++++ packages/core/examples/jsframework.rs | 109 +++++++++++++++++++++ packages/core/src/diff.rs | 6 +- 6 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 packages/core/benches/jsframework.rs create mode 100644 packages/core/examples/jsframework.rs diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 79505b018..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 264a6c56d..005afdd29 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -49,6 +49,7 @@ async-std = { version = "1.9.0", features = ["attributes"] } criterion = "0.3.5" dioxus-html = { path = "../html" } fern = { version = "0.6.0", features = ["colored"] } +rand = { version = "0.8.4", features = ["small_rng"] } simple_logger = "1.13.0" @@ -59,3 +60,7 @@ serialize = ["serde"] [[bench]] name = "create" harness = false + +[[bench]] +name = "jsframework" +harness = false diff --git a/packages/core/benches/create.rs b/packages/core/benches/create.rs index 2b0c26ef5..2f614ac0e 100644 --- a/packages/core/benches/create.rs +++ b/packages/core/benches/create.rs @@ -13,10 +13,4 @@ fn criterion_benchmark(c: &mut Criterion) { } criterion_group!(benches, criterion_benchmark); - -fn main() { - benches(); - Criterion::default().configure_from_args().final_summary(); - // $crate::__warn_about_html_reports_feature(); - // $crate::__warn_about_cargo_bench_support_feature(); -} +criterion_main!(benches); diff --git a/packages/core/benches/jsframework.rs b/packages/core/benches/jsframework.rs new file mode 100644 index 000000000..91b97dd74 --- /dev/null +++ b/packages/core/benches/jsframework.rs @@ -0,0 +1,131 @@ +//! This benchmark tests just the overhead of Dioxus itself. +//! +//! For the JS Framework Benchmark, both the framework and the browser is benchmarked together. Dioxus prepares changes +//! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence, +//! we are measuring the overhead of Dioxus, not the performance of the "apply" phase. +//! +//! On my MBP 2019: +//! - Dioxus takes 3ms to create 1_000 rows +//! - Dioxus takes 30ms to create 10_000 rows +//! +//! As pure "overhead", these are really really good numbers, mostly slowed down by hitting the global allocator. +//! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster. + +use std::fmt::Display; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_html as dioxus_elements; +use rand::prelude::*; + +criterion_group!(mbenches, create_rows); +criterion_main!(mbenches); + +fn create_rows(c: &mut Criterion) { + static App: FC<()> = |cx| { + let mut rng = SmallRng::from_entropy(); + let rows = (0..10_000).map(|f| { + let label = Label::new(&mut rng); + rsx! { + Row { + row_id: f, + label: label + } + } + }); + cx.render(rsx! { + table { + tbody { + {rows} + } + } + }) + }; + + c.bench_function("create rows", |b| { + b.iter(|| { + let mut dom = VirtualDom::new(App); + let g = dom.rebuild().unwrap(); + assert!(g.edits.len() > 1); + }) + }); +} + +#[derive(PartialEq, Props)] +struct RowProps { + row_id: usize, + label: Label, +} +fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree { + cx.render(rsx! { + tr { + td { class:"col-md-1", "{cx.row_id}" } + td { class:"col-md-1", onclick: move |_| { /* run onselect */ } + a { class: "lbl", "{cx.label}" } + } + td { class: "col-md-1" + a { class: "remove", onclick: move |_| {/* remove */} + span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" } + } + } + td { class: "col-md-6" } + } + }) +} + +#[derive(PartialEq)] +struct Label([&'static str; 3]); + +impl Label { + fn new(rng: &mut SmallRng) -> Self { + Label([ + ADJECTIVES.choose(rng).unwrap(), + COLOURS.choose(rng).unwrap(), + NOUNS.choose(rng).unwrap(), + ]) + } +} +impl Display for Label { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {} {}", self.0[0], self.0[1], self.0[2]) + } +} + +static ADJECTIVES: &[&str] = &[ + "pretty", + "large", + "big", + "small", + "tall", + "short", + "long", + "handsome", + "plain", + "quaint", + "clean", + "elegant", + "easy", + "angry", + "crazy", + "helpful", + "mushy", + "odd", + "unsightly", + "adorable", + "important", + "inexpensive", + "cheap", + "expensive", + "fancy", +]; + +static COLOURS: &[&str] = &[ + "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", + "orange", +]; + +static NOUNS: &[&str] = &[ + "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", + "pizza", "mouse", "keyboard", +]; diff --git a/packages/core/examples/jsframework.rs b/packages/core/examples/jsframework.rs new file mode 100644 index 000000000..84be02832 --- /dev/null +++ b/packages/core/examples/jsframework.rs @@ -0,0 +1,109 @@ +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_html as dioxus_elements; +use rand::prelude::*; +use std::fmt::Display; + +fn main() { + let mut dom = VirtualDom::new(App); + let g = dom.rebuild().unwrap(); + assert!(g.edits.len() > 1); +} + +static App: FC<()> = |cx| { + let mut rng = SmallRng::from_entropy(); + let rows = (0..10_000).map(|f| { + let label = Label::new(&mut rng); + rsx! { + Row { + row_id: f, + label: label + } + } + }); + cx.render(rsx! { + table { + tbody { + {rows} + } + } + }) +}; + +#[derive(PartialEq, Props)] +struct RowProps { + row_id: usize, + label: Label, +} +fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree { + cx.render(rsx! { + tr { + td { class:"col-md-1", "{cx.row_id}" } + td { class:"col-md-1", onclick: move |_| { /* run onselect */ } + a { class: "lbl", "{cx.label}" } + } + td { class: "col-md-1" + a { class: "remove", onclick: move |_| {/* remove */} + span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" } + } + } + td { class: "col-md-6" } + } + }) +} + +#[derive(PartialEq)] +struct Label([&'static str; 3]); + +impl Label { + fn new(rng: &mut SmallRng) -> Self { + Label([ + ADJECTIVES.choose(rng).unwrap(), + COLOURS.choose(rng).unwrap(), + NOUNS.choose(rng).unwrap(), + ]) + } +} +impl Display for Label { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {} {}", self.0[0], self.0[1], self.0[2]) + } +} + +static ADJECTIVES: &[&str] = &[ + "pretty", + "large", + "big", + "small", + "tall", + "short", + "long", + "handsome", + "plain", + "quaint", + "clean", + "elegant", + "easy", + "angry", + "crazy", + "helpful", + "mushy", + "odd", + "unsightly", + "adorable", + "important", + "inexpensive", + "cheap", + "expensive", + "fancy", +]; + +static COLOURS: &[&str] = &[ + "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", + "orange", +]; + +static NOUNS: &[&str] = &[ + "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", + "pizza", "mouse", "keyboard", +]; diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 4c4894161..cc46650c5 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -945,7 +945,8 @@ impl<'bump> DiffMachine<'bump> { for child in RealChildIterator::new(next_new, self.vdom) { let el = child.direct_id(); self.mutations.push_root(el); - self.mutations.insert_before(1); + todo!(); + // self.mutations.insert_before(1); } } else { self.stack.create_node( @@ -990,7 +991,8 @@ impl<'bump> DiffMachine<'bump> { for child in RealChildIterator::new(last_node, self.vdom) { let el = child.direct_id(); self.mutations.push_root(el); - self.mutations.insert_after(1); + // self.mutations.insert_after(1); + todo!(); } } else { eprintln!("key is not contained {:?}", key); From 082765f6873d9ba72d67e3383dfab6c901eb772a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 23 Aug 2021 13:41:47 -0400 Subject: [PATCH 20/53] wip: refactor non_keyed diff --- packages/core/.vscode/settings.json | 2 +- packages/core/src/diff.rs | 186 +++++++++------------------- packages/core/src/diff_stack.rs | 10 +- 3 files changed, 62 insertions(+), 136 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index cc46650c5..d048fb094 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -185,14 +185,6 @@ impl<'bump> DiffMachine<'bump> { self.create_node(node); } - DiffInstruction::Remove { child } => { - self.remove_nodes(Some(child)); - } - - DiffInstruction::RemoveChildren { children } => { - self.remove_nodes(children); - } - DiffInstruction::Mount { and } => { self.mount(and); } @@ -219,7 +211,7 @@ impl<'bump> DiffMachine<'bump> { let root = todo!(); self.mutations.replace_with(root, nodes_created as u32); } - MountType::ReplaceByElementId { old } => { + MountType::ReplaceByElementId { el: old } => { self.mutations.replace_with(old, nodes_created as u32); } @@ -584,73 +576,79 @@ impl<'bump> DiffMachine<'bump> { // 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) => { + match (old, new) { + ([], []) => {} + ([], _) => { self.stack.create_children(new, MountType::Append); } - - // Completely removing old nodes and putting an anchor in its place - // no anchor (old has nodes) and the new is empty - // remove all the old nodes - (IS_NOT_EMPTY, IS_EMPTY) => { + (_, []) => { for node in old { self.remove_nodes(Some(node)); } } + ([VNode::Anchor(old_anchor)], [VNode::Anchor(new_anchor)]) => { + old_anchor.dom_id.set(new_anchor.dom_id.get()); + } + ([VNode::Anchor(anchor)], _) => { + let el = anchor.dom_id.get().unwrap(); + self.stack + .create_children(new, MountType::ReplaceByElementId { el }); + } + (_, [VNode::Anchor(_)]) => { + self.replace_and_create_many_with_one(old, &new[0]); + } + _ => { + let new_is_keyed = new[0].key().is_some(); + let old_is_keyed = old[0].key().is_some(); - (IS_NOT_EMPTY, IS_NOT_EMPTY) => { - let first_old = &old[0]; - let first_new = &new[0]; + 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" + ); - match (&first_old, &first_new) { - // Anchors can only appear in empty fragments - (VNode::Anchor(old_anchor), VNode::Anchor(new_anchor)) => { - old_anchor.dom_id.set(new_anchor.dom_id.get()); - } - - // Replace the anchor with whatever new nodes are coming down the pipe - (VNode::Anchor(anchor), _) => { - self.stack - .create_children(new, MountType::Replace { old: first_old }); - } - - // Replace whatever nodes are sitting there with the anchor - (_, VNode::Anchor(anchor)) => { - self.replace_and_create_many_with_one(old, first_new); - } - - // Use the complex diff algorithm to diff the nodes - _ => { - 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); - } - } + 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: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + // Handled these cases in `diff_children` before calling this function. + debug_assert!(!new.is_empty()); + debug_assert!(!old.is_empty()); + + for (new, old) in new.iter().zip(old.iter()).rev() { + self.stack.push(DiffInstruction::DiffNode { new, old }); + } + + if old.len() > new.len() { + self.remove_nodes(&old[new.len()..]); + } else { + self.stack.create_children( + &new[old.len()..], + MountType::InsertAfter { + other_node: old.last().unwrap(), + }, + ); + } + } + // Diffing "keyed" children. // // With keyed children, we care about whether we delete, move, or create nodes @@ -1027,70 +1025,6 @@ impl<'bump> DiffMachine<'bump> { } } - // 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: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { - // Handled these cases in `diff_children` before calling this function. - // - debug_assert!(!new.is_empty()); - debug_assert!(!old.is_empty()); - - match old.len().cmp(&new.len()) { - // old.len > new.len -> removing some nodes - Ordering::Greater => { - // Generate instructions to diff the existing elements - for (new_child, old_child) in new.iter().zip(old.iter()).rev() { - self.stack.push(DiffInstruction::DiffNode { - new: new_child, - old: old_child, - }); - } - - self.stack.push(DiffInstruction::RemoveChildren { - children: &old[new.len()..], - }); - } - - // old.len < new.len -> adding some nodes - // this is wrong in the case where we're diffing fragments - // - // we need to save the last old element and then replace it with all the new ones - Ordering::Less => { - // Generate instructions to diff the existing elements - for (new_child, old_child) in new.iter().zip(old.iter()).rev() { - self.stack.push(DiffInstruction::DiffNode { - new: new_child, - old: old_child, - }); - } - - // Generate instructions to add in the new elements - self.stack.create_children( - &new[old.len()..], - MountType::InsertAfter { - other_node: old.last().unwrap(), - }, - ); - } - - // old.len == new.len -> no nodes added/removed, but perhaps changed - Ordering::Equal => { - for (new_child, old_child) in new.iter().zip(old.iter()).rev() { - self.stack.push(DiffInstruction::DiffNode { - new: new_child, - old: old_child, - }); - } - } - } - } - // ===================== // Utilities // ===================== diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index b82658e29..a0528e947 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -18,14 +18,6 @@ pub enum DiffInstruction<'a> { node: &'a VNode<'a>, }, - Remove { - child: &'a VNode<'a>, - }, - - RemoveChildren { - children: &'a [VNode<'a>], - }, - Mount { and: MountType<'a>, }, @@ -40,7 +32,7 @@ pub enum MountType<'a> { Absorb, Append, Replace { old: &'a VNode<'a> }, - ReplaceByElementId { old: ElementId }, + ReplaceByElementId { el: ElementId }, InsertAfter { other_node: &'a VNode<'a> }, InsertBefore { other_node: &'a VNode<'a> }, } From 778baffb10c2b14ac350b38229d1b3acfa49ec39 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 23 Aug 2021 15:35:26 -0400 Subject: [PATCH 21/53] wip: it works? --- packages/core/.vscode/settings.json | 2 +- packages/core/src/diff.rs | 329 ++++++++-------------------- packages/core/src/diff_stack.rs | 10 + 3 files changed, 103 insertions(+), 238 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 79505b018..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index d048fb094..53522c0b2 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -188,6 +188,10 @@ impl<'bump> DiffMachine<'bump> { DiffInstruction::Mount { and } => { self.mount(and); } + + DiffInstruction::PrepareMoveNode { node } => { + // + } }; } @@ -224,6 +228,12 @@ impl<'bump> DiffMachine<'bump> { let root = self.find_first_element(other_node).direct_id(); self.mutations.insert_before(root, nodes_created as u32); } + + MountType::InsertAfterFlush { other_node } => { + self.stack.push_nodes_created(0); + let root = self.find_last_element(other_node).direct_id(); + self.mutations.insert_after(root, nodes_created as u32); + } } } @@ -693,94 +703,72 @@ impl<'bump> DiffMachine<'bump> { // // `shared_prefix_count` is the count of how many nodes at the start of // `new` and `old` share the same keys. - // - // TODO: just inline this - let shared_prefix_count = match self.diff_keyed_prefix(old, new) { - KeyedPrefixResult::Finished => return, - KeyedPrefixResult::MoreWorkToDo(count) => count, + let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { + Some(count) => count, + None => return, }; - // Next, we find out how many of the nodes at the end of the children have - // the same key. We do _not_ diff them yet, since we want to emit the change - // list instructions such that they can be applied in a single pass over the - // DOM. Instead, we just save this information for later. - // - // `shared_suffix_count` is the count of how many nodes at the end of `new` - // and `old` share the same keys. - let shared_suffix_count = old[shared_prefix_count..] - .iter() - .rev() - .zip(new[shared_prefix_count..].iter().rev()) - .take_while(|&(old, new)| old.key() == new.key()) - .count(); - - let old_shared_suffix_start = old.len() - shared_suffix_count; - let new_shared_suffix_start = new.len() - shared_suffix_count; - // 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. self.diff_keyed_middle( - &old[shared_prefix_count..old_shared_suffix_start], - &new[shared_prefix_count..new_shared_suffix_start], - shared_prefix_count, - shared_suffix_count, - old_shared_suffix_start, + &old[left_offset..(old.len() - right_offset)], + &new[left_offset..(new.len() - right_offset)], ); - - // Finally, diff the nodes at the end of `old` and `new` that share keys. - let old_suffix = &old[old_shared_suffix_start..]; - let new_suffix = &new[new_shared_suffix_start..]; - debug_assert_eq!(old_suffix.len(), new_suffix.len()); - if !old_suffix.is_empty() { - self.diff_keyed_suffix(old_suffix, new_suffix, new_shared_suffix_start) - } } - // Diff the prefix of children in `new` and `old` that share the same keys in - // the same order. - // - // The stack is empty upon entry. - fn diff_keyed_prefix( + /// 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: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>], - ) -> KeyedPrefixResult { - let mut shared_prefix_count = 0; + ) -> 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); - shared_prefix_count += 1; + self.stack.push(DiffInstruction::DiffNode { 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 shared_prefix_count == old.len() { - // Load the last element - let last_node = self.find_last_element(new.last().unwrap()).direct_id(); - self.mutations.push_root(last_node); - - // Create the new children and insert them after - // - todo!(); - // let meta = self.create_children(&new[shared_prefix_count..]); - // self.mutations.insert_after(meta.added_to_stack); - - return KeyedPrefixResult::Finished; + if left_offset == old.len() { + self.stack.create_children( + &new[left_offset..], + MountType::InsertAfter { + other_node: 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 shared_prefix_count == new.len() { - self.remove_nodes(&old[shared_prefix_count..]); - return KeyedPrefixResult::Finished; + if left_offset == new.len() { + self.remove_nodes(&old[left_offset..]); + return None; } - KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) + // 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. @@ -796,14 +784,7 @@ impl<'bump> DiffMachine<'bump> { // 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 state. - fn diff_keyed_middle( - &mut self, - old: &'bump [VNode<'bump>], - mut new: &'bump [VNode<'bump>], - shared_prefix_count: usize, - shared_suffix_count: usize, - old_shared_suffix_start: usize, - ) { + fn diff_keyed_middle(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { /* 1. Map the old keys into a numerical ordering based on indicies. 2. Create a map of old key to its index @@ -841,39 +822,30 @@ impl<'bump> DiffMachine<'bump> { .collect::>(); let mut shared_keys = FxHashSet::default(); - let mut to_add = 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(|n| { - let key = n.key().unwrap(); + .map(|node| { + let key = node.key().unwrap(); 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::>(); - // 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_suffix_count == 0 && shared_keys.is_empty() { + // 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() { self.replace_and_create_many_with_many(old, new); 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 - // correctly within `new`. We will leave these nodes in place in the DOM, - // and only move nodes that are not part of the LIS. This results in the - // maximum number of DOM nodes left in place, AKA the minimum number of DOM - // nodes moved. + // TODO: investigate if we can go without the hashset here, I can't quite tell from the LIS crate let mut new_index_is_in_lis = FxHashSet::default(); new_index_is_in_lis.reserve(new_index_to_old_index.len()); @@ -888,141 +860,50 @@ impl<'bump> DiffMachine<'bump> { &mut starts, ); - // 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(); + // REMEMBER: we're generating instructions. we cannot rely on anything stack related to be consistent. - // 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 { - let new_node = &new[*id]; - let key = new_node.key().unwrap(); - let old_index = old_key_to_old_index.get(&key).unwrap(); - let old_node = &old[*old_index]; - self.diff_node(old_node, new_node); - } + // walk the new list, creating nodes or diffing them. if we hit an LIS, then we call "insert before" + let mut last_lis = 0; - // return the old node from the key - let load_old_node_from_lsi = |key| -> &VNode { - let old_index = old_key_to_old_index.get(key).unwrap(); - let old_node = &old[*old_index]; - old_node - }; + self.stack.push_nodes_created(0); + for (idx, new_node) in new.into_iter().enumerate().rev() { + let old_index = new_index_to_old_index[idx]; - let mut root = None; - let mut new_iter = new.iter().enumerate(); - for lis_id in &lis_in_order { - eprintln!("tracking {:?}", lis_id); - // this is the next milestone node we are working up to - let new_anchor = &new[*lis_id]; - root = Some(new_anchor); - - // let anchor_el = self.find_first_element(new_anchor); - // self.mutations.push_root(anchor_el.direct_id()); - // // let mut pushed = false; - - 'inner: loop { - let (next_id, next_new) = new_iter.next().unwrap(); - if next_id == *lis_id { - // we've reached the milestone, break this loop so we can step to the next milestone - // remember: we already diffed this node - eprintln!("breaking {:?}", next_id); - break 'inner; - } else { - let key = next_new.key().unwrap(); - eprintln!("found key {:?}", key); - if shared_keys.contains(&key) { - eprintln!("key is contained {:?}", key); - shared_keys.remove(key); - // diff the two nodes - let old_node = load_old_node_from_lsi(key); - self.diff_node(old_node, next_new); - - // now move all the nodes into the right spot - for child in RealChildIterator::new(next_new, self.vdom) { - let el = child.direct_id(); - self.mutations.push_root(el); - todo!(); - // self.mutations.insert_before(1); - } - } else { - self.stack.create_node( - next_new, - MountType::InsertBefore { - other_node: new_anchor, - }, - ); - } - } - } - - self.mutations.pop(); - } - - let final_lis_node = root.unwrap(); - let final_el_node = self.find_last_element(final_lis_node); - let final_el = final_el_node.direct_id(); - self.mutations.push_root(final_el); - - let mut last_iter = new.iter().rev().enumerate(); - let last_key = final_lis_node.key().unwrap(); - loop { - let (last_id, last_node) = last_iter.next().unwrap(); - let key = last_node.key().unwrap(); - - eprintln!("checking final nodes {:?}", key); - - if last_key == key { - eprintln!("breaking final nodes"); - break; - } - - if shared_keys.contains(&key) { - eprintln!("key is contained {:?}", key); - shared_keys.remove(key); - // diff the two nodes - let old_node = load_old_node_from_lsi(key); - self.diff_node(old_node, last_node); - - // now move all the nodes into the right spot - for child in RealChildIterator::new(last_node, self.vdom) { - let el = child.direct_id(); - self.mutations.push_root(el); - // self.mutations.insert_after(1); - todo!(); - } + if old_index == u32::MAX as usize { + self.stack.create_node(new_node, MountType::Absorb); } 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(last_node); - // self.mutations.insert_after(meta.added_to_stack); + // diff / move + if let Some(new_index) = new_index_is_in_lis.get(&idx) { + // take all the nodes on the stack and insert them before this one + self.stack.push(DiffInstruction::Mount { + and: MountType::InsertAfterFlush { + other_node: new_node, + }, + }); + self.stack.push(DiffInstruction::DiffNode { + new: new_node, + old: &old[old_index], + }); + last_lis = *new_index; + } else { + // this is not an LIS node, we need to diff it and move it + self.stack + .push(DiffInstruction::PrepareMoveNode { node: new_node }); + self.stack.push(DiffInstruction::DiffNode { + new: new_node, + old: &old[old_index], + }); + } } } - self.mutations.pop(); - } - // Diff the suffix of keyed children that share the same keys in the same order. - // - // The parent must be on the change list stack when we enter this function: - // - // [... parent] - // - // When this function exits, the change list stack remains the same. - fn diff_keyed_suffix( - &mut self, - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], - new_shared_suffix_start: usize, - ) { - debug_assert_eq!(old.len(), new.len()); - debug_assert!(!old.is_empty()); - - for (old_child, new_child) in old.iter().zip(new.iter()) { - self.diff_node(old_child, new_child); - } + // There will be nodes left on top of the stack. They need to be inserted before the final + let last_node = &old[last_lis]; + self.stack.push(DiffInstruction::Mount { + and: MountType::InsertBefore { + other_node: last_node, + }, + }); } // ===================== @@ -1075,14 +956,6 @@ impl<'bump> DiffMachine<'bump> { } } - fn replace_many_with_many(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { - // - } - - fn replace_one_with_many(&mut self, old: &'bump VNode<'bump>, new: &'bump [VNode<'bump>]) { - self.stack.create_children(new, MountType::Replace { old }); - } - fn replace_and_create_many_with_one( &mut self, old: &'bump [VNode<'bump>], @@ -1230,15 +1103,6 @@ impl<'bump> DiffMachine<'bump> { } } - fn replace_node_with_node( - &mut self, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, - ) { - self.stack - .create_node(new_node, MountType::Replace { old: old_node }); - } - fn fix_listener<'a>(&mut self, listener: &'a Listener<'a>) { let scope_id = self.stack.current_scope(); if let Some(scope_id) = scope_id { @@ -1249,12 +1113,3 @@ impl<'bump> DiffMachine<'bump> { } } } - -enum KeyedPrefixResult { - // Fast path: we finished diffing all the children just by looking at the - // prefix of shared keys! - Finished, - // There is more diffing work to do. Here is a count of how many children at - // the beginning of `new` and `old` we already processed. - MoreWorkToDo(usize), -} diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index a0528e947..6876faa96 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -18,6 +18,11 @@ pub enum DiffInstruction<'a> { node: &'a VNode<'a>, }, + /// pushes the node elements onto the stack for use in mount + PrepareMoveNode { + node: &'a VNode<'a>, + }, + Mount { and: MountType<'a>, }, @@ -34,6 +39,7 @@ pub enum MountType<'a> { Replace { old: &'a VNode<'a> }, ReplaceByElementId { el: ElementId }, InsertAfter { other_node: &'a VNode<'a> }, + InsertAfterFlush { other_node: &'a VNode<'a> }, InsertBefore { other_node: &'a VNode<'a> }, } @@ -74,6 +80,10 @@ impl<'bump> DiffStack<'bump> { } } + pub fn push_nodes_created(&mut self, count: usize) { + self.nodes_created_stack.push(count); + } + pub fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) { self.nodes_created_stack.push(0); self.instructions.push(DiffInstruction::Mount { and }); From 0479252a5fba96e554dcf2422a8566e248fc6593 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 24 Aug 2021 10:01:01 -0400 Subject: [PATCH 22/53] feat: keyed diffing --- packages/core/src/childiter.rs | 27 +- packages/core/src/diff.rs | 196 +++++++++---- packages/core/src/diff_stack.rs | 1 - packages/core/src/nodes.rs | 8 +- packages/core/tests/diffing.rs | 470 ++++++++++++++++++++++++++++---- 5 files changed, 563 insertions(+), 139 deletions(-) diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs index e3c520d1c..936a632be 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/childiter.rs @@ -48,7 +48,7 @@ impl<'a> Iterator for RealChildIterator<'a> { match &node { // We can only exit our looping when we get "real" nodes // This includes fragments and components when they're empty (have a single root) - VNode::Element(_) | VNode::Text(_) => { + VNode::Element(_) | VNode::Text(_) | VNode::Suspended(_) | VNode::Anchor(_) => { // We've recursed INTO an element/text // We need to recurse *out* of it and move forward to the next should_pop = true; @@ -70,31 +70,6 @@ impl<'a> Iterator for RealChildIterator<'a> { should_push = Some(&frag.children[subcount]); } } - // // If we get a fragment we push the next child - // VNodeKind::Fragment(frag) => { - // let subcount = *count as usize; - - // if frag.children.len() == 0 { - // should_pop = true; - // returned_node = Some(&*node); - // } - - // if subcount >= frag.children.len() { - // should_pop = true; - // } else { - // should_push = Some(&frag.children[subcount]); - // } - // } - - // Immediately abort suspended nodes - can't do anything with them yet - VNode::Suspended(node) => { - // VNodeKind::Suspended => should_pop = true, - todo!() - } - - VNode::Anchor(a) => { - todo!() - } // For components, we load their root and push them onto the stack VNode::Component(sc) => { diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 53522c0b2..db5d9930b 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -190,7 +190,11 @@ impl<'bump> DiffMachine<'bump> { } DiffInstruction::PrepareMoveNode { node } => { - // + log::debug!("Preparing to move node: {:?}", node); + for el in RealChildIterator::new(node, self.vdom) { + self.mutations.push_root(el.direct_id()); + self.stack.add_child_count(1); + } } }; } @@ -211,10 +215,15 @@ impl<'bump> DiffMachine<'bump> { many: nodes_created as u32, }); } + MountType::Replace { old } => { - let root = todo!(); - self.mutations.replace_with(root, nodes_created as u32); + let mut iter = RealChildIterator::new(old, self.vdom); + let first = iter.next().unwrap(); + self.mutations + .replace_with(first.direct_id(), nodes_created as u32); + self.remove_nodes(iter); } + MountType::ReplaceByElementId { el: old } => { self.mutations.replace_with(old, nodes_created as u32); } @@ -225,15 +234,9 @@ impl<'bump> DiffMachine<'bump> { } MountType::InsertBefore { other_node } => { - let root = self.find_first_element(other_node).direct_id(); + let root = self.find_first_element_id(other_node).unwrap(); self.mutations.insert_before(root, nodes_created as u32); } - - MountType::InsertAfterFlush { other_node } => { - self.stack.push_nodes_created(0); - let root = self.find_last_element(other_node).direct_id(); - self.mutations.insert_after(root, nodes_created as u32); - } } } @@ -415,8 +418,15 @@ impl<'bump> DiffMachine<'bump> { // // This case is rather rare (typically only in non-keyed lists) if new.tag_name != old.tag_name || new.namespace != old.namespace { - todo!("element changed"); - // self.replace_node_with_node(old, new); + // maybe make this an instruction? + // issue is that we need the "vnode" but this method only has the velement + self.stack.push_nodes_created(0); + self.stack.push(DiffInstruction::Mount { + and: MountType::ReplaceByElementId { + el: old.dom_id.get().unwrap(), + }, + }); + self.create_element_node(new); return; } @@ -640,6 +650,7 @@ impl<'bump> DiffMachine<'bump> { // 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>]) { // Handled these cases in `diff_children` before calling this function. + log::debug!("diffing non-keyed case"); debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); @@ -649,7 +660,8 @@ impl<'bump> DiffMachine<'bump> { if old.len() > new.len() { self.remove_nodes(&old[new.len()..]); - } else { + } else if new.len() > old.len() { + log::debug!("Calling create children on array differences"); self.stack.create_children( &new[old.len()..], MountType::InsertAfter { @@ -707,6 +719,11 @@ impl<'bump> DiffMachine<'bump> { Some(count) => count, None => return, }; + log::debug!( + "Left offset, right offset, {}, {}", + left_offset, + right_offset, + ); // 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 @@ -846,64 +863,107 @@ impl<'bump> DiffMachine<'bump> { // 4. Compute the LIS of this list // TODO: investigate if we can go without the hashset here, I can't quite tell from the LIS crate - let mut new_index_is_in_lis = FxHashSet::default(); - new_index_is_in_lis.reserve(new_index_to_old_index.len()); + // let mut new_index_is_in_lis = Vec::default(); + 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 new_index_is_in_lis, + &mut lis_sequence, |a, b| a < b, &mut predecessors, &mut starts, ); - // REMEMBER: we're generating instructions. we cannot rely on anything stack related to be consistent. + // the lis comes out backwards, I think. can't quite tell + lis_sequence.sort_unstable(); - // walk the new list, creating nodes or diffing them. if we hit an LIS, then we call "insert before" - let mut last_lis = 0; + // 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(); + } - self.stack.push_nodes_created(0); - for (idx, new_node) in new.into_iter().enumerate().rev() { - let old_index = new_index_to_old_index[idx]; + log::debug!("LIS Indicies: {:?}", lis_sequence); + let apply = |new_idx, new_node: &'bump VNode<'bump>, stack: &mut DiffStack<'bump>| { + log::debug!("applying {:?}", new_node); + + let old_index = new_index_to_old_index[new_idx]; + dbg!(new_idx, old_index); if old_index == u32::MAX as usize { - self.stack.create_node(new_node, MountType::Absorb); + stack.create_node(new_node, MountType::Absorb); } else { - // diff / move - if let Some(new_index) = new_index_is_in_lis.get(&idx) { - // take all the nodes on the stack and insert them before this one - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertAfterFlush { - other_node: new_node, - }, - }); - self.stack.push(DiffInstruction::DiffNode { - new: new_node, - old: &old[old_index], - }); - last_lis = *new_index; - } else { - // this is not an LIS node, we need to diff it and move it - self.stack - .push(DiffInstruction::PrepareMoveNode { node: new_node }); - self.stack.push(DiffInstruction::DiffNode { - new: new_node, - old: &old[old_index], - }); - } + // this funciton should never take LIS indicies + stack.push(DiffInstruction::PrepareMoveNode { node: new_node }); + stack.push(DiffInstruction::DiffNode { + new: new_node, + old: &old[old_index], + }); + } + }; + + // add mount instruction for the last items not covered by the lis + let first_lis = *lis_sequence.first().unwrap(); + dbg!(first_lis); + if first_lis > 0 { + self.stack.push_nodes_created(0); + self.stack.push(DiffInstruction::Mount { + and: MountType::InsertBefore { + other_node: &new[first_lis], + }, + }); + + for (idx, new_node) in new[..first_lis].iter().enumerate().rev() { + apply(idx, new_node, &mut self.stack); } } - // There will be nodes left on top of the stack. They need to be inserted before the final - let last_node = &old[last_lis]; - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertBefore { - other_node: last_node, - }, - }); + // for each spacing, generate a mount instruction + let mut lis_iter = lis_sequence.iter().rev(); + let mut last = *lis_iter.next().unwrap(); + while let Some(&next) = lis_iter.next() { + if last - next > 1 { + self.stack.push_nodes_created(0); + self.stack.push(DiffInstruction::Mount { + and: MountType::InsertBefore { + other_node: &new[last], + }, + }); + for (idx, new_node) in new[(next + 1)..last].iter().enumerate().rev() { + apply(idx + next + 1, new_node, &mut self.stack); + } + } + last = next; + } + + // add mount instruction for the first items not covered by the lis + let last_lis = *lis_sequence.last().unwrap(); + log::debug!("last lis is {}, new len is {}", last_lis, new.len()); + if last_lis < (new.len() - 1) { + let after = &new[last_lis]; + dbg!(after); + self.stack.push_nodes_created(0); + self.stack.push(DiffInstruction::Mount { + and: MountType::InsertAfter { other_node: after }, + }); + + for (idx, new_node) in new[(last_lis + 1)..].iter().enumerate().rev() { + apply(idx + last_lis + 1, new_node, &mut self.stack); + } + } + + for idx in lis_sequence.iter().rev() { + let old_index = new_index_to_old_index[*idx]; + let new_node = &new[*idx]; + log::debug!("diffing {:?}", new_node); + self.stack.push(DiffInstruction::DiffNode { + new: new_node, + old: &old[old_index], + }); + } } // ===================== @@ -933,17 +993,13 @@ impl<'bump> DiffMachine<'bump> { } } - fn find_first_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { + fn find_first_element_id(&mut self, vnode: &'bump VNode<'bump>) -> Option { let mut search_node = Some(vnode); loop { let node = search_node.take().unwrap(); match &node { // the ones that have a direct id - VNode::Text(_) | VNode::Element(_) | VNode::Anchor(_) | VNode::Suspended(_) => { - break node - } - VNode::Fragment(frag) => { search_node = Some(&frag.children[0]); } @@ -952,9 +1008,35 @@ impl<'bump> DiffMachine<'bump> { let scope = self.vdom.get_scope(scope_id).unwrap(); search_node = Some(scope.root()); } + VNode::Text(t) => break t.dom_id.get(), + VNode::Element(t) => break t.dom_id.get(), + VNode::Suspended(t) => break t.node.get(), + VNode::Anchor(t) => break t.dom_id.get(), } } } + // fn find_first_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { + // let mut search_node = Some(vnode); + + // loop { + // let node = search_node.take().unwrap(); + // match &node { + // // the ones that have a direct id + // VNode::Text(_) | VNode::Element(_) | VNode::Anchor(_) | VNode::Suspended(_) => { + // break node + // } + + // VNode::Fragment(frag) => { + // search_node = Some(&frag.children[0]); + // } + // VNode::Component(el) => { + // let scope_id = el.ass_scope.get().unwrap(); + // let scope = self.vdom.get_scope(scope_id).unwrap(); + // search_node = Some(scope.root()); + // } + // } + // } + // } fn replace_and_create_many_with_one( &mut self, diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index 6876faa96..9819412d8 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -39,7 +39,6 @@ pub enum MountType<'a> { Replace { old: &'a VNode<'a> }, ReplaceByElementId { el: ElementId }, InsertAfter { other_node: &'a VNode<'a> }, - InsertAfterFlush { other_node: &'a VNode<'a> }, InsertBefore { other_node: &'a VNode<'a> }, } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 14d21a5a7..8671cf45d 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -651,7 +651,13 @@ impl Debug for NodeFactory<'_> { impl Debug for VNode<'_> { fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { match &self { - VNode::Element(el) => write!(s, "VElement {{ name: {} }}", el.tag_name), + VNode::Element(el) => { + // + s.debug_struct("VElement") + .field("name", &el.tag_name) + .field("key", &el.key) + .finish() + } VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text), VNode::Anchor(a) => write!(s, "VAnchor"), diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index a4ce42240..35203f608 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -1,7 +1,7 @@ //! Diffing Tests //! ------------- //! -//! These should always compile and run, but the result is not validated for each test. +//! These should always compile and run, but the result is not validat root: (), m: () ed for each test. //! TODO: Validate the results beyond visual inspection. use bumpalo::Bump; @@ -13,6 +13,8 @@ use dioxus::{ use dioxus_core as dioxus; use dioxus_html as dioxus_elements; use futures_util::FutureExt; +mod test_logging; +use DomEdit::*; struct TestDom { bump: Bump, @@ -20,6 +22,7 @@ struct TestDom { } impl TestDom { fn new() -> TestDom { + test_logging::set_up_logging(); let bump = Bump::new(); let resources = SharedResources::new(); TestDom { bump, resources } @@ -106,28 +109,70 @@ fn diffing_works() {} #[test] fn html_and_rsx_generate_the_same_output() { let dom = TestDom::new(); - let edits = dom.lazy_diff( + let (create, change) = dom.lazy_diff( rsx! ( div { "Hello world" } ), rsx! ( div { "Goodbye world" } ), ); - dbg!(edits); + assert_eq!( + create.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateTextNode { + id: 1, + text: "Hello world" + }, + AppendChildren { many: 1 }, + AppendChildren { many: 1 }, + ] + ); + + assert_eq!( + change.edits, + [ + PushRoot { id: 1 }, + SetText { + text: "Goodbye world" + }, + PopRoot + ] + ); } /// Should result in 3 elements on the stack #[test] fn fragments_create_properly() { let dom = TestDom::new(); - let Mutations { edits, noderefs } = dom.create(rsx! { + + let create = dom.create(rsx! { div { "Hello a" } div { "Hello b" } div { "Hello c" } }); - assert!(&edits[0].is("CreateElement")); - assert!(&edits[3].is("CreateElement")); - assert!(&edits[6].is("CreateElement")); - assert_eq!(*edits.last().unwrap(), DomEdit::AppendChildren { many: 3 }); - dbg!(edits); + assert_eq!( + create.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateTextNode { + id: 1, + text: "Hello a" + }, + AppendChildren { many: 1 }, + CreateElement { id: 2, tag: "div" }, + CreateTextNode { + id: 3, + text: "Hello b" + }, + AppendChildren { many: 1 }, + CreateElement { id: 4, tag: "div" }, + CreateTextNode { + id: 5, + text: "Hello c" + }, + AppendChildren { many: 1 }, + AppendChildren { many: 3 }, + ] + ); } /// Should result in the creation of an anchor (placeholder) and then a replacewith @@ -138,8 +183,19 @@ fn empty_fragments_create_anchors() { let left = rsx!({ (0..0).map(|f| rsx! { div {}}) }); let right = rsx!({ (0..1).map(|f| rsx! { div {}}) }); - let edits = dom.lazy_diff(left, right); - dbg!(edits); + let (create, change) = dom.lazy_diff(left, right); + + assert_eq!( + create.edits, + [CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }] + ); + assert_eq!( + change.edits, + [ + CreateElement { id: 1, tag: "div" }, + ReplaceWith { m: 1, root: 0 } + ] + ); } /// Should result in the creation of an anchor (placeholder) and then a replacewith m=5 @@ -150,8 +206,22 @@ fn empty_fragments_create_many_anchors() { let left = rsx!({ (0..0).map(|f| rsx! { div {}}) }); let right = rsx!({ (0..5).map(|f| rsx! { div {}}) }); - let edits = dom.lazy_diff(left, right); - dbg!(edits); + let (create, change) = dom.lazy_diff(left, right); + assert_eq!( + create.edits, + [CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }] + ); + assert_eq!( + change.edits, + [ + CreateElement { id: 1, tag: "div" }, + CreateElement { id: 2, tag: "div" }, + CreateElement { id: 3, tag: "div" }, + CreateElement { id: 4, tag: "div" }, + CreateElement { id: 5, tag: "div" }, + ReplaceWith { m: 5, root: 0 } + ] + ); } /// Should result in the creation of an anchor (placeholder) and then a replacewith @@ -162,15 +232,40 @@ fn empty_fragments_create_anchors_with_many_children() { let left = rsx!({ (0..0).map(|f| rsx! { div {} }) }); let right = rsx!({ - (0..5).map(|f| { - rsx! { div { "hello" }} + (0..3).map(|f| { + rsx! { div { "hello: {f}" }} }) }); - let edits = dom.lazy_diff(left, right); - dbg!(&edits); - let last_edit = edits.1.edits.last().unwrap(); - assert!(last_edit.is("ReplaceWith")); + let (create, change) = dom.lazy_diff(left, right); + assert_eq!( + create.edits, + [CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }] + ); + assert_eq!( + change.edits, + [ + CreateElement { id: 1, tag: "div" }, + CreateTextNode { + text: "hello: 0", + id: 2 + }, + AppendChildren { many: 1 }, + CreateElement { id: 3, tag: "div" }, + CreateTextNode { + text: "hello: 1", + id: 4 + }, + AppendChildren { many: 1 }, + CreateElement { id: 5, tag: "div" }, + CreateTextNode { + text: "hello: 2", + id: 6 + }, + AppendChildren { many: 1 }, + ReplaceWith { m: 3, root: 0 } + ] + ); } /// Should result in every node being pushed and then replaced with an anchor @@ -185,8 +280,35 @@ fn many_items_become_fragment() { }); let right = rsx!({ (0..0).map(|f| rsx! { div {} }) }); - let edits = dom.lazy_diff(left, right); - dbg!(&edits); + let (create, change) = dom.lazy_diff(left, right); + assert_eq!( + create.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateTextNode { + text: "hello", + id: 1 + }, + AppendChildren { many: 1 }, + CreateElement { id: 2, tag: "div" }, + CreateTextNode { + text: "hello", + id: 3 + }, + AppendChildren { many: 1 }, + AppendChildren { many: 2 }, + ] + ); + + // hmmmmmmmmm worried about reusing IDs that we shouldnt be + assert_eq!( + change.edits, + [ + Remove { root: 2 }, + CreatePlaceholder { id: 4 }, + ReplaceWith { root: 0, m: 1 }, + ] + ); } /// Should result in no edits @@ -205,9 +327,8 @@ fn two_equal_fragments_are_equal() { }) }); - let edits = dom.lazy_diff(left, right); - dbg!(&edits); - assert!(edits.1.edits.is_empty()); + let (create, change) = dom.lazy_diff(left, right); + assert!(change.edits.is_empty()); } /// Should result the creation of more nodes appended after the old last node @@ -216,11 +337,11 @@ fn two_fragments_with_differrent_elements_are_differet() { let dom = TestDom::new(); let left = rsx!( - {(0..2).map(|f| {rsx! { div { }}})} + { (0..2).map(|f| rsx! { div { }} ) } p {} ); let right = rsx!( - {(0..5).map(|f| {rsx! { h1 { }}})} + { (0..5).map(|f| rsx! (h1 { }) ) } p {} ); @@ -242,8 +363,31 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() { p {} ); - let edits = dom.lazy_diff(left, right); - dbg!(&edits); + let (create, change) = dom.lazy_diff(left, right); + assert_eq!( + create.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateElement { id: 1, tag: "div" }, + CreateElement { id: 2, tag: "div" }, + CreateElement { id: 3, tag: "div" }, + CreateElement { id: 4, tag: "div" }, + CreateElement { id: 5, tag: "p" }, + AppendChildren { many: 6 }, + ] + ); + assert_eq!( + change.edits, + [ + Remove { root: 2 }, + Remove { root: 3 }, + Remove { root: 4 }, + CreateElement { id: 6, tag: "h1" }, + ReplaceWith { root: 0, m: 1 }, + CreateElement { id: 7, tag: "h1" }, + ReplaceWith { root: 1, m: 1 }, + ] + ); } /// Should result in multiple nodes destroyed - with no changes @@ -260,26 +404,25 @@ fn two_fragments_with_same_elements_are_differet() { p {} ); - let edits = dom.lazy_diff(left, right); - dbg!(&edits); -} - -// Similar test from above, but with extra child nodes -#[test] -fn two_fragments_with_same_elements_are_differet_shorter() { - let dom = TestDom::new(); - - let left = rsx!( - {(0..5).map(|f| {rsx! { div { }}})} - p {"e"} + let (create, change) = dom.lazy_diff(left, right); + assert_eq!( + create.edits, + [ + CreateElement { id: 0, tag: "div" }, + CreateElement { id: 1, tag: "div" }, + CreateElement { id: 2, tag: "p" }, + AppendChildren { many: 3 }, + ] ); - let right = rsx!( - {(0..2).map(|f| {rsx! { div { }}})} - p {"e"} + assert_eq!( + change.edits, + [ + CreateElement { id: 3, tag: "div" }, + CreateElement { id: 4, tag: "div" }, + CreateElement { id: 5, tag: "div" }, + InsertAfter { root: 1, n: 3 }, + ] ); - - let edits = dom.lazy_diff(left, right); - dbg!(&edits); } /// should result in the removal of elements @@ -296,8 +439,11 @@ fn keyed_diffing_order() { p {"e"} ); - let edits = dom.lazy_diff(left, right); - dbg!(&edits); + let (create, change) = dom.lazy_diff(left, right); + assert_eq!( + change.edits, + [Remove { root: 2 }, Remove { root: 3 }, Remove { root: 4 },] + ); } #[test] @@ -313,23 +459,239 @@ fn fragment_keys() { fn keyed_diffing_out_of_order() { let dom = TestDom::new(); - // 0, 1, 2, 3, 4, 5, 6, 7, 8, let left = rsx!({ - (0..3).chain(3..6).chain(6..9).map(|f| { + [0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); - // 0, 1, 2, 6, 5, 4, 3, 7, 8, 9 let right = rsx!({ - (0..3).chain((3..7).rev()).chain(7..10).map(|f| { + [0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); - // LIS: 3, 7, 8, let edits = dom.lazy_diff(left, right); - dbg!(&edits); + dbg!(&edits.1); +} + +/// Should result in moves only +#[test] +fn keyed_diffing_out_of_order_adds() { + let dom = TestDom::new(); + + let left = rsx!({ + [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let right = rsx!({ + [/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let (_, change) = dom.lazy_diff(left, right); + assert_eq!( + change.edits, + [ + PushRoot { id: 4 }, + PushRoot { id: 3 }, + InsertBefore { n: 2, root: 0 } + ] + ); +} +/// Should result in moves onl +#[test] +fn keyed_diffing_out_of_order_adds_2() { + let dom = TestDom::new(); + + let left = rsx!({ + [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let right = rsx!({ + [/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let (_, change) = dom.lazy_diff(left, right); + assert_eq!( + change.edits, + [ + PushRoot { id: 3 }, + PushRoot { id: 4 }, + InsertBefore { n: 2, root: 0 } + ] + ); +} + +/// Should result in moves onl +#[test] +fn keyed_diffing_out_of_order_adds_3() { + let dom = TestDom::new(); + + let left = rsx!({ + [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let right = rsx!({ + [/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let (_, change) = dom.lazy_diff(left, right); + assert_eq!( + change.edits, + [ + PushRoot { id: 4 }, + PushRoot { id: 3 }, + InsertBefore { n: 2, root: 1 } + ] + ); +} + +/// Should result in moves onl +#[test] +fn keyed_diffing_out_of_order_adds_4() { + let dom = TestDom::new(); + + let left = rsx!({ + [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let right = rsx!({ + [/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let (_, change) = dom.lazy_diff(left, right); + assert_eq!( + change.edits, + [ + PushRoot { id: 4 }, + PushRoot { id: 3 }, + InsertBefore { n: 2, root: 2 } + ] + ); +} + +/// Should result in moves onl +#[test] +fn keyed_diffing_out_of_order_adds_5() { + let dom = TestDom::new(); + + let left = rsx!({ + [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let right = rsx!({ + [/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let (_, change) = dom.lazy_diff(left, right); + assert_eq!( + change.edits, + [PushRoot { id: 4 }, InsertBefore { n: 1, root: 3 }] + ); +} + +#[test] +fn keyed_diffing_additions() { + let dom = TestDom::new(); + + let left = rsx!({ + [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let right = rsx!({ + [/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let (_, change) = dom.lazy_diff(left, right); + assert_eq!( + change.edits, + [ + CreateElement { id: 5, tag: "div" }, + CreateElement { id: 6, tag: "div" }, + InsertAfter { n: 2, root: 4 } + ] + ); +} + +#[test] +fn keyed_diffing_additions_and_moves_on_ends() { + let dom = TestDom::new(); + + let left = rsx!({ + [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let right = rsx!({ + [/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| { + // [/**/ 8, 7, 4, 5, 6, 9, 10 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let (_, change) = dom.lazy_diff(left, right); + dbg!(change); + // assert_eq!( + // change.edits, + // [ + // CreateElement { id: 5, tag: "div" }, + // CreateElement { id: 6, tag: "div" }, + // InsertAfter { n: 2, root: 4 } + // ] + // ); +} + +#[test] +fn keyed_diffing_additions_and_moves_in_middle() { + let dom = TestDom::new(); + + let left = rsx!({ + [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let right = rsx!({ + [/**/ 7, 4, 13, 17, 5, 11, 12, 6 /**/].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let (_, change) = dom.lazy_diff(left, right); + dbg!(change); + // assert_eq!( + // change.edits, + // [ + // CreateElement { id: 5, tag: "div" }, + // CreateElement { id: 6, tag: "div" }, + // InsertAfter { n: 2, root: 4 } + // ] + // ); } #[test] From a0a977a12d4008155fe332ee226f04e5f59acf95 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 24 Aug 2021 11:03:19 -0400 Subject: [PATCH 23/53] wip: more test --- packages/core/src/diff.rs | 159 ++++++-------------------------- packages/core/src/diff_stack.rs | 2 - packages/core/tests/diffing.rs | 78 +++++++++++++--- 3 files changed, 91 insertions(+), 148 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index db5d9930b..539c6986f 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -6,8 +6,8 @@ //! ## Notice: //! //! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support -//! Components, Fragments, Suspense, SubTree memoization, incremental diffing, cancelation, NodeRefs, and additional -//! batching operations. +//! Components, Fragments, Suspense, SubTree memoization, incremental diffing, cancelation, NodeRefs, pausing, priority +//! scheduling, and additional batching operations. //! //! ## Implementation Details: //! @@ -41,12 +41,13 @@ //! 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. +//! Due to the frequent calls to "yield_now" we can get the pure "fetch-as-you-render" behavior of React fiber. //! //! 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. +//! and contents, without the need for the "use_suspense" hook. In the interim, this is the quickest way to get suspense working. //! //! ## Subtree Memoization //! ----------------------- @@ -80,17 +81,16 @@ //! ## Further Reading and Thoughts //! ---------------------------- //! There are more ways of increasing diff performance here that are currently not implemented. +//! - Strong memoization of subtrees. +//! - Guided diffing. +//! - Certain web-dom-specific optimizations. +//! //! More info on how to improve this diffing algorithm: //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ use crate::{arena::SharedResources, innerlude::*}; use futures_util::{Future, FutureExt}; -use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; -use smallvec::{smallvec, SmallVec}; - -use std::{ - any::Any, cell::Cell, cmp::Ordering, collections::HashSet, marker::PhantomData, pin::Pin, -}; +use fxhash::{FxHashMap, FxHashSet}; use DomEdit::*; /// Our DiffMachine is an iterative tree differ. @@ -158,8 +158,6 @@ impl<'bump> DiffMachine<'bump> { // 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.stack.pop() { - log::debug!("Handling diff instruction: {:?}", instruction); - // todo: call this less frequently, there is a bit of overhead involved yield_now().await; @@ -168,11 +166,6 @@ impl<'bump> DiffMachine<'bump> { self.stack.pop_scope(); } - DiffInstruction::PopElement => { - todo!(); - // self.mutations.pop(); - } - DiffInstruction::DiffNode { old, new, .. } => { self.diff_node(old, new); } @@ -229,7 +222,7 @@ impl<'bump> DiffMachine<'bump> { } MountType::InsertAfter { other_node } => { - let root = self.find_last_element(other_node).direct_id(); + let root = self.find_last_element(other_node).unwrap(); self.mutations.insert_after(root, nodes_created as u32); } @@ -862,8 +855,6 @@ impl<'bump> DiffMachine<'bump> { } // 4. Compute the LIS of this list - // TODO: investigate if we can go without the hashset here, I can't quite tell from the LIS crate - // let mut new_index_is_in_lis = Vec::default(); let mut lis_sequence = Vec::default(); lis_sequence.reserve(new_index_to_old_index.len()); @@ -878,7 +869,7 @@ impl<'bump> DiffMachine<'bump> { &mut starts, ); - // the lis comes out backwards, I think. can't quite tell + // 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) @@ -886,13 +877,8 @@ impl<'bump> DiffMachine<'bump> { lis_sequence.pop(); } - log::debug!("LIS Indicies: {:?}", lis_sequence); - let apply = |new_idx, new_node: &'bump VNode<'bump>, stack: &mut DiffStack<'bump>| { - log::debug!("applying {:?}", new_node); - let old_index = new_index_to_old_index[new_idx]; - dbg!(new_idx, old_index); if old_index == u32::MAX as usize { stack.create_node(new_node, MountType::Absorb); } else { @@ -907,7 +893,6 @@ impl<'bump> DiffMachine<'bump> { // add mount instruction for the last items not covered by the lis let first_lis = *lis_sequence.first().unwrap(); - dbg!(first_lis); if first_lis > 0 { self.stack.push_nodes_created(0); self.stack.push(DiffInstruction::Mount { @@ -940,28 +925,23 @@ impl<'bump> DiffMachine<'bump> { } // add mount instruction for the first items not covered by the lis - let last_lis = *lis_sequence.last().unwrap(); - log::debug!("last lis is {}, new len is {}", last_lis, new.len()); - if last_lis < (new.len() - 1) { - let after = &new[last_lis]; - dbg!(after); + let last = *lis_sequence.last().unwrap(); + if last < (new.len() - 1) { self.stack.push_nodes_created(0); self.stack.push(DiffInstruction::Mount { - and: MountType::InsertAfter { other_node: after }, + and: MountType::InsertAfter { + other_node: &new[last], + }, }); - - for (idx, new_node) in new[(last_lis + 1)..].iter().enumerate().rev() { - apply(idx + last_lis + 1, new_node, &mut self.stack); + for (idx, new_node) in new[(last + 1)..].iter().enumerate().rev() { + apply(idx + last + 1, new_node, &mut self.stack); } } for idx in lis_sequence.iter().rev() { - let old_index = new_index_to_old_index[*idx]; - let new_node = &new[*idx]; - log::debug!("diffing {:?}", new_node); self.stack.push(DiffInstruction::DiffNode { - new: new_node, - old: &old[old_index], + new: &new[*idx], + old: &old[new_index_to_old_index[*idx]], }); } } @@ -970,16 +950,15 @@ impl<'bump> DiffMachine<'bump> { // Utilities // ===================== - fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { + fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> Option { let mut search_node = Some(vnode); loop { - let node = search_node.take().unwrap(); - match &node { - // the ones that have a direct id - VNode::Text(_) | VNode::Element(_) | VNode::Anchor(_) | VNode::Suspended(_) => { - break node - } + match &search_node.take().unwrap() { + VNode::Text(t) => break t.dom_id.get(), + VNode::Element(t) => break t.dom_id.get(), + VNode::Suspended(t) => break t.node.get(), + VNode::Anchor(t) => break t.dom_id.get(), VNode::Fragment(frag) => { search_node = frag.children.last(); @@ -997,8 +976,7 @@ impl<'bump> DiffMachine<'bump> { let mut search_node = Some(vnode); loop { - let node = search_node.take().unwrap(); - match &node { + match &search_node.take().unwrap() { // the ones that have a direct id VNode::Fragment(frag) => { search_node = Some(&frag.children[0]); @@ -1015,28 +993,6 @@ impl<'bump> DiffMachine<'bump> { } } } - // fn find_first_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { - // let mut search_node = Some(vnode); - - // loop { - // let node = search_node.take().unwrap(); - // match &node { - // // the ones that have a direct id - // VNode::Text(_) | VNode::Element(_) | VNode::Anchor(_) | VNode::Suspended(_) => { - // break node - // } - - // VNode::Fragment(frag) => { - // search_node = Some(&frag.children[0]); - // } - // VNode::Component(el) => { - // let scope_id = el.ass_scope.get().unwrap(); - // let scope = self.vdom.get_scope(scope_id).unwrap(); - // search_node = Some(scope.root()); - // } - // } - // } - // } fn replace_and_create_many_with_one( &mut self, @@ -1082,6 +1038,7 @@ impl<'bump> DiffMachine<'bump> { VNode::Fragment(f) => { self.remove_nodes(f.children); } + VNode::Component(c) => { let scope_id = c.ass_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); @@ -1090,66 +1047,6 @@ impl<'bump> DiffMachine<'bump> { } } } - - // let mut nodes_to_replace = Vec::new(); - // let mut nodes_to_search = vec![old_node]; - // let mut scopes_obliterated = Vec::new(); - // while let Some(node) = nodes_to_search.pop() { - // match &node { - // // the ones that have a direct id return immediately - // VNode::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - // VNode::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - // VNode::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - // VNode::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), - - // // Fragments will either have a single anchor or a list of children - // VNode::Fragment(frag) => { - // for child in frag.children { - // nodes_to_search.push(child); - // } - // } - - // // Components can be any of the nodes above - // // However, we do need to track which components need to be removed - // VNode::Component(el) => { - // let scope_id = el.ass_scope.get().unwrap(); - // let scope = self.vdom.get_scope(scope_id).unwrap(); - // let root = scope.root(); - // nodes_to_search.push(root); - // scopes_obliterated.push(scope_id); - // } - // } - // // TODO: enable internal garabge collection - // // self.create_garbage(node); - // } - - // let n = nodes_to_replace.len(); - // for node in nodes_to_replace { - // self.mutations.push_root(node); - // } - - // let mut nodes_created = 0; - // for node in new_nodes { - // todo!(); - // let meta = self.create_vnode(node); - // nodes_created += meta.added_to_stack; - // } - - // if 0 nodes are created, then it gets interperted as a deletion - // self.mutations.replace_with(n as u32, nodes_created); - - // self.instructions.push(DiffInstruction::CreateChildren { - // and: MountType::Replace { old: None }, - // children: - // }); - - // obliterate! - // for scope in scopes_obliterated { - - // todo: mark as garbage - - // self.destroy_scopes(scope); - // } } /// Remove all the old nodes and replace them with newly created new nodes. diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index 9819412d8..0150d8d51 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -27,8 +27,6 @@ pub enum DiffInstruction<'a> { and: MountType<'a>, }, - PopElement, - PopScope, } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 35203f608..b1511c926 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -16,13 +16,19 @@ use futures_util::FutureExt; mod test_logging; use DomEdit::*; +// logging is wired up to the test harness +// feel free to enable while debugging +const IS_LOGGING_ENABLED: bool = true; + struct TestDom { bump: Bump, resources: SharedResources, } impl TestDom { fn new() -> TestDom { - test_logging::set_up_logging(); + if IS_LOGGING_ENABLED { + test_logging::set_up_logging(); + } let bump = Bump::new(); let resources = SharedResources::new(); TestDom { bump, resources } @@ -346,7 +352,7 @@ fn two_fragments_with_differrent_elements_are_differet() { ); let edits = dom.lazy_diff(left, right); - dbg!(&edits); + log::debug!("{:#?}", &edits); } /// Should result in multiple nodes destroyed - with changes to the first nodes @@ -472,7 +478,7 @@ fn keyed_diffing_out_of_order() { }); let edits = dom.lazy_diff(left, right); - dbg!(&edits.1); + log::debug!("{:?}", &edits.1); } /// Should result in moves only @@ -655,7 +661,7 @@ fn keyed_diffing_additions_and_moves_on_ends() { }); let (_, change) = dom.lazy_diff(left, right); - dbg!(change); + log::debug!("{:?}", change); // assert_eq!( // change.edits, // [ @@ -683,7 +689,7 @@ fn keyed_diffing_additions_and_moves_in_middle() { }); let (_, change) = dom.lazy_diff(left, right); - dbg!(change); + log::debug!("{:?}", change); // assert_eq!( // change.edits, // [ @@ -698,22 +704,64 @@ fn keyed_diffing_additions_and_moves_in_middle() { fn controlled_keyed_diffing_out_of_order() { let dom = TestDom::new(); - let left = [4, 5, 6, 7]; let left = rsx!({ - left.iter().map(|f| { - rsx! { div { key: "{f}" "{f}" }} + [4, 5, 6, 7].iter().map(|f| { + rsx! { div { key: "{f}" }} }) }); - // 0, 1, 2, 6, 5, 4, 3, 7, 8, 9 - let right = [0, 5, 9, 6, 4]; let right = rsx!({ - right.iter().map(|f| { - rsx! { div { key: "{f}" "{f}" }} + [0, 5, 9, 6, 4].iter().map(|f| { + rsx! { div { key: "{f}" }} }) }); - // LIS: 3, 7, 8, - let edits = dom.lazy_diff(left, right); - dbg!(&edits); + // LIS: 5, 6 + let (_, changes) = dom.lazy_diff(left, right); + log::debug!("{:#?}", &changes); + assert_eq!( + changes.edits, + [ + // move 4 to after 6 + PushRoot { id: 0 }, + InsertAfter { n: 1, root: 2 }, + // remove 7 + + // create 9 and insert before 6 + CreateElement { id: 4, tag: "div" }, + InsertBefore { n: 1, root: 2 }, + // create 0 and insert before 5 + CreateElement { id: 5, tag: "div" }, + InsertBefore { n: 1, root: 1 }, + ] + ); +} + +#[test] +fn controlled_keyed_diffing_out_of_order_max_test() { + let dom = TestDom::new(); + + let left = rsx!({ + [0, 1, 2, 3, 4].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let right = rsx!({ + [3, 0, 1, 10, 2].iter().map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + let (_, changes) = dom.lazy_diff(left, right); + log::debug!("{:#?}", &changes); + assert_eq!( + changes.edits, + [ + CreateElement { id: 5, tag: "div" }, + InsertBefore { n: 1, root: 2 }, + PushRoot { id: 3 }, + InsertBefore { n: 1, root: 0 }, + ] + ); } From cdcd8611e87ffb5e24de7b9fe6c656af3053276e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 24 Aug 2021 12:43:46 -0400 Subject: [PATCH 24/53] wip: it works but the page is backwards --- packages/core/benches/create.rs | 17 +-- packages/core/benches/jsframework.rs | 15 +- packages/core/examples/alternative.rs | 9 +- packages/core/examples/fragment.rs | 18 --- packages/core/examples/fragment_from_iter.rs | 30 ++-- packages/core/examples/macrosrc.rs | 137 ------------------- packages/core/src/diff.rs | 40 +++--- packages/core/src/scope.rs | 5 +- packages/core/tests/create_iterative.rs | 14 +- packages/core/tests/diff_iterative.rs | 4 +- packages/core/tests/diffing.rs | 100 ++++++++------ packages/core/tests/test_logging.rs | 6 +- packages/html/src/{attrval.s => attrval.rs} | 3 + packages/ssr/examples/basic.rs | 25 ++++ packages/ssr/examples/tofile.rs | 2 +- packages/ssr/src/lib.rs | 16 +-- packages/web/examples/basic.rs | 2 +- packages/web/src/dom.rs | 40 +++--- packages/web/src/lib.rs | 105 ++++++++------ 19 files changed, 239 insertions(+), 349 deletions(-) delete mode 100644 packages/core/examples/fragment.rs delete mode 100644 packages/core/examples/macrosrc.rs rename packages/html/src/{attrval.s => attrval.rs} (86%) create mode 100644 packages/ssr/examples/basic.rs diff --git a/packages/core/benches/create.rs b/packages/core/benches/create.rs index 2f614ac0e..f328e4d9d 100644 --- a/packages/core/benches/create.rs +++ b/packages/core/benches/create.rs @@ -1,16 +1 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; - -fn fibonacci(n: u64) -> u64 { - match n { - 0 => 1, - 1 => 1, - n => fibonacci(n - 1) + fibonacci(n - 2), - } -} - -fn criterion_benchmark(c: &mut Criterion) { - c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20)))); -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); +fn main() {} diff --git a/packages/core/benches/jsframework.rs b/packages/core/benches/jsframework.rs index 91b97dd74..b9dff6bb6 100644 --- a/packages/core/benches/jsframework.rs +++ b/packages/core/benches/jsframework.rs @@ -1,3 +1,4 @@ +#![allow(non_snake_case, non_upper_case_globals)] //! This benchmark tests just the overhead of Dioxus itself. //! //! For the JS Framework Benchmark, both the framework and the browser is benchmarked together. Dioxus prepares changes @@ -8,12 +9,10 @@ //! - Dioxus takes 3ms to create 1_000 rows //! - Dioxus takes 30ms to create 10_000 rows //! -//! As pure "overhead", these are really really good numbers, mostly slowed down by hitting the global allocator. +//! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator. //! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster. -use std::fmt::Display; - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_html as dioxus_elements; @@ -58,11 +57,12 @@ struct RowProps { label: Label, } fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree { + let [adj, col, noun] = cx.label.0; cx.render(rsx! { tr { td { class:"col-md-1", "{cx.row_id}" } td { class:"col-md-1", onclick: move |_| { /* run onselect */ } - a { class: "lbl", "{cx.label}" } + a { class: "lbl", "{adj}" "{col}" "{noun}" } } td { class: "col-md-1" a { class: "remove", onclick: move |_| {/* remove */} @@ -86,11 +86,6 @@ impl Label { ]) } } -impl Display for Label { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} {} {}", self.0[0], self.0[1], self.0[2]) - } -} static ADJECTIVES: &[&str] = &[ "pretty", diff --git a/packages/core/examples/alternative.rs b/packages/core/examples/alternative.rs index fc30beb62..177eca78f 100644 --- a/packages/core/examples/alternative.rs +++ b/packages/core/examples/alternative.rs @@ -6,17 +6,16 @@ pub static Example: FC<()> = |cx| { let list = (0..10).map(|f| LazyNodes::new(move |f| todo!())); cx.render(LazyNodes::new(move |cx| { - let bump = cx.bump(); cx.raw_element( "div", None, - &mut [], - &mut [], - cx.bump().alloc([ + [], + [], + [ cx.text(format_args!("hello")), cx.text(format_args!("hello")), cx.fragment_from_iter(list), - ]), + ], None, ) })) diff --git a/packages/core/examples/fragment.rs b/packages/core/examples/fragment.rs deleted file mode 100644 index 3d4b97b8b..000000000 --- a/packages/core/examples/fragment.rs +++ /dev/null @@ -1,18 +0,0 @@ - - -fn main() { - - // let g = rsx! { - // Fragment { - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // } - // }; -} diff --git a/packages/core/examples/fragment_from_iter.rs b/packages/core/examples/fragment_from_iter.rs index 08018c68c..58c4df70f 100644 --- a/packages/core/examples/fragment_from_iter.rs +++ b/packages/core/examples/fragment_from_iter.rs @@ -1,31 +1,27 @@ -fn main() {} - use dioxus_core::prelude::*; -fn App(cx: Context<()>) -> DomTree { - // +fn main() {} + +fn app(cx: Context<()>) -> DomTree { let vak = use_suspense( cx, || async {}, - |c, res| { - // - c.render(LazyNodes::new(move |f| f.text(format_args!("")))) - }, + |c, _res| c.render(LazyNodes::new(move |f| f.text(format_args!("")))), ); let d1 = cx.render(LazyNodes::new(move |f| { f.raw_element( "div", None, - &mut [], - &[], - f.bump().alloc([ + [], + [], + [ f.fragment_from_iter(vak), f.text(format_args!("")), f.text(format_args!("")), f.text(format_args!("")), f.text(format_args!("")), - ]), + ], None, ) })); @@ -34,15 +30,15 @@ fn App(cx: Context<()>) -> DomTree { f.raw_element( "div", None, - &mut [], - &[], - f.bump().alloc([ + [], + [], + [ f.text(format_args!("")), f.text(format_args!("")), f.text(format_args!("")), f.text(format_args!("")), - f.fragment_from_iter(d1), - ]), + d1.unwrap(), + ], None, ) })) diff --git a/packages/core/examples/macrosrc.rs b/packages/core/examples/macrosrc.rs deleted file mode 100644 index 2a8dfe4cd..000000000 --- a/packages/core/examples/macrosrc.rs +++ /dev/null @@ -1,137 +0,0 @@ -// #![allow(unused, non_upper_case_globals, non_snake_case)] -// use bumpalo::Bump; -// use dioxus_core::nodebuilder::*; -// use dioxus_core::prelude::*; -// use std::{collections::HashMap, future::Future, marker::PhantomData}; - -fn main() {} -// fn main() { -// let mut vdom = VirtualDom::new_with_props( -// component, -// Props { -// blah: false, -// text: "blah".into(), -// }, -// ); - -// vdom.progress(); - -// let somet = String::from("asd"); -// let text = somet.as_str(); - -// /* -// this could be auto-generated via the macro -// this props is allocated in this -// but the component and props would like need to be cached -// we could box this fn, abstracting away the props requirement and just keep the entrance and allocator requirement -// How do we keep cached things around? -// Need some sort of caching mechanism - -// how do we enter into a childscope from a parent scope? - -// Problems: -// 1: Comp props need to be stored somewhere so we can re-evalute components when they receive updates -// 2: Trees are not evaluated - -// */ -// let example_caller = move |cx: &Bump| { -// todo!() -// // let p = Props { blah: true, text }; -// // let c = Context { props: &p }; -// // let r = component(&c); -// }; - -// // check the edit list -// } - -// // ~~~ Text shared between components via props can be done with lifetimes! ~~~ -// // Super duper efficient :) -// struct Props { -// blah: bool, -// text: String, -// // text: &'src str, -// } -// impl Properties for Props { -// fn new() -> Self { -// todo!() -// } -// } - -// fn component<'a>(cx: Context<'a, Props>) -> DomTree<'a> { -// // Write asynchronous rendering code that immediately returns a "suspended" VNode -// // The concurrent API will then progress this component when the future finishes -// // You can suspend the entire component, or just parts of it -// let product_list = cx.suspend(async { -// // Suspend the rendering that completes when the future is done -// match fetch_data().await { -// Ok(data) => html! {

"success!"
}, -// Err(_) => html! {
"failure :("
}, -// } -// }); - -// // todo!() -// cx.render(html! { -//
-//

"Products"

-// -// // Subnodes can even be suspended -// // When completely rendered, they won't cause the component itself to re-render, just their slot - -// //

{ product_list }

-//
-// }) -// } - -// fn BuilderComp<'a>(cx: Context<'a, Props>) -> DomTree<'a> { -// // VNodes can be constructed via a builder or the html! macro -// // However, both of these are "lazy" - they need to be evaluated (aka, "viewed") -// // We can "view" them with Context for ultimate speed while inside components -// cx.render(|bump| { -// div(bump) -// .attr("class", "edit") -// .child(text("Hello")) -// .child(text(cx.cx.text.as_ref())) -// .finish() -// }) -// } - -// #[fc] -// fn EffcComp(cx: Context, name: &str) -> DomTree { -// // VNodes can be constructed via a builder or the html! macro -// // However, both of these are "lazy" - they need to be evaluated (aka, "viewed") -// // We can "view" them with Context for ultimate speed while inside components -// // use "phase" style allocation; - -// cx.render(html! { -//
-// // your template goes here -// // feel free to directly use "name" -//
-// }) -// } - -// fn FullySuspended<'a>(cx: &'a Context) -> DomTree<'a> { -// cx.suspend(async { -// let i: i32 = 0; - -// // full suspended works great with just returning VNodes! -// // Feel free to capture the html! macro directly -// // Anything returned here is automatically viewed -// let tex = match i { -// 1 => html! {
}, -// 2 => html! {
}, -// _ => html! {
}, -// }; - -// if cx.cx.blah { -// html! {
} -// } else { -// tex -// } -// }) -// } - -// /// An example of a datafetching service -// async fn fetch_data() -> Result { -// todo!() -// } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 539c6986f..0702830a3 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -89,7 +89,6 @@ //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ use crate::{arena::SharedResources, innerlude::*}; -use futures_util::{Future, FutureExt}; use fxhash::{FxHashMap, FxHashSet}; use DomEdit::*; @@ -281,19 +280,20 @@ impl<'bump> DiffMachine<'bump> { } = element; let real_id = self.vdom.reserve_node(); + dom_id.set(Some(real_id)); + self.mutations.create_element(tag_name, *namespace, real_id); self.stack.add_child_count(1); - dom_id.set(Some(real_id)); - - let cur_scope = self.stack.current_scope().unwrap(); + let cur_scope_id = self.stack.current_scope().unwrap(); + let scope = self.vdom.get_scope(cur_scope_id).unwrap(); listeners.iter().for_each(|listener| { - self.fix_listener(listener); + self.attach_listener_to_scope(listener, scope); listener.mounted_node.set(Some(real_id)); self.mutations - .new_event_listener(listener, cur_scope.clone()); + .new_event_listener(listener, cur_scope_id.clone()); }); for attr in *attributes { @@ -466,16 +466,19 @@ impl<'bump> DiffMachine<'bump> { // 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 - let cur_scope = self.stack.current_scope().unwrap(); + + let cur_scope_id = self.stack.current_scope().unwrap(); + let scope = self.vdom.get_scope(cur_scope_id).unwrap(); + 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 { please_commit(&mut self.mutations.edits); self.mutations.remove_event_listener(old_l.event); - self.mutations.new_event_listener(new_l, cur_scope); + self.mutations.new_event_listener(new_l, cur_scope_id); } new_l.mounted_node.set(old_l.mounted_node.get()); - self.fix_listener(new_l); + self.attach_listener_to_scope(new_l, scope); } } else { please_commit(&mut self.mutations.edits); @@ -484,10 +487,8 @@ impl<'bump> DiffMachine<'bump> { } for listener in new.listeners { listener.mounted_node.set(Some(root)); - self.mutations.new_event_listener(listener, cur_scope); - - // Make sure the listener gets attached to the scope list - self.fix_listener(listener); + self.mutations.new_event_listener(listener, cur_scope_id); + self.attach_listener_to_scope(listener, scope); } } @@ -1082,13 +1083,10 @@ impl<'bump> DiffMachine<'bump> { } } - fn fix_listener<'a>(&mut self, listener: &'a Listener<'a>) { - let scope_id = self.stack.current_scope(); - if let Some(scope_id) = scope_id { - let scope = self.vdom.get_scope(scope_id).unwrap(); - let mut queue = scope.listeners.borrow_mut(); - let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; - queue.push(long_listener as *const _) - } + /// Adds a listener closure to a scope during diff. + fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &Scope) { + let mut queue = scope.listeners.borrow_mut(); + let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; + queue.push(long_listener as *const _) } } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 00b4f251f..61945156a 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -113,7 +113,6 @@ impl Scope { child_nodes: ScopeChildren, ) { self.caller = caller; - // let child_nodes = unsafe { std::mem::transmute(child_nodes) }; let child_nodes = unsafe { child_nodes.extend_lifetime() }; self.child_nodes = child_nodes; } @@ -237,8 +236,8 @@ impl Scope { } pub fn consume_garbage(&self) -> Vec<&VNode> { - let mut garbage = self.pending_garbage.borrow_mut(); - garbage + self.pending_garbage + .borrow_mut() .drain(..) .map(|node| { // safety: scopes cannot cycle without their garbage being collected. these nodes are safe diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_iterative.rs index 519e97141..cbd4068a0 100644 --- a/packages/core/tests/create_iterative.rs +++ b/packages/core/tests/create_iterative.rs @@ -7,6 +7,8 @@ use dioxus_core as dioxus; use dioxus_html as dioxus_elements; use DomEdit::*; +const LOGGING_ENABLED: bool = false; + #[test] fn test_original_diff() { static App: FC<()> = |cx| { @@ -57,7 +59,7 @@ async fn create() { }) }; - test_logging::set_up_logging(); + test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); assert_eq!( @@ -98,7 +100,7 @@ async fn create_list() { }) }; - test_logging::set_up_logging(); + test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); @@ -141,7 +143,7 @@ async fn create_simple() { }) }; - test_logging::set_up_logging(); + test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); @@ -177,7 +179,7 @@ async fn create_components() { }) }; - test_logging::set_up_logging(); + test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); @@ -223,7 +225,7 @@ async fn anchors() { }) }; - test_logging::set_up_logging(); + test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); @@ -249,7 +251,7 @@ async fn suspended() { cx.render(rsx! { {val} }) }; - test_logging::set_up_logging(); + test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); let mutations = dom.rebuild_async().await.unwrap(); diff --git a/packages/core/tests/diff_iterative.rs b/packages/core/tests/diff_iterative.rs index f5fba7852..be1c02992 100644 --- a/packages/core/tests/diff_iterative.rs +++ b/packages/core/tests/diff_iterative.rs @@ -6,6 +6,8 @@ mod test_logging; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; +const LOGGING_ENABLED: bool = false; + #[async_std::test] async fn test_iterative_create_components() { static App: FC<()> = |cx| { @@ -34,7 +36,7 @@ async fn test_iterative_create_components() { }) } - test_logging::set_up_logging(); + test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index b1511c926..bd71bc6d7 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -1,12 +1,7 @@ //! Diffing Tests -//! ------------- -//! -//! These should always compile and run, but the result is not validat root: (), m: () ed for each test. -//! TODO: Validate the results beyond visual inspection. use bumpalo::Bump; -use anyhow::{Context, Result}; use dioxus::{ arena::SharedResources, diff::DiffMachine, prelude::*, DiffInstruction, DomEdit, MountType, }; @@ -18,7 +13,7 @@ use DomEdit::*; // logging is wired up to the test harness // feel free to enable while debugging -const IS_LOGGING_ENABLED: bool = true; +const IS_LOGGING_ENABLED: bool = false; struct TestDom { bump: Bump, @@ -26,9 +21,7 @@ struct TestDom { } impl TestDom { fn new() -> TestDom { - if IS_LOGGING_ENABLED { - test_logging::set_up_logging(); - } + test_logging::set_up_logging(IS_LOGGING_ENABLED); let bump = Bump::new(); let resources = SharedResources::new(); TestDom { bump, resources } @@ -82,8 +75,6 @@ impl TestDom { let new = self.bump.alloc(self.render(right)); - // let mut create_edits = Vec::new(); - let mut machine = DiffMachine::new_headless(&self.resources); machine.stack.create_node(old, MountType::Append); @@ -343,16 +334,31 @@ fn two_fragments_with_differrent_elements_are_differet() { let dom = TestDom::new(); let left = rsx!( - { (0..2).map(|f| rsx! { div { }} ) } + { (0..2).map(|_| rsx! { div { }} ) } p {} ); let right = rsx!( - { (0..5).map(|f| rsx! (h1 { }) ) } + { (0..5).map(|_| rsx! (h1 { }) ) } p {} ); - let edits = dom.lazy_diff(left, right); - log::debug!("{:#?}", &edits); + let (create, changes) = dom.lazy_diff(left, right); + log::debug!("{:#?}", &changes); + assert_eq!( + changes.edits, + [ + // create the new h1s + CreateElement { tag: "h1", id: 3 }, + CreateElement { tag: "h1", id: 4 }, + CreateElement { tag: "h1", id: 5 }, + InsertAfter { root: 1, n: 3 }, + // replace the divs with new h1s + CreateElement { tag: "h1", id: 6 }, + ReplaceWith { root: 0, m: 1 }, + CreateElement { tag: "h1", id: 7 }, + ReplaceWith { root: 1, m: 1 }, + ] + ); } /// Should result in multiple nodes destroyed - with changes to the first nodes @@ -452,14 +458,6 @@ fn keyed_diffing_order() { ); } -#[test] -fn fragment_keys() { - let r = 1; - let p = rsx! { - Fragment { key: "asd {r}" } - }; -} - /// Should result in moves, but not removals or additions #[test] fn keyed_diffing_out_of_order() { @@ -477,8 +475,12 @@ fn keyed_diffing_out_of_order() { }) }); - let edits = dom.lazy_diff(left, right); - log::debug!("{:?}", &edits.1); + let (_, changes) = dom.lazy_diff(left, right); + log::debug!("{:?}", &changes); + assert_eq!( + changes.edits, + [PushRoot { id: 6 }, InsertBefore { root: 4, n: 1 }] + ); } /// Should result in moves only @@ -655,21 +657,24 @@ fn keyed_diffing_additions_and_moves_on_ends() { let right = rsx!({ [/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| { - // [/**/ 8, 7, 4, 5, 6, 9, 10 /**/].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); let (_, change) = dom.lazy_diff(left, right); log::debug!("{:?}", change); - // assert_eq!( - // change.edits, - // [ - // CreateElement { id: 5, tag: "div" }, - // CreateElement { id: 6, tag: "div" }, - // InsertAfter { n: 2, root: 4 } - // ] - // ); + assert_eq!( + change.edits, + [ + // create 11, 12 + CreateElement { tag: "div", id: 4 }, + CreateElement { tag: "div", id: 5 }, + InsertAfter { root: 2, n: 2 }, + // move 7 to the front + PushRoot { id: 3 }, + InsertBefore { root: 0, n: 1 } + ] + ); } #[test] @@ -688,16 +693,25 @@ fn keyed_diffing_additions_and_moves_in_middle() { }) }); + // LIS: 4, 5, 6 let (_, change) = dom.lazy_diff(left, right); - log::debug!("{:?}", change); - // assert_eq!( - // change.edits, - // [ - // CreateElement { id: 5, tag: "div" }, - // CreateElement { id: 6, tag: "div" }, - // InsertAfter { n: 2, root: 4 } - // ] - // ); + log::debug!("{:#?}", change); + assert_eq!( + change.edits, + [ + // create 13, 17 + CreateElement { tag: "div", id: 4 }, + CreateElement { tag: "div", id: 5 }, + InsertBefore { root: 1, n: 2 }, + // create 11, 12 + CreateElement { tag: "div", id: 6 }, + CreateElement { tag: "div", id: 7 }, + InsertBefore { root: 2, n: 2 }, + // move 7 + PushRoot { id: 3 }, + InsertBefore { root: 0, n: 1 } + ] + ); } #[test] diff --git a/packages/core/tests/test_logging.rs b/packages/core/tests/test_logging.rs index 41498a9e9..aafb831e2 100644 --- a/packages/core/tests/test_logging.rs +++ b/packages/core/tests/test_logging.rs @@ -1,7 +1,11 @@ -pub fn set_up_logging() { +pub fn set_up_logging(enabled: bool) { use fern::colors::{Color, ColoredLevelConfig}; use log::debug; + if !enabled { + return; + } + // configure colors for the whole line let colors_line = ColoredLevelConfig::new() .error(Color::Red) diff --git a/packages/html/src/attrval.s b/packages/html/src/attrval.rs similarity index 86% rename from packages/html/src/attrval.s rename to packages/html/src/attrval.rs index cb081f4cb..9ac1f1373 100644 --- a/packages/html/src/attrval.s +++ b/packages/html/src/attrval.rs @@ -1,3 +1,6 @@ +//! This module is not included anywhere. +//! +//! It is a prototype for a system that supports non-string attribute values. trait AsAttributeValue: Sized { fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a>; diff --git a/packages/ssr/examples/basic.rs b/packages/ssr/examples/basic.rs new file mode 100644 index 000000000..b3bfb6ca5 --- /dev/null +++ b/packages/ssr/examples/basic.rs @@ -0,0 +1,25 @@ +use dioxus::virtual_dom::VirtualDom; +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_html as dioxus_elements; + +fn main() { + let mut dom = VirtualDom::new(App); + dom.rebuild().unwrap(); + println!( + "{}", + dioxus_ssr::render_vdom(&dom, |c| c.newline(true).indent(true)) + ) +} + +pub static App: FC<()> = |cx| { + cx.render(rsx!( + div { + class: "overflow-hidden" + ul { + {(0..10).map(|i| rsx!{ li { class: "flex flex-col", "entry: {i}"}})} + } + "hello world!" + } + )) +}; diff --git a/packages/ssr/examples/tofile.rs b/packages/ssr/examples/tofile.rs index 35ce6db89..09b710486 100644 --- a/packages/ssr/examples/tofile.rs +++ b/packages/ssr/examples/tofile.rs @@ -13,7 +13,7 @@ fn main() { let mut file = File::create("example.html").unwrap(); let mut dom = VirtualDom::new(App); - dom.rebuild_in_place().expect("failed to run virtualdom"); + dom.rebuild().expect("failed to run virtualdom"); file.write_fmt(format_args!( "{}", diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 80a4c17ef..866acf0a9 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -46,7 +46,7 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option { /// ```ignore /// static App: FC<()> = |cx| cx.render(rsx!(div { "hello world" })); /// let mut vdom = VirtualDom::new(App); -/// vdom.rebuild_in_place(); +/// vdom.rebuild(); /// /// let renderer = TextRenderer::new(&vdom); /// let output = format!("{}", renderer); @@ -74,7 +74,7 @@ impl<'a> TextRenderer<'a> { } fn html_render(&self, node: &VNode, f: &mut std::fmt::Formatter, il: u16) -> std::fmt::Result { - match &node.kind { + match &node { VNode::Text(text) => { if self.cfg.indent { for _ in 0..il { @@ -286,28 +286,28 @@ mod tests { #[test] fn to_string_works() { let mut dom = VirtualDom::new(SIMPLE_APP); - dom.rebuild_in_place().expect("failed to run virtualdom"); + dom.rebuild().expect("failed to run virtualdom"); dbg!(render_vdom(&dom, |c| c)); } #[test] fn hydration() { let mut dom = VirtualDom::new(NESTED_APP); - dom.rebuild_in_place().expect("failed to run virtualdom"); + dom.rebuild().expect("failed to run virtualdom"); dbg!(render_vdom(&dom, |c| c.pre_render(true))); } #[test] fn nested() { let mut dom = VirtualDom::new(NESTED_APP); - dom.rebuild_in_place().expect("failed to run virtualdom"); + dom.rebuild().expect("failed to run virtualdom"); dbg!(render_vdom(&dom, |c| c)); } #[test] fn fragment_app() { let mut dom = VirtualDom::new(FRAGMENT_APP); - dom.rebuild_in_place().expect("failed to run virtualdom"); + dom.rebuild().expect("failed to run virtualdom"); dbg!(render_vdom(&dom, |c| c)); } @@ -319,7 +319,7 @@ mod tests { let mut file = File::create("index.html").unwrap(); let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX); - dom.rebuild_in_place().expect("failed to run virtualdom"); + dom.rebuild().expect("failed to run virtualdom"); file.write_fmt(format_args!( "{}", @@ -337,7 +337,7 @@ mod tests { }; let mut dom = VirtualDom::new(STLYE_APP); - dom.rebuild_in_place().expect("failed to run virtualdom"); + dom.rebuild().expect("failed to run virtualdom"); dbg!(render_vdom(&dom, |c| c)); } } diff --git a/packages/web/examples/basic.rs b/packages/web/examples/basic.rs index 518115f7c..35d82daae 100644 --- a/packages/web/examples/basic.rs +++ b/packages/web/examples/basic.rs @@ -25,7 +25,7 @@ fn main() { } static App: FC<()> = |cx| { - let mut count = use_state(cx, || 0); + let mut count = use_state(cx, || 3); cx.render(rsx! { div { diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index e3a4e2c53..2aa57ea5b 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -84,8 +84,8 @@ impl WebsysDom { DomEdit::PushRoot { id: root } => self.push(root), DomEdit::PopRoot => self.pop(), DomEdit::AppendChildren { many } => self.append_children(many), - DomEdit::ReplaceWith { n, m } => self.replace_with(n, m), - DomEdit::Remove => self.remove(), + DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), + DomEdit::Remove { root } => self.remove(root), DomEdit::RemoveAllChildren => self.remove_all_children(), DomEdit::CreateTextNode { text, id } => self.create_text_node(text, id), DomEdit::CreateElement { tag, id } => self.create_element(tag, None, id), @@ -102,8 +102,8 @@ impl WebsysDom { DomEdit::SetAttribute { field, value, ns } => self.set_attribute(field, value, ns), DomEdit::RemoveAttribute { name } => self.remove_attribute(name), - DomEdit::InsertAfter { n } => self.insert_after(n), - DomEdit::InsertBefore { n } => self.insert_before(n), + DomEdit::InsertAfter { n, root } => self.insert_after(n, root), + DomEdit::InsertBefore { n, root } => self.insert_before(n, root), } } } @@ -154,7 +154,7 @@ impl WebsysDom { } } - fn replace_with(&mut self, n: u32, m: u32) { + fn replace_with(&mut self, m: u32, root: u64) { log::debug!("Called [`replace_with`]"); let mut new_nodes = vec![]; @@ -162,19 +162,11 @@ impl WebsysDom { new_nodes.push(self.stack.pop()); } - let mut old_nodes = vec![]; - for _ in 0..n { - old_nodes.push(self.stack.pop()); - } - - for node in &old_nodes[1..] { - node.dyn_ref::().unwrap().remove(); - } - - let old = old_nodes[0].clone(); + let old = self.nodes[root as usize].as_ref().unwrap(); let arr: js_sys::Array = new_nodes.iter().collect(); - let el = old.dyn_into::().unwrap(); + let el = old.dyn_ref::().unwrap(); el.replace_with_with_node(&arr).unwrap(); + // let arr = js_sys::Array::from(); // TODO: use different-sized replace withs @@ -205,9 +197,17 @@ impl WebsysDom { // self.stack.push(new_node); } - fn remove(&mut self) { + fn remove(&mut self, root: u64) { log::debug!("Called [`remove`]"); - todo!() + + let node = self.nodes[root as usize].as_ref().unwrap(); + if let Some(element) = node.dyn_ref::() { + element.remove(); + } else { + if let Some(parent) = node.parent_node() { + parent.remove_child(&node).unwrap(); + } + } } fn remove_all_children(&mut self) { @@ -361,7 +361,7 @@ impl WebsysDom { } } - fn insert_after(&mut self, n: u32) { + fn insert_after(&mut self, n: u32, root: u64) { let mut new_nodes = vec![]; for _ in 0..n { new_nodes.push(self.stack.pop()); @@ -380,7 +380,7 @@ impl WebsysDom { // let el = self.stack.top(); } - fn insert_before(&mut self, n: u32) { + fn insert_before(&mut self, n: u32, root: u64) { let n = n as usize; let root = self .stack diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 9ff8906e9..2adfb6b1c 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -1,30 +1,56 @@ //! Dioxus WebSys -//! -------------- -//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys. - -/* -From Google's guide on rAF and rIC: --------- - -If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed, -which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside - of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next - frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout, - which is a potential performance bottleneck. - -Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable, -and as such we could easily go past the deadline the browser provided. - -The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the -browser with that type of work in mind. That means that our code will need to use a document fragment, which can then -be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback -to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback. - -Essentially: ------------- -- Do the VDOM work during the idlecallback -- Do DOM work in the next requestAnimationFrame callback -*/ +//! +//! ## Overview +//! ------------ +//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using WebSys. This web render for +//! Dioxus is one of the more advanced renderers, supporting: +//! - idle work +//! - animations +//! - jank-free rendering +//! - noderefs +//! - controlled components +//! - re-hydration +//! - and more. +//! +//! The actual implementation is farily thin, with the heavy lifting happening inside the Dioxus Core crate. +//! +//! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide +//! validation of websys-specific features and not the general use of Dioxus. +//! +//! ## RequestAnimationFrame and RequestIdleCallback +//! ------------------------------------------------ +//! React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long +//! running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the +//! main thread. +//! +//! React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus, +//! the diff phase is non-blocking, using "yield_now" to allow the browser to process other events. When the diff phase +//! is finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply. +//! +//! Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then +//! setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame +//! +//! From Google's guide on rAF and rIC: +//! ----------------------------------- +//! +//! If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed, +//! which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside +//! of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next +//! frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout, +//! which is a potential performance bottleneck. +//! +//! Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable, +//! and as such we could easily go past the deadline the browser provided. +//! +//! The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the +//! browser with that type of work in mind. That means that our code will need to use a document fragment, which can then +//! be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback +//! to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback. +//! +//! Essentially: +//! ------------ +//! - Do the VDOM work during the idlecallback +//! - Do DOM work in the next requestAnimationFrame callback use std::rc::Rc; @@ -103,20 +129,22 @@ pub async fn run_with_props( ) -> Result<()> { let mut dom = VirtualDom::new_with_props(root, root_props); + let hydrating = cfg.hydrate; + let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap(); - let tasks = dom.get_event_sender(); + let sender_callback = Rc::new(move |event| tasks.unbounded_send(event).unwrap()); - // initialize the virtualdom first - // if cfg.hydrate { - // dom.rebuild_in_place()?; - // } + let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback); - let mut websys_dom = dom::WebsysDom::new( - root_el, - cfg, - Rc::new(move |event| tasks.unbounded_send(event).unwrap()), - ); + let mut mutations = dom.rebuild().unwrap(); + log::info!("Mutations: {:#?}", mutations); + + // hydrating is simply running the dom for a single render. If the page is already written, then the corresponding + // ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory + if !hydrating { + websys_dom.process_edits(&mut mutations.edits); + } loop { let deadline = gloo_timers::future::TimeoutFuture::new(16); @@ -127,8 +155,3 @@ pub async fn run_with_props( Ok(()) } - -struct HydrationNode { - id: usize, - node: Node, -} From df8aa77198712559f72bef093d064e03b2a5245a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 24 Aug 2021 12:51:53 -0400 Subject: [PATCH 25/53] fix: append isnt backwards --- packages/web/src/dom.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 2aa57ea5b..8d06a6f5f 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -124,8 +124,6 @@ impl WebsysDom { } fn append_children(&mut self, many: u32) { - log::debug!("Called [`append_child`]"); - let root: Node = self .stack .list @@ -133,9 +131,11 @@ impl WebsysDom { .unwrap() .clone(); - for _ in 0..many { - let child = self.stack.pop(); - + for child in self + .stack + .list + .drain((self.stack.list.len() - many as usize)..) + { if child.dyn_ref::().is_some() { if self.last_node_was_text { let comment_node = self @@ -143,20 +143,17 @@ impl WebsysDom { .create_comment("dioxus") .dyn_into::() .unwrap(); - self.stack.top().append_child(&comment_node).unwrap(); + root.append_child(&comment_node).unwrap(); } self.last_node_was_text = true; } else { self.last_node_was_text = false; } - root.append_child(&child).unwrap(); } } fn replace_with(&mut self, m: u32, root: u64) { - log::debug!("Called [`replace_with`]"); - let mut new_nodes = vec![]; for _ in 0..m { new_nodes.push(self.stack.pop()); @@ -198,8 +195,6 @@ impl WebsysDom { } fn remove(&mut self, root: u64) { - log::debug!("Called [`remove`]"); - let node = self.nodes[root as usize].as_ref().unwrap(); if let Some(element) = node.dyn_ref::() { element.remove(); @@ -211,7 +206,6 @@ impl WebsysDom { } fn remove_all_children(&mut self) { - log::debug!("Called [`remove_all_children`]"); todo!() } @@ -255,7 +249,6 @@ impl WebsysDom { self.nodes[id] = Some(el); // let nid = self.node_counter.?next(); // let nid = self.nodes.insert(el).data().as_ffi(); - // log::debug!("Called [`create_element`]: {}, {:?}", tag, nid); // ElementId::new(nid) } From f457b7113129479cad577237ef21cb735fffe483 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 24 Aug 2021 15:12:20 -0400 Subject: [PATCH 26/53] feat: rebuild doesn't return errors --- packages/core/benches/jsframework.rs | 2 +- packages/core/examples/jsframework.rs | 2 +- packages/core/src/diff.rs | 4 +- packages/core/src/events.rs | 46 +++--- packages/core/src/mutations.rs | 4 +- packages/core/src/virtual_dom.rs | 49 +++--- packages/core/tests/create_iterative.rs | 14 +- packages/core/tests/diff_iterative.rs | 4 +- packages/core/tests/eventsystem.rs | 2 +- packages/core/tests/vdom_rebuild.rs | 12 +- packages/core/tests/work_sync.rs | 2 +- packages/ssr/examples/basic.rs | 2 +- packages/ssr/examples/tide.rs | 4 +- packages/ssr/examples/tofile.rs | 3 +- packages/ssr/src/lib.rs | 12 +- packages/web/examples/basic.rs | 48 ++++-- packages/web/src/dom.rs | 210 +++++++++++++----------- packages/web/src/lib.rs | 2 +- 18 files changed, 233 insertions(+), 189 deletions(-) diff --git a/packages/core/benches/jsframework.rs b/packages/core/benches/jsframework.rs index b9dff6bb6..294a9a952 100644 --- a/packages/core/benches/jsframework.rs +++ b/packages/core/benches/jsframework.rs @@ -45,7 +45,7 @@ fn create_rows(c: &mut Criterion) { c.bench_function("create rows", |b| { b.iter(|| { let mut dom = VirtualDom::new(App); - let g = dom.rebuild().unwrap(); + let g = dom.rebuild(); assert!(g.edits.len() > 1); }) }); diff --git a/packages/core/examples/jsframework.rs b/packages/core/examples/jsframework.rs index 84be02832..79400afd1 100644 --- a/packages/core/examples/jsframework.rs +++ b/packages/core/examples/jsframework.rs @@ -6,7 +6,7 @@ use std::fmt::Display; fn main() { let mut dom = VirtualDom::new(App); - let g = dom.rebuild().unwrap(); + let g = dom.rebuild(); assert!(g.edits.len() > 1); } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 0702830a3..1646219aa 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -153,7 +153,7 @@ 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). - pub async fn work(&mut self) -> Result<()> { + pub async fn work(&mut self) { // 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.stack.pop() { @@ -190,8 +190,6 @@ impl<'bump> DiffMachine<'bump> { } }; } - - Ok(()) } fn mount(&mut self, and: MountType<'bump>) { diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 6c3d7c1d0..5c56f953e 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -602,8 +602,31 @@ pub mod on { } pub trait KeyboardEventInner { + fn alt_key(&self) -> bool; + fn char_code(&self) -> u32; + /// Identify which "key" was entered. + /// + /// This is the best method to use for all languages. They key gets mapped to a String sequence which you can match on. + /// The key isn't an enum because there are just so many context-dependent keys. + /// + /// A full list on which keys to use is available at: + /// + /// + /// # Example + /// + /// ```rust + /// match event.key().as_str() { + /// "Esc" | "Escape" => {} + /// "ArrowDown" => {} + /// "ArrowLeft" => {} + /// _ => {} + /// } + /// ``` + /// + fn key(&self) -> String; + /// Get the key code as an enum Variant. /// /// This is intended for things like arrow keys, escape keys, function keys, and other non-international keys. @@ -627,35 +650,14 @@ pub mod on { /// Check if the ctrl key was pressed down fn ctrl_key(&self) -> bool; - /// Identify which "key" was entered. - /// - /// This is the best method to use for all languages. They key gets mapped to a String sequence which you can match on. - /// The key isn't an enum because there are just so many context-dependent keys. - /// - /// A full list on which keys to use is available at: - /// - /// - /// # Example - /// - /// ```rust - /// match event.key().as_str() { - /// "Esc" | "Escape" => {} - /// "ArrowDown" => {} - /// "ArrowLeft" => {} - /// _ => {} - /// } - /// ``` - /// - fn key(&self) -> String; + fn get_modifier_state(&self, key_code: &str) -> bool; - // fn key(&self) -> String; fn locale(&self) -> String; fn location(&self) -> usize; fn meta_key(&self) -> bool; fn repeat(&self) -> bool; fn shift_key(&self) -> bool; fn which(&self) -> usize; - fn get_modifier_state(&self, key_code: usize) -> bool; } pub trait FocusEventInner { diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index afefc1cf0..7b00b2fd0 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -139,8 +139,8 @@ impl<'a> Mutations<'a> { // refs are only assigned once pub struct NodeRefMutation<'a> { - element: &'a mut Option>>, - element_id: ElementId, + pub element: &'a mut Option>>, + pub element_id: ElementId, } impl<'a> std::fmt::Debug for NodeRefMutation<'a> { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 2718aa222..5506b5f03 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -124,26 +124,12 @@ impl VirtualDom { } } - pub fn launch_in_place(root: FC<()>) -> Self { - let mut s = Self::new(root); - s.rebuild().unwrap(); - s - } - - /// Creates a new virtualdom and immediately rebuilds it in place, not caring about the RealDom to write into. - /// - pub fn launch_with_props_in_place(root: FC

, root_props: P) -> Self { - let mut s = Self::new_with_props(root, root_props); - s.rebuild().unwrap(); - s - } - pub fn base_scope(&self) -> &Scope { - unsafe { self.shared.get_scope(self.base_scope).unwrap() } + self.shared.get_scope(self.base_scope).unwrap() } pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> { - unsafe { self.shared.get_scope(id) } + self.shared.get_scope(id) } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch @@ -152,7 +138,7 @@ impl VirtualDom { /// /// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed /// through "run". We completely avoid the task scheduler infrastructure. - pub fn rebuild<'s>(&'s mut self) -> Result> { + pub fn rebuild<'s>(&'s mut self) -> Mutations<'s> { let mut fut = self.rebuild_async().boxed_local(); loop { @@ -163,7 +149,11 @@ impl VirtualDom { } /// Rebuild the dom from the ground up - pub async fn rebuild_async<'s>(&'s mut self) -> Result> { + /// + /// This method is asynchronous to prevent the application from blocking while the dom is being rebuilt. Computing + /// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This + /// method can be useful when needing to perform some crucial periodic tasks. + pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> { let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); let cur_component = self @@ -176,7 +166,7 @@ impl VirtualDom { diff_machine .stack .create_node(cur_component.frames.fin_head(), MountType::Append); - diff_machine.work().await.unwrap(); + diff_machine.work().await; } else { // todo: should this be a hard error? log::warn!( @@ -185,11 +175,20 @@ impl VirtualDom { ); } - Ok(diff_machine.mutations) + diff_machine.mutations } - // diff the dom with itself - pub async fn diff_async<'s>(&'s mut self) -> Result> { + pub fn diff_sync<'s>(&'s mut self) -> Mutations<'s> { + let mut fut = self.diff_async().boxed_local(); + + loop { + if let Some(edits) = (&mut fut).now_or_never() { + break edits; + } + } + } + + pub async fn diff_async<'s>(&'s mut self) -> Mutations<'s> { let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); let cur_component = self @@ -204,15 +203,15 @@ impl VirtualDom { new: cur_component.frames.fin_head(), }); - diff_machine.work().await.unwrap(); + diff_machine.work().await; - Ok(diff_machine.mutations) + diff_machine.mutations } /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. /// /// This method will not wait for any suspended nodes to complete. - pub fn run_immediate<'s>(&'s mut self) -> Result> { + pub fn run_immediate<'s>(&'s mut self) -> Mutations<'s> { todo!() // use futures_util::FutureExt; // let mut is_ready = || false; diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_iterative.rs index cbd4068a0..e6303e31e 100644 --- a/packages/core/tests/create_iterative.rs +++ b/packages/core/tests/create_iterative.rs @@ -22,7 +22,7 @@ fn test_original_diff() { }; let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild().unwrap(); + let mutations = dom.rebuild(); assert_eq!( mutations.edits, [ @@ -61,7 +61,7 @@ async fn create() { test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await.unwrap(); + let mutations = dom.rebuild_async().await; assert_eq!( mutations.edits, [ @@ -103,7 +103,7 @@ async fn create_list() { test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await.unwrap(); + let mutations = dom.rebuild_async().await; // copilot wrote this test :P assert_eq!( @@ -146,7 +146,7 @@ async fn create_simple() { test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await.unwrap(); + let mutations = dom.rebuild_async().await; // copilot wrote this test :P assert_eq!( @@ -182,7 +182,7 @@ async fn create_components() { test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await.unwrap(); + let mutations = dom.rebuild_async().await; assert_eq!( mutations.edits, @@ -228,7 +228,7 @@ async fn anchors() { test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await.unwrap(); + let mutations = dom.rebuild_async().await; assert_eq!( mutations.edits, [ @@ -254,7 +254,7 @@ async fn suspended() { test_logging::set_up_logging(LOGGING_ENABLED); let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await.unwrap(); + let mutations = dom.rebuild_async().await; assert_eq!( mutations.edits, diff --git a/packages/core/tests/diff_iterative.rs b/packages/core/tests/diff_iterative.rs index be1c02992..e4d247031 100644 --- a/packages/core/tests/diff_iterative.rs +++ b/packages/core/tests/diff_iterative.rs @@ -40,9 +40,9 @@ async fn test_iterative_create_components() { let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await.unwrap(); + let mutations = dom.rebuild_async().await; dbg!(mutations); - let mutations = dom.diff_async().await.unwrap(); + let mutations = dom.diff_async().await; dbg!(mutations); } diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs index 919b0a447..677f9445b 100644 --- a/packages/core/tests/eventsystem.rs +++ b/packages/core/tests/eventsystem.rs @@ -14,7 +14,7 @@ async fn event_queue_works() { }; let mut dom = VirtualDom::new(App); - let edits = dom.rebuild().unwrap(); + let edits = dom.rebuild(); async_std::task::spawn_local(async move { match dom.run_unbounded().await { diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index a3c1fdb3b..3cd985f11 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -19,7 +19,7 @@ fn app_runs() { cx.render(rsx!( div{"hello"} )) }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild().unwrap(); + let edits = vdom.rebuild(); dbg!(edits); } @@ -32,7 +32,7 @@ fn fragments_work() { )) }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild().unwrap(); + let edits = vdom.rebuild(); // should result in a final "appendchildren n=2" dbg!(edits); } @@ -46,7 +46,7 @@ fn lists_work() { )) }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild().unwrap(); + let edits = vdom.rebuild(); dbg!(edits); } @@ -61,7 +61,7 @@ fn conditional_rendering() { }; let mut vdom = VirtualDom::new(App); - let mutations = vdom.rebuild().unwrap(); + let mutations = vdom.rebuild(); dbg!(&mutations); // the "false" fragment should generate an empty placeholder to re-visit assert!(mutations.edits[mutations.edits.len() - 2].is("CreatePlaceholder")); @@ -82,7 +82,7 @@ fn child_components() { )) }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild().unwrap(); + let edits = vdom.rebuild(); dbg!(edits); } @@ -94,6 +94,6 @@ fn suspended_works() { }; let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild().unwrap(); + let edits = vdom.rebuild(); dbg!(edits); } diff --git a/packages/core/tests/work_sync.rs b/packages/core/tests/work_sync.rs index ed9269bc7..ba2d785d3 100644 --- a/packages/core/tests/work_sync.rs +++ b/packages/core/tests/work_sync.rs @@ -22,7 +22,7 @@ fn worksync() { let mutations = loop { let g = (&mut fut).now_or_never(); if g.is_some() { - break g.unwrap().unwrap(); + break g.unwrap(); } }; diff --git a/packages/ssr/examples/basic.rs b/packages/ssr/examples/basic.rs index b3bfb6ca5..ece077759 100644 --- a/packages/ssr/examples/basic.rs +++ b/packages/ssr/examples/basic.rs @@ -5,7 +5,7 @@ use dioxus_html as dioxus_elements; fn main() { let mut dom = VirtualDom::new(App); - dom.rebuild().unwrap(); + dom.rebuild(); println!( "{}", dioxus_ssr::render_vdom(&dom, |c| c.newline(true).indent(true)) diff --git a/packages/ssr/examples/tide.rs b/packages/ssr/examples/tide.rs index 26f742bcf..9fc58d2be 100644 --- a/packages/ssr/examples/tide.rs +++ b/packages/ssr/examples/tide.rs @@ -19,7 +19,9 @@ async fn main() -> Result<(), std::io::Error> { .map(|f| f.parse().unwrap_or("...?".to_string())) .unwrap_or("...?".to_string()); - let dom = VirtualDom::launch_with_props_in_place(Example, ExampleProps { initial_name }); + let mut dom = VirtualDom::new_with_props(Example, ExampleProps { initial_name }); + + dom.rebuild(); Ok(Response::builder(200) .body(format!("{}", dioxus_ssr::render_vdom(&dom, |c| c))) diff --git a/packages/ssr/examples/tofile.rs b/packages/ssr/examples/tofile.rs index 09b710486..ec72532d0 100644 --- a/packages/ssr/examples/tofile.rs +++ b/packages/ssr/examples/tofile.rs @@ -13,7 +13,8 @@ fn main() { let mut file = File::create("example.html").unwrap(); let mut dom = VirtualDom::new(App); - dom.rebuild().expect("failed to run virtualdom"); + + dom.rebuild(); file.write_fmt(format_args!( "{}", diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 866acf0a9..00c028cd1 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -286,28 +286,28 @@ mod tests { #[test] fn to_string_works() { let mut dom = VirtualDom::new(SIMPLE_APP); - dom.rebuild().expect("failed to run virtualdom"); + dom.rebuild(); dbg!(render_vdom(&dom, |c| c)); } #[test] fn hydration() { let mut dom = VirtualDom::new(NESTED_APP); - dom.rebuild().expect("failed to run virtualdom"); + dom.rebuild(); dbg!(render_vdom(&dom, |c| c.pre_render(true))); } #[test] fn nested() { let mut dom = VirtualDom::new(NESTED_APP); - dom.rebuild().expect("failed to run virtualdom"); + dom.rebuild(); dbg!(render_vdom(&dom, |c| c)); } #[test] fn fragment_app() { let mut dom = VirtualDom::new(FRAGMENT_APP); - dom.rebuild().expect("failed to run virtualdom"); + dom.rebuild(); dbg!(render_vdom(&dom, |c| c)); } @@ -319,7 +319,7 @@ mod tests { let mut file = File::create("index.html").unwrap(); let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX); - dom.rebuild().expect("failed to run virtualdom"); + dom.rebuild(); file.write_fmt(format_args!( "{}", @@ -337,7 +337,7 @@ mod tests { }; let mut dom = VirtualDom::new(STLYE_APP); - dom.rebuild().expect("failed to run virtualdom"); + dom.rebuild(); dbg!(render_vdom(&dom, |c| c)); } } diff --git a/packages/web/examples/basic.rs b/packages/web/examples/basic.rs index 35d82daae..0fbecec7c 100644 --- a/packages/web/examples/basic.rs +++ b/packages/web/examples/basic.rs @@ -1,19 +1,17 @@ //! Basic example that renders a simple VNode to the browser. +// all these imports are done automatically with the `dioxus` crate and `prelude` +// need to do them manually for this example use dioxus::events::on::MouseEvent; use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_hooks::*; use dioxus_html as dioxus_elements; -// use wasm_timer; - -use std::future::Future; - -use std::{pin::Pin, time::Duration}; use dioxus::prelude::*; - use dioxus_web::*; +use std::future::Future; +use std::{pin::Pin, time::Duration}; fn main() { // Setup logging @@ -21,17 +19,33 @@ fn main() { console_error_panic_hook::set_once(); // Run the app - dioxus_web::launch(App, |c| c) + dioxus_web::launch(APP, |c| c) } -static App: FC<()> = |cx| { +static APP: FC<()> = |cx| { let mut count = use_state(cx, || 3); cx.render(rsx! { div { - button { - "add" - onclick: move |_| count += 1 + button { onclick: move |_| count += 1, "add" } + div { + div { + div { + div { + div { + Fragment { + a {"wo"} + p {"wo"} + li {"wo"} + em {"wo"} + } + } + } + Child {} + } + Child {} + } + Child {} } ul { {(0..*count).map(|f| rsx!{ @@ -43,3 +57,15 @@ static App: FC<()> = |cx| { } }) }; + +static Child: FC<()> = |cx| { + cx.render(rsx! { + div { + div { + div { + "hello child" + } + } + } + }) +}; diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 8d06a6f5f..d36e6e2d8 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, rc::Rc, sync::Arc}; use dioxus_core::{ events::{EventTrigger, VirtualEvent}, + mutations::NodeRefMutation, DomEdit, ElementId, ScopeId, }; use fxhash::FxHashMap; @@ -77,6 +78,18 @@ impl WebsysDom { } } + pub fn apply_refs(&mut self, refs: &[NodeRefMutation]) { + for item in refs { + if let Some(bla) = &item.element { + let node = self.nodes[item.element_id.as_u64() as usize] + .as_ref() + .unwrap() + .clone(); + bla.set(Box::new(node)).unwrap(); + } + } + } + pub fn process_edits(&mut self, edits: &mut Vec) { for edit in edits.drain(..) { log::info!("Handling edit: {:#?}", edit); @@ -92,12 +105,13 @@ impl WebsysDom { DomEdit::CreateElementNs { tag, id, ns } => self.create_element(tag, Some(ns), id), DomEdit::CreatePlaceholder { id } => self.create_placeholder(id), DomEdit::NewEventListener { - event_name: event, + event_name, scope, - mounted_node_id: node, - } => self.new_event_listener(event, scope, node), + mounted_node_id, + } => self.new_event_listener(event_name, scope, mounted_node_id), DomEdit::RemoveEventListener { event } => todo!(), + DomEdit::SetText { text } => self.set_text(text), DomEdit::SetAttribute { field, value, ns } => self.set_attribute(field, value, ns), DomEdit::RemoveAttribute { name } => self.remove_attribute(name), @@ -118,6 +132,7 @@ impl WebsysDom { self.stack.push(real_node); } + // drop the node off the stack fn pop(&mut self) { self.stack.pop(); @@ -154,44 +169,21 @@ impl WebsysDom { } fn replace_with(&mut self, m: u32, root: u64) { - let mut new_nodes = vec![]; - for _ in 0..m { - new_nodes.push(self.stack.pop()); - } - let old = self.nodes[root as usize].as_ref().unwrap(); - let arr: js_sys::Array = new_nodes.iter().collect(); - let el = old.dyn_ref::().unwrap(); - el.replace_with_with_node(&arr).unwrap(); - // let arr = js_sys::Array::from(); + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - m as usize)..) + .collect(); - // TODO: use different-sized replace withs - // if m == 1 { - // if old_node.has_type::() { - // old_node - // .dyn_ref::() - // .unwrap() - // .replace_with_with_node_1(&new_node) - // .unwrap(); - // } else if old_node.has_type::() { - // old_node - // .dyn_ref::() - // .unwrap() - // .replace_with_with_node_1(&new_node) - // .unwrap(); - // } else if old_node.has_type::() { - // old_node - // .dyn_ref::() - // .unwrap() - // .replace_with_with_node_1(&new_node) - // .unwrap(); - // } else { - // panic!("Cannot replace node: {:?}", old_node); - // } - // } - - // self.stack.push(new_node); + if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } } fn remove(&mut self, root: u64) { @@ -213,22 +205,22 @@ impl WebsysDom { self.create_element("pre", None, id); self.set_attribute("hidden", "", None); } - fn create_text_node(&mut self, text: &str, id: u64) { - // let nid = self.node_counter.next(); + fn create_text_node(&mut self, text: &str, id: u64) { let textnode = self .document .create_text_node(text) .dyn_into::() .unwrap(); - let id = id as usize; self.stack.push(textnode.clone()); - self.nodes[id] = Some(textnode); + + self.nodes[(id as usize)] = Some(textnode); } fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { let tag = wasm_bindgen::intern(tag); + let el = match ns { Some(ns) => self .document @@ -243,17 +235,12 @@ impl WebsysDom { .dyn_into::() .unwrap(), }; - let id = id as usize; self.stack.push(el.clone()); - self.nodes[id] = Some(el); - // let nid = self.node_counter.?next(); - // let nid = self.nodes.insert(el).data().as_ffi(); - // ElementId::new(nid) + self.nodes[(id as usize)] = Some(el); } fn new_event_listener(&mut self, event: &'static str, scope: ScopeId, real_id: u64) { - // let (_on, event) = event.split_at(2); let event = wasm_bindgen::intern(event); // attach the correct attributes to the element @@ -313,7 +300,8 @@ impl WebsysDom { } fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>) { - if let Some(el) = self.stack.top().dyn_ref::() { + let node = self.stack.top(); + if let Some(el) = node.dyn_ref::() { match ns { // inline style support Some("style") => { @@ -323,11 +311,20 @@ impl WebsysDom { } _ => el.set_attribute(name, value).unwrap(), } - match name { - "value" => {} - "checked" => {} - "selected" => {} - _ => {} + } + + if let Some(node) = node.dyn_ref::() { + if name == "value" { + node.set_value(value); + } + if name == "checked" { + node.set_checked(true); + } + } + + if let Some(node) = node.dyn_ref::() { + if name == "selected" { + node.set_selected(true); } } } @@ -355,35 +352,50 @@ impl WebsysDom { } fn insert_after(&mut self, n: u32, root: u64) { - let mut new_nodes = vec![]; - for _ in 0..n { - new_nodes.push(self.stack.pop()); + let old = self.nodes[root as usize].as_ref().unwrap(); + + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); } - - let after = self.stack.top().clone(); - let arr: js_sys::Array = new_nodes.iter().collect(); - - let el = after.dyn_into::().unwrap(); - el.after_with_node(&arr).unwrap(); - // let mut old_nodes = vec![]; - // for _ in 0..n { - // old_nodes.push(self.stack.pop()); - // } - - // let el = self.stack.top(); } fn insert_before(&mut self, n: u32, root: u64) { - let n = n as usize; - let root = self - .stack - .list - .get(self.stack.list.len() - n) - .unwrap() - .clone(); - for _ in 0..n { - let el = self.stack.pop(); - root.insert_before(&el, None).unwrap(); + let after = self.nodes[root as usize].as_ref().unwrap(); + + if n == 1 { + let before = self.stack.pop(); + + after + .parent_node() + .unwrap() + .insert_before(&before, Some(&after)) + .unwrap(); + + after.insert_before(&before, None).unwrap(); + } else { + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = after.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = after.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = after.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } } } } @@ -447,46 +459,50 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { "keydown" | "keypress" | "keyup" => { struct Event(web_sys::KeyboardEvent); impl KeyboardEventInner for Event { + fn alt_key(&self) -> bool { + self.0.alt_key() + } fn char_code(&self) -> u32 { - todo!() + self.0.char_code() } - fn key_code(&self) -> KeyCode { - todo!() - } - fn ctrl_key(&self) -> bool { - todo!() + fn key(&self) -> String { + self.0.key() } - fn key(&self) -> String { - todo!() + fn key_code(&self) -> KeyCode { + KeyCode::from_raw_code(self.0.key_code() as u8) + } + + fn ctrl_key(&self) -> bool { + self.0.ctrl_key() + } + + fn get_modifier_state(&self, key_code: &str) -> bool { + self.0.get_modifier_state(key_code) } fn locale(&self) -> String { - todo!() + todo!("Locale is currently not supported. :(") } fn location(&self) -> usize { - todo!() + self.0.location() as usize } fn meta_key(&self) -> bool { - todo!() + self.0.meta_key() } fn repeat(&self) -> bool { - todo!() + self.0.repeat() } fn shift_key(&self) -> bool { - todo!() + self.0.shift_key() } fn which(&self) -> usize { - todo!() - } - - fn get_modifier_state(&self, key_code: usize) -> bool { - todo!() + self.0.which() as usize } } let evt: web_sys::KeyboardEvent = event.clone().dyn_into().unwrap(); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 2adfb6b1c..aa482b294 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -137,7 +137,7 @@ pub async fn run_with_props( let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback); - let mut mutations = dom.rebuild().unwrap(); + let mut mutations = dom.rebuild(); log::info!("Mutations: {:#?}", mutations); // hydrating is simply running the dom for a single render. If the page is already written, then the corresponding From 8b0d04ce585596f561f15e92fc087e2c7063eb07 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 24 Aug 2021 16:29:10 -0400 Subject: [PATCH 27/53] wip: ric_raf wired up --- packages/core/src/virtual_dom.rs | 20 +++++--- packages/web/src/lib.rs | 42 ++++++---------- packages/web/src/ric_raf.rs | 83 ++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 packages/web/src/ric_raf.rs diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 5506b5f03..3b95aec76 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -225,8 +225,8 @@ impl VirtualDom { /// If there are pending tasks, they will be progressed before returning. This is useful when rendering an application /// that has suspended nodes or suspended tasks. Be warned - any async tasks running forever will prevent this method /// from completing. Consider using `run` and specifing a deadline. - pub async fn run_unbounded<'s>(&'s mut self) -> Result> { - self.run_with_deadline(async {}).await + pub async fn run_unbounded<'s>(&'s mut self) -> Mutations<'s> { + self.run_with_deadline(async {}).await.unwrap() } /// Run the virtualdom with a deadline. @@ -275,7 +275,7 @@ impl VirtualDom { pub async fn run_with_deadline<'s>( &'s mut self, deadline: impl Future, - ) -> Result> { + ) -> Option> { let mut committed_mutations = Mutations::new(); let mut deadline = Box::pin(deadline.fuse()); @@ -293,12 +293,12 @@ impl VirtualDom { let deadline_expired = self.scheduler.wait_for_any_trigger(&mut deadline).await; if deadline_expired { - return Ok(committed_mutations); + return Some(committed_mutations); } } // Create work from the pending event queue - self.scheduler.consume_pending_events()?; + self.scheduler.consume_pending_events(); // Work through the current subtree, and commit the results when it finishes // When the deadline expires, give back the work @@ -316,10 +316,10 @@ impl VirtualDom { */ if !self.scheduler.has_any_work() { - return Ok(committed_mutations); + return Some(committed_mutations); } } - FiberResult::Interrupted => return Ok(committed_mutations), + FiberResult::Interrupted => return None, } } } @@ -327,6 +327,12 @@ impl VirtualDom { pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { self.shared.ui_event_sender.clone() } + + pub fn has_work(&self) -> bool { + true + } + + pub async fn wait_for_any_work(&self) {} } // TODO! diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index aa482b294..9b840d74c 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -56,20 +56,16 @@ use std::rc::Rc; pub use crate::cfg::WebConfig; use crate::dom::load_document; -use dioxus::prelude::{Context, Properties, VNode}; +use dioxus::prelude::Properties; use dioxus::virtual_dom::VirtualDom; pub use dioxus_core as dioxus; -use dioxus_core::error::Result; -use dioxus_core::{events::EventTrigger, prelude::FC}; -use futures_util::{pin_mut, Stream, StreamExt}; -use fxhash::FxHashMap; -use js_sys::Iterator; -use web_sys::{window, Document, Element, Event, Node, NodeList}; +use dioxus_core::prelude::FC; mod cache; mod cfg; mod dom; mod nodeslab; +mod ric_raf; /// Launches the VirtualDOM from the specified component function. /// @@ -100,15 +96,8 @@ where { let config = config(WebConfig::default()); let fut = run_with_props(root, root_props, config); - - wasm_bindgen_futures::spawn_local(async { - match fut.await { - Ok(_) => log::error!("Your app completed running... somehow?"), - Err(e) => log::error!("Your app crashed! {}", e), - } - }); + wasm_bindgen_futures::spawn_local(fut); } - /// This method is the primary entrypoint for Websys Dioxus apps. Will panic if an error occurs while rendering. /// See DioxusErrors for more information on how these errors could occour. /// @@ -122,11 +111,7 @@ where /// /// Run the app to completion, panicing if any error occurs while rendering. /// Pairs well with the wasm_bindgen async handler -pub async fn run_with_props( - root: FC, - root_props: T, - cfg: WebConfig, -) -> Result<()> { +pub async fn run_with_props(root: FC, root_props: T, cfg: WebConfig) { let mut dom = VirtualDom::new_with_props(root, root_props); let hydrating = cfg.hydrate; @@ -138,7 +123,6 @@ pub async fn run_with_props( let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback); let mut mutations = dom.rebuild(); - log::info!("Mutations: {:#?}", mutations); // hydrating is simply running the dom for a single render. If the page is already written, then the corresponding // ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory @@ -146,12 +130,18 @@ pub async fn run_with_props( websys_dom.process_edits(&mut mutations.edits); } + let work_loop = ric_raf::RafLoop::new(); loop { - let deadline = gloo_timers::future::TimeoutFuture::new(16); - let mut mutations = dom.run_with_deadline(deadline).await?; + // if virtualdom has nothing, wait for it to have something before requesting idle time + if !dom.has_work() { + dom.wait_for_any_work().await; + } - websys_dom.process_edits(&mut mutations.edits); + let deadline = work_loop.wait_for_idle_time().await; + + if let Some(mut mutations) = dom.run_with_deadline(deadline).await { + work_loop.wait_for_raf().await; + websys_dom.process_edits(&mut mutations.edits); + } } - - Ok(()) } diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs new file mode 100644 index 000000000..42850fb3e --- /dev/null +++ b/packages/web/src/ric_raf.rs @@ -0,0 +1,83 @@ +//! RequestAnimationFrame and RequestIdleCallback port and polyfill. + +use std::{cell::RefCell, fmt, rc::Rc}; + +use gloo_timers::future::TimeoutFuture; +use js_sys::Function; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen::{prelude::Closure, JsValue}; +use web_sys::Window; + +pub struct RafLoop { + window: Window, + ric_receiver: async_channel::Receiver<()>, + raf_receiver: async_channel::Receiver<()>, + ric_closure: Closure, + raf_closure: Closure, +} + +impl RafLoop { + pub fn new() -> Self { + let (raf_sender, raf_receiver) = async_channel::unbounded(); + + let raf_closure: Closure = + Closure::wrap(Box::new(move |v: JsValue| raf_sender.try_send(()).unwrap())); + + let (ric_sender, ric_receiver) = async_channel::unbounded(); + + let ric_closure: Closure = + Closure::wrap(Box::new(move |v: JsValue| ric_sender.try_send(()).unwrap())); + + Self { + window: web_sys::window().unwrap(), + raf_receiver, + raf_closure, + ric_receiver, + ric_closure, + } + } + /// waits for some idle time and returns a timeout future that expires after the idle time has passed + pub async fn wait_for_idle_time(&self) -> TimeoutFuture { + // comes with its own safari polyfill :) + + let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); + let deadline: u32 = self.window.request_idle_callback(ric_fn).unwrap(); + + self.ric_receiver.recv().await.unwrap(); + + let deadline = TimeoutFuture::new(deadline); + deadline + } + + pub async fn wait_for_raf(&self) { + let raf_fn = self.raf_closure.as_ref().dyn_ref::().unwrap(); + let id: i32 = self.window.request_animation_frame(raf_fn).unwrap(); + self.raf_receiver.recv().await.unwrap(); + } +} + +#[derive(Debug)] +pub struct AnimationFrame { + render_id: i32, + closure: Closure, + callback_wrapper: Rc>>, +} + +struct CallbackWrapper(Box); +impl fmt::Debug for CallbackWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("CallbackWrapper") + } +} + +impl Drop for AnimationFrame { + fn drop(&mut self) { + if self.callback_wrapper.borrow_mut().is_some() { + web_sys::window() + .unwrap_throw() + .cancel_animation_frame(self.render_id) + .unwrap_throw() + } + } +} From 59219b9ef22caa5896046ccaabc8a225cc4b5a53 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 24 Aug 2021 16:36:00 -0400 Subject: [PATCH 28/53] wip: ricraf polyfill --- packages/core/src/diff.rs.old | 1708 ------------------------------- packages/web/examples/basic.rs | 20 +- packages/web/src/ric_raf.rs | 5 + packages/web/src/ricpolyfill.js | 28 + 4 files changed, 34 insertions(+), 1727 deletions(-) delete mode 100644 packages/core/src/diff.rs.old create mode 100644 packages/web/src/ricpolyfill.js diff --git a/packages/core/src/diff.rs.old b/packages/core/src/diff.rs.old deleted file mode 100644 index 6bcf03568..000000000 --- a/packages/core/src/diff.rs.old +++ /dev/null @@ -1,1708 +0,0 @@ -//! 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, incremental diffing, cancelation, NodeRefs, and additional -//! batching operations. -//! -//! ## Implementation Details: -//! -//! ### IDs for elements -//! -------------------- -//! 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 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 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 -//! -//! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments -//! can be particularly challenging when they are empty, so the placeholder node lets us "reserve" a spot for the empty -//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the NodeFactory - it is -//! 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 -//! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the -//! calls to "create" propogate this information upwards. Structures like the one below are entirely static: -//! ```rust -//! 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 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. 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 -//! --------------------- -//! Dioxus uses a passive garbage collection system to clean up old nodes once the work has been completed. This garabge -//! collection is done internally once the main diffing work is complete. After the "garbage" is collected, Dioxus will then -//! start to re-use old keys for new nodes. This results in a passive memory management system that is very efficient. -//! -//! The IDs used by the key/map are just an index into a vec. This means that Dioxus will drive the key allocation strategy -//! 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. -//! -//! Further Reading and Thoughts -//! ---------------------------- -//! There are more ways of increasing diff performance here that are currently not implemented. -//! More info on how to improve this diffing algorithm: -//! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ - -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, collections::HashSet, marker::PhantomData, pin::Pin, -}; -use DomEdit::*; - -pub struct DiffMachine<'bump> { - vdom: &'bump SharedResources, - - pub mutations: Mutations<'bump>, - - pub node_stack: SmallVec<[SearchNode<'bump>; 10]>, - - pub scope_stack: SmallVec<[ScopeId; 5]>, - - pub diffed: FxHashSet, - - pub seen_scopes: FxHashSet, -} -enum SearchNode<'a> { - Node { - old: &'a VNode<'a>, - new: &'a VNode<'a>, - }, - NodeList { - old: &'a [VNode<'a>], - node: &'a [VNode<'a>], - }, -} - -impl<'bump> DiffMachine<'bump> { - pub(crate) fn new( - edits: Mutations<'bump>, - cur_scope: ScopeId, - shared: &'bump SharedResources, - ) -> Self { - Self { - node_stack: smallvec![], - mutations: edits, - scope_stack: smallvec![cur_scope], - vdom: shared, - diffed: FxHashSet::default(), - seen_scopes: FxHashSet::default(), - } - } - - /// Allows the creation of a diff machine without the concept of scopes or a virtualdom - /// this is mostly useful for testing - /// - /// This will PANIC if any component elements are passed in. - pub fn new_headless(shared: &'bump SharedResources) -> Self { - Self { - node_stack: smallvec![], - mutations: Mutations::new(), - scope_stack: smallvec![ScopeId(0)], - vdom: shared, - diffed: FxHashSet::default(), - seen_scopes: FxHashSet::default(), - } - } - - // - 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); - Ok(()) - } - - /// Progress the diffing for this "fiber" - /// - /// 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 - /// - /// - /// - pub async fn work(&mut self) -> Result<()> { - // - while let Some(search) = self.node_stack.last_mut() { - // Prevent our task from blocking permanently - yield_now().await; - - match search { - SearchNode::Node { old, new } => { - // - // self.diff_node(old, new); - } - - SearchNode::NodeList { old, node } => { - // - todo!() - } - } - } - - Ok(()) - } - - // Diff the `old` node with the `new` node. Emits instructions to modify a - // physical DOM node that reflects `old` into something that reflects `new`. - // - // 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>) { - 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". - // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. - (VNodeKind::Text(old), VNodeKind::Text(new)) => { - let root = old_node.direct_id(); - - if old.text != new.text { - self.edit_push_root(root); - self.edit_set_text(new.text); - self.edit_pop(); - } - - new.dom_id.set(Some(root)); - } - - (VNodeKind::Element(old), VNodeKind::Element(new)) => { - let root = old_node.direct_id(); - - // 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_name != old.tag_name || new.namespace != old.namespace { - self.replace_node_with_node(root, old_node, new_node); - return; - } - - new.dom_id.set(Some(root)); - - // Don't push the root if we don't have to - let mut has_comitted = false; - let mut please_commit = |edits: &mut Vec| { - if !has_comitted { - has_comitted = true; - edits.push(PushRoot { id: root.as_u64() }); - } - }; - - // 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 { - please_commit(&mut self.mutations.edits); - self.edit_set_attribute(new_attr); - } - } - } else { - // TODO: provide some sort of report on how "good" the diffing was - please_commit(&mut self.mutations.edits); - for attribute in old.attributes { - self.edit_remove_attribute(attribute); - } - for attribute in new.attributes { - self.edit_set_attribute(attribute) - } - } - - // 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 - let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); - 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 { - please_commit(&mut self.mutations.edits); - self.edit_remove_event_listener(old_l.event); - self.edit_new_event_listener(new_l, cur_scope); - } - new_l.mounted_node.set(old_l.mounted_node.get()); - self.fix_listener(new_l); - } - } else { - please_commit(&mut self.mutations.edits); - for listener in old.listeners { - self.edit_remove_event_listener(listener.event); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.edit_new_event_listener(listener, cur_scope); - - // Make sure the listener gets attached to the scope list - self.fix_listener(listener); - } - } - - if has_comitted { - self.edit_pop(); - } - - self.diff_children(old.children, new.children); - } - - (VNodeKind::Component(old), VNodeKind::Component(new)) => { - let scope_addr = old.ass_scope.get().unwrap(); - - // Make sure we're dealing with the same component (by function pointer) - if old.user_fc == new.user_fc { - // - self.scope_stack.push(scope_addr); - - // Make sure the new component vnode is referencing the right scope id - new.ass_scope.set(Some(scope_addr)); - - // make sure the component's caller function is up to date - let scope = self.get_scope_mut(&scope_addr).unwrap(); - - scope - .update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); - - // React doesn't automatically memoize, but we do. - let compare = old.comparator.unwrap(); - - match compare(new) { - true => { - // the props are the same... - } - false => { - // the props are different... - scope.run_scope().unwrap(); - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); - } - } - - self.scope_stack.pop(); - - self.seen_scopes.insert(scope_addr); - } else { - let mut old_iter = RealChildIterator::new(old_node, &self.vdom); - let first = old_iter - .next() - .expect("Components should generate a placeholder root"); - - // remove any leftovers - for to_remove in old_iter { - self.edit_push_root(to_remove.direct_id()); - self.edit_remove(); - } - - // seems like we could combine this into a single instruction.... - self.replace_node_with_node(first.direct_id(), old_node, new_node); - - // Wipe the old one and plant the new one - let old_scope = old.ass_scope.get().unwrap(); - self.destroy_scopes(old_scope); - } - } - - (VNodeKind::Fragment(old), VNodeKind::Fragment(new)) => { - // 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; - } - - self.diff_children(old.children, new.children); - } - - (VNodeKind::Anchor(old), VNodeKind::Anchor(new)) => { - new.dom_id.set(old.dom_id.get()); - } - - // The strategy here is to pick the first possible node from the previous set and use that as our replace with root - // - // We also walk the "real node" list to make sure all latent roots are claened up - // This covers the case any time a fragment or component shows up with pretty much anything else - // - // This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases - // are pretty rare. IE replacing a list (component or fragment) with a single node. - ( - VNodeKind::Component(_) - | VNodeKind::Fragment(_) - | VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_), - VNodeKind::Component(_) - | VNodeKind::Fragment(_) - | VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_), - ) => { - self.replace_and_create_many_with_many([old_node], [new_node]); - } - - // TODO - (VNodeKind::Suspended(old), new) => { - // - self.replace_and_create_many_with_many([old_node], [new_node]); - } - // a node that was once real is now suspended - (old, VNodeKind::Suspended(_)) => { - // - self.replace_and_create_many_with_many([old_node], [new_node]); - } - } - } - - // Emit instructions to create the given virtual node. - // - // The change list stack may have any shape upon entering this function: - // - // [...] - // - // When this function returns, the new node is on top of the change list stack: - // - // [... node] - pub fn create_vnode(&mut self, node: &'bump VNode<'bump>) -> CreateMeta { - match &node.kind { - VNodeKind::Text(text) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_text_node(text.text, real_id); - text.dom_id.set(Some(real_id)); - CreateMeta::new(text.is_static, 1) - } - - VNodeKind::Anchor(anchor) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_placeholder(real_id); - anchor.dom_id.set(Some(real_id)); - CreateMeta::new(false, 1) - } - - VNodeKind::Element(el) => { - // we have the potential to completely eliminate working on this node in the future(!) - // - // This can only be done if all of the elements properties (attrs, children, listeners, etc) are static - // While creating these things, keep track if we can memoize this element. - // At the end, we'll set this flag on the element to skip it - let mut is_static: bool = true; - - let VElement { - tag_name, - listeners, - attributes, - children, - namespace, - static_attrs: _, - static_children: _, - static_listeners: _, - dom_id, - } = el; - - let real_id = self.vdom.reserve_node(); - self.edit_create_element(tag_name, *namespace, real_id); - dom_id.set(Some(real_id)); - - let cur_scope = self.current_scope().unwrap(); - - listeners.iter().for_each(|listener| { - self.fix_listener(listener); - listener.mounted_node.set(Some(real_id)); - self.edit_new_event_listener(listener, cur_scope.clone()); - - // if the node has an event listener, then it must be visited ? - is_static = false; - }); - - for attr in *attributes { - is_static = is_static && attr.is_static; - self.edit_set_attribute(attr); - } - - // Fast path: if there is a single text child, it is faster to - // create-and-append the text node all at once via setting the - // parent's `textContent` in a single change list instruction than - // to emit three instructions to (1) create a text node, (2) set its - // text content, and finally (3) append the text node to this - // parent. - // - // Notice: this is a web-specific optimization and may be changed in the future - // - // TODO move over - // if children.len() == 1 { - // if let VNodeKind::Text(text) = &children[0].kind { - // self.set_text(text.text); - // return CreateMeta::new(is_static, 1); - // } - // } - - for child in *children { - let child_meta = self.create_vnode(child); - is_static = is_static && child_meta.is_static; - - // append whatever children were generated by this call - self.edit_append_children(child_meta.added_to_stack); - } - - CreateMeta::new(is_static, 1) - } - - VNodeKind::Component(vcomponent) => { - let caller = vcomponent.caller.clone(); - - let parent_idx = self.scope_stack.last().unwrap().clone(); - - // Insert a new scope into our component list - let new_idx = self.vdom.insert_scope_with_key(|new_idx| { - let parent_scope = self.get_scope(&parent_idx).unwrap(); - let height = parent_scope.height + 1; - Scope::new( - caller, - new_idx, - Some(parent_idx), - height, - ScopeChildren(vcomponent.children), - self.vdom.clone(), - ) - }); - - // Actually initialize the caller's slot with the right address - vcomponent.ass_scope.set(Some(new_idx)); - - if !vcomponent.can_memoize { - let cur_scope = self.get_scope_mut(&parent_idx).unwrap(); - let extended = *vcomponent as *const VComponent; - let extended: *const VComponent<'static> = - unsafe { std::mem::transmute(extended) }; - cur_scope.borrowed_props.borrow_mut().push(extended); - } - - // TODO: - // add noderefs to current noderef list Noderefs - // add effects to current effect list Effects - - let new_component = self.get_scope_mut(&new_idx).unwrap(); - - // Run the scope for one iteration to initialize it - match new_component.run_scope() { - Ok(_) => { - // all good, new nodes exist - } - Err(err) => { - // failed to run. this is the first time the component ran, and it failed - // we manually set its head node to an empty fragment - panic!("failing components not yet implemented"); - } - } - - // 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); - - // Run the creation algorithm with this scope on the stack - 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. - self.seen_scopes.insert(new_idx); - - CreateMeta::new(vcomponent.is_static, meta.added_to_stack) - } - - // Fragments are the only nodes that can contain dynamic content (IE through curlies or iterators). - // We can never ignore their contents, so the prescence of a fragment indicates that we need always diff them. - // Fragments will just put all their nodes onto the stack after creation - VNodeKind::Fragment(frag) => self.create_children(frag.children), - - VNodeKind::Suspended(VSuspended { node: real_node }) => { - let id = self.vdom.reserve_node(); - self.edit_create_placeholder(id); - real_node.set(Some(id)); - CreateMeta::new(false, 1) - } - } - } - - fn create_children(&mut self, children: &'bump [VNode<'bump>]) -> CreateMeta { - let mut is_static = true; - let mut added_to_stack = 0; - - // add them backwards - for child in children.iter().rev() { - let child_meta = self.create_vnode(child); - is_static = is_static && child_meta.is_static; - added_to_stack += child_meta.added_to_stack; - } - - CreateMeta { - is_static, - added_to_stack, - } - } - - /// Destroy a scope and all of its descendents. - /// - /// Calling this will run the destuctors on all hooks in the tree. - /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. - fn destroy_scopes(&mut self, old_scope: ScopeId) { - let mut nodes_to_delete = vec![old_scope]; - let mut scopes_to_explore = vec![old_scope]; - - // explore the scope tree breadth first - while let Some(scope_id) = scopes_to_explore.pop() { - // If we're planning on deleting this node, then we don't need to both rendering it - self.seen_scopes.insert(scope_id); - let scope = self.get_scope(&scope_id).unwrap(); - for child in scope.descendents.borrow().iter() { - // Add this node to be explored - scopes_to_explore.push(child.clone()); - - // Also add it for deletion - nodes_to_delete.push(child.clone()); - } - } - - // Delete all scopes that we found as part of this subtree - for node in nodes_to_delete { - log::debug!("Removing scope {:#?}", node); - let _scope = self.vdom.try_remove(node).unwrap(); - // do anything we need to do to delete the scope - // I think we need to run the destructors on the hooks - // TODO - } - } - - // 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. - fn diff_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { - const IS_EMPTY: bool = true; - const IS_NOT_EMPTY: bool = false; - - 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) => { - let meta = self.create_children(new); - self.edit_append_children(meta.added_to_stack); - } - - // Completely removing old nodes and putting an anchor in its place - // no anchor (old has nodes) and the new is empty - // remove all the old nodes - (IS_NOT_EMPTY, IS_EMPTY) => { - for node in old { - self.remove_vnode(node); - } - } - - (IS_NOT_EMPTY, IS_NOT_EMPTY) => { - let first_old = &old[0]; - let first_new = &new[0]; - - match (&first_old.kind, &first_new.kind) { - // Anchors can only appear in empty fragments - (VNodeKind::Anchor(old_anchor), VNodeKind::Anchor(new_anchor)) => { - old_anchor.dom_id.set(new_anchor.dom_id.get()); - } - - // Replace the anchor with whatever new nodes are coming down the pipe - (VNodeKind::Anchor(anchor), _) => { - self.edit_push_root(anchor.dom_id.get().unwrap()); - let mut added = 0; - for el in new { - let meta = self.create_vnode(el); - added += meta.added_to_stack; - } - self.edit_replace_with(1, added); - } - - // Replace whatever nodes are sitting there with the anchor - (_, VNodeKind::Anchor(anchor)) => { - self.replace_and_create_many_with_many(old, [first_new]); - } - - // Use the complex diff algorithm to diff the nodes - _ => { - 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); - } - } - } - } - } - } - - // 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: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { - if cfg!(debug_assertions) { - let mut keys = fxhash::FxHashSet::default(); - let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { - 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. - // - // TODO: just inline this - let shared_prefix_count = match self.diff_keyed_prefix(old, new) { - KeyedPrefixResult::Finished => return, - KeyedPrefixResult::MoreWorkToDo(count) => count, - }; - - // Next, we find out how many of the nodes at the end of the children have - // the same key. We do _not_ diff them yet, since we want to emit the change - // list instructions such that they can be applied in a single pass over the - // DOM. Instead, we just save this information for later. - // - // `shared_suffix_count` is the count of how many nodes at the end of `new` - // and `old` share the same keys. - let shared_suffix_count = old[shared_prefix_count..] - .iter() - .rev() - .zip(new[shared_prefix_count..].iter().rev()) - .take_while(|&(old, new)| old.key == new.key) - .count(); - - let old_shared_suffix_start = old.len() - shared_suffix_count; - let new_shared_suffix_start = new.len() - shared_suffix_count; - - // 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. - self.diff_keyed_middle( - &old[shared_prefix_count..old_shared_suffix_start], - &new[shared_prefix_count..new_shared_suffix_start], - shared_prefix_count, - shared_suffix_count, - old_shared_suffix_start, - ); - - // Finally, diff the nodes at the end of `old` and `new` that share keys. - let old_suffix = &old[old_shared_suffix_start..]; - let new_suffix = &new[new_shared_suffix_start..]; - debug_assert_eq!(old_suffix.len(), new_suffix.len()); - if !old_suffix.is_empty() { - self.diff_keyed_suffix(old_suffix, new_suffix, new_shared_suffix_start) - } - } - - // Diff the prefix of children in `new` and `old` that share the same keys in - // the same order. - // - // The stack is empty upon entry. - fn diff_keyed_prefix( - &mut self, - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], - ) -> KeyedPrefixResult { - let mut shared_prefix_count = 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); - shared_prefix_count += 1; - } - - // If that was all of the old children, then create and append the remaining - // new children and we're finished. - if shared_prefix_count == old.len() { - // Load the last element - let last_node = self.find_last_element(new.last().unwrap()).direct_id(); - self.edit_push_root(last_node); - - // Create the new children and insert them after - let meta = self.create_children(&new[shared_prefix_count..]); - self.edit_insert_after(meta.added_to_stack); - - return KeyedPrefixResult::Finished; - } - - // And if that was all of the new children, then remove all of the remaining - // old children and we're finished. - if shared_prefix_count == new.len() { - self.remove_children(&old[shared_prefix_count..]); - return KeyedPrefixResult::Finished; - } - - KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) - } - - // Create the given children and append them to the parent node. - // - // The parent node must currently be on top of the change list stack: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { - for child in new { - let meta = self.create_vnode(child); - self.edit_append_children(meta.added_to_stack); - } - } - - // 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 state. - fn diff_keyed_middle( - &mut self, - old: &'bump [VNode<'bump>], - mut new: &'bump [VNode<'bump>], - shared_prefix_count: usize, - shared_suffix_count: usize, - old_shared_suffix_start: usize, - ) { - // 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`. - // 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 - .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 - .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 - } - } - }) - .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. - if shared_suffix_count == 0 && shared_keys.is_empty() { - self.replace_and_create_many_with_many(old, new); - 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; - // } - - // The longest increasing subsequence within `new_index_to_old_index`. This - // is the longest sequence on DOM nodes in `old` that are relatively ordered - // correctly within `new`. We will leave these nodes in place in the DOM, - // and only move nodes that are not part of the LIS. This results in the - // maximum number of DOM nodes left in place, AKA the minimum number of DOM - // nodes moved. - let mut new_index_is_in_lis = FxHashSet::default(); - new_index_is_in_lis.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 new_index_is_in_lis, - |a, b| a < b, - &mut predecessors, - &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 { - let new_node = &new[*id]; - let key = new_node.key().unwrap(); - let old_index = old_key_to_old_index.get(&key).unwrap(); - let old_node = &old[*old_index]; - self.diff_node(old_node, new_node); - } - - // return the old node from the key - let load_old_node_from_lsi = |key| -> &VNode { - let old_index = old_key_to_old_index.get(key).unwrap(); - let old_node = &old[*old_index]; - old_node - }; - - let mut root = None; - let mut new_iter = new.iter().enumerate(); - for lis_id in &lis_in_order { - eprintln!("tracking {:?}", lis_id); - // this is the next milestone node we are working up to - 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; - - 'inner: loop { - let (next_id, next_new) = new_iter.next().unwrap(); - if next_id == *lis_id { - // we've reached the milestone, break this loop so we can step to the next milestone - // remember: we already diffed this node - eprintln!("breaking {:?}", next_id); - break 'inner; - } else { - let key = next_new.key().unwrap(); - eprintln!("found key {:?}", key); - if shared_keys.contains(&key) { - eprintln!("key is contained {:?}", key); - shared_keys.remove(key); - // diff the two nodes - let old_node = load_old_node_from_lsi(key); - self.diff_node(old_node, next_new); - - // now move all the nodes into the right spot - for child in RealChildIterator::new(next_new, self.vdom) { - let el = child.direct_id(); - self.edit_push_root(el); - self.edit_insert_before(1); - } - } else { - eprintln!("key is not contained {:?}", key); - // new node needs to be created - // insert it before the current milestone - let meta = self.create_vnode(next_new); - self.edit_insert_before(meta.added_to_stack); - } - } - } - - self.edit_pop(); - } - - let final_lis_node = root.unwrap(); - let final_el_node = self.find_last_element(final_lis_node); - let final_el = final_el_node.direct_id(); - self.edit_push_root(final_el); - - let mut last_iter = new.iter().rev().enumerate(); - let last_key = final_lis_node.key().unwrap(); - loop { - let (last_id, last_node) = last_iter.next().unwrap(); - let key = last_node.key().unwrap(); - - eprintln!("checking final nodes {:?}", key); - - if last_key == key { - eprintln!("breaking final nodes"); - break; - } - - if shared_keys.contains(&key) { - eprintln!("key is contained {:?}", key); - shared_keys.remove(key); - // diff the two nodes - let old_node = load_old_node_from_lsi(key); - self.diff_node(old_node, last_node); - - // now move all the nodes into the right spot - for child in RealChildIterator::new(last_node, self.vdom) { - let el = child.direct_id(); - self.edit_push_root(el); - self.edit_insert_after(1); - } - } else { - eprintln!("key is not contained {:?}", key); - // new node needs to be created - // insert it before the current milestone - let meta = self.create_vnode(last_node); - self.edit_insert_after(meta.added_to_stack); - } - } - self.edit_pop(); - } - - // Diff the suffix of keyed children that share the same keys in the same order. - // - // The parent must be on the change list stack when we enter this function: - // - // [... parent] - // - // When this function exits, the change list stack remains the same. - fn diff_keyed_suffix( - &mut self, - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], - new_shared_suffix_start: usize, - ) { - debug_assert_eq!(old.len(), new.len()); - debug_assert!(!old.is_empty()); - - for (old_child, new_child) in old.iter().zip(new.iter()) { - self.diff_node(old_child, new_child); - } - } - - // 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. - 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()); - debug_assert!(!old.is_empty()); - - match old.len().cmp(&new.len()) { - // old.len > new.len -> removing some nodes - Ordering::Greater => { - // diff them together - for (new_child, old_child) in new.iter().zip(old.iter()) { - self.diff_node(old_child, new_child); - } - - // todo: we would emit fewer instructions if we just did a replace many - // remove whatever is still dangling - for item in &old[new.len()..] { - for i in RealChildIterator::new(item, self.vdom) { - self.edit_push_root(i.direct_id()); - self.edit_remove(); - } - } - } - - // old.len < new.len -> adding some nodes - // this is wrong in the case where we're diffing fragments - // - // we need to save the last old element and then replace it with all the new ones - Ordering::Less => { - // Add the new elements to the last old element while it still exists - let last = self.find_last_element(old.last().unwrap()); - self.edit_push_root(last.direct_id()); - - // create the rest and insert them - let meta = self.create_children(&new[old.len()..]); - self.edit_insert_after(meta.added_to_stack); - - self.edit_pop(); - - // diff the rest - 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).await; - } - } - } - } - - // ====================== - // Support methods - // ====================== - // Remove all of a node's children. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { - // debug_assert!(self.traversal_is_committed()); - log::debug!("REMOVING CHILDREN"); - for _child in old { - // registry.remove_subtree(child); - } - // Fast way to remove all children: set the node's textContent to an empty - // string. - todo!() - // self.set_inner_text(""); - } - // Remove the current child and all of its following siblings. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent child] - // - // After the function returns, the child is no longer on the change list stack: - // - // [... parent] - fn remove_children(&mut self, old: &'bump [VNode<'bump>]) { - self.replace_and_create_many_with_many(old, None) - } - - fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { - let mut search_node = Some(vnode); - - loop { - let node = search_node.take().unwrap(); - match &node.kind { - // the ones that have a direct id - VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_) - | VNodeKind::Suspended(_) => break node, - - VNodeKind::Fragment(frag) => { - search_node = frag.children.last(); - } - VNodeKind::Component(el) => { - let scope_id = el.ass_scope.get().unwrap(); - let scope = self.get_scope(&scope_id).unwrap(); - search_node = Some(scope.root()); - } - } - } - } - - fn find_first_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { - let mut search_node = Some(vnode); - - loop { - let node = search_node.take().unwrap(); - match &node.kind { - // the ones that have a direct id - VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_) - | VNodeKind::Suspended(_) => break node, - - VNodeKind::Fragment(frag) => { - search_node = Some(&frag.children[0]); - } - VNodeKind::Component(el) => { - let scope_id = el.ass_scope.get().unwrap(); - let scope = self.get_scope(&scope_id).unwrap(); - search_node = Some(scope.root()); - } - } - } - } - - fn remove_child(&mut self, node: &'bump VNode<'bump>) { - self.replace_and_create_many_with_many(Some(node), None); - } - - /// Remove all the old nodes and replace them with newly created new nodes. - /// - /// The new nodes *will* be created - don't create them yourself! - fn replace_and_create_many_with_many( - &mut self, - old_nodes: impl IntoIterator>, - new_nodes: impl IntoIterator>, - ) { - let mut nodes_to_replace = Vec::new(); - let mut nodes_to_search = old_nodes.into_iter().collect::>(); - let mut scopes_obliterated = Vec::new(); - while let Some(node) = nodes_to_search.pop() { - match &node.kind { - // the ones that have a direct id return immediately - VNodeKind::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNodeKind::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNodeKind::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNodeKind::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), - - // Fragments will either have a single anchor or a list of children - VNodeKind::Fragment(frag) => { - for child in frag.children { - nodes_to_search.push(child); - } - } - - // Components can be any of the nodes above - // However, we do need to track which components need to be removed - VNodeKind::Component(el) => { - let scope_id = el.ass_scope.get().unwrap(); - let scope = self.get_scope(&scope_id).unwrap(); - let root = scope.root(); - nodes_to_search.push(root); - scopes_obliterated.push(scope_id); - } - } - // TODO: enable internal garabge collection - // self.create_garbage(node); - } - - let n = nodes_to_replace.len(); - for node in nodes_to_replace { - self.edit_push_root(node); - } - - let mut nodes_created = 0; - for node in new_nodes { - let meta = self.create_vnode(node); - nodes_created += meta.added_to_stack; - } - - // if 0 nodes are created, then it gets interperted as a deletion - self.edit_replace_with(n as u32, nodes_created); - - // obliterate! - for scope in scopes_obliterated { - self.destroy_scopes(scope); - } - } - - fn create_garbage(&mut self, node: &'bump VNode<'bump>) { - match self.current_scope().and_then(|id| self.get_scope(&id)) { - Some(scope) => { - let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) }; - scope.pending_garbage.borrow_mut().push(garbage); - } - None => { - log::info!("No scope to collect garbage into") - } - } - } - - fn immediately_dispose_garabage(&mut self, node: ElementId) { - self.vdom.collect_garbage(node) - } - - fn replace_node_with_node( - &mut self, - anchor: ElementId, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, - ) { - self.edit_push_root(anchor); - let meta = self.create_vnode(new_node); - self.edit_replace_with(1, meta.added_to_stack); - self.create_garbage(old_node); - self.edit_pop(); - } - - fn remove_vnode(&mut self, node: &'bump VNode<'bump>) { - match &node.kind { - VNodeKind::Text(el) => self.immediately_dispose_garabage(node.direct_id()), - VNodeKind::Element(el) => { - self.immediately_dispose_garabage(node.direct_id()); - for child in el.children { - self.remove_vnode(&child); - } - } - VNodeKind::Anchor(a) => { - // - } - VNodeKind::Fragment(frag) => { - for child in frag.children { - self.remove_vnode(&child); - } - } - VNodeKind::Component(el) => { - // - // self.destroy_scopes(old_scope) - } - VNodeKind::Suspended(_) => todo!(), - } - } - - fn current_scope(&self) -> Option { - self.scope_stack.last().map(|f| f.clone()) - } - - fn fix_listener<'a>(&mut self, listener: &'a Listener<'a>) { - let scope_id = self.current_scope(); - if let Some(scope_id) = scope_id { - let scope = self.get_scope(&scope_id).unwrap(); - let mut queue = scope.listeners.borrow_mut(); - let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; - queue.push(long_listener as *const _) - } - } - - pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'bump mut Scope> { - // ensure we haven't seen this scope before - // if we have, then we're trying to alias it, which is not allowed - debug_assert!(!self.seen_scopes.contains(id)); - - unsafe { self.vdom.get_scope_mut(*id) } - } - pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'bump Scope> { - // ensure we haven't seen this scope before - // if we have, then we're trying to alias it, which is not allowed - unsafe { self.vdom.get_scope(*id) } - } - - // Navigation - pub(crate) fn edit_push_root(&mut self, root: ElementId) { - let id = root.as_u64(); - self.mutations.edits.push(PushRoot { id }); - } - - pub(crate) fn edit_pop(&mut self) { - self.mutations.edits.push(PopRoot {}); - } - - // Add Nodes to the dom - // add m nodes from the stack - pub(crate) fn edit_append_children(&mut self, many: u32) { - self.mutations.edits.push(AppendChildren { many }); - } - - // replace the n-m node on the stack with the m nodes - // ends with the last element of the chain on the top of the stack - pub(crate) fn edit_replace_with(&mut self, n: u32, m: u32) { - self.mutations.edits.push(ReplaceWith { n, m }); - } - - pub(crate) fn edit_insert_after(&mut self, n: u32) { - self.mutations.edits.push(InsertAfter { n }); - } - - pub(crate) fn edit_insert_before(&mut self, n: u32) { - self.mutations.edits.push(InsertBefore { n }); - } - - // Remove Nodesfrom the dom - pub(crate) fn edit_remove(&mut self) { - self.mutations.edits.push(Remove); - } - - // Create - pub(crate) fn edit_create_text_node(&mut self, text: &'bump str, id: ElementId) { - let id = id.as_u64(); - self.mutations.edits.push(CreateTextNode { text, id }); - } - - pub(crate) fn edit_create_element( - &mut self, - tag: &'static str, - ns: Option<&'static str>, - id: ElementId, - ) { - let id = id.as_u64(); - match ns { - Some(ns) => self.mutations.edits.push(CreateElementNs { id, ns, tag }), - None => self.mutations.edits.push(CreateElement { id, tag }), - } - } - - // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom - pub(crate) fn edit_create_placeholder(&mut self, id: ElementId) { - let id = id.as_u64(); - self.mutations.edits.push(CreatePlaceholder { id }); - } - - // events - pub(crate) fn edit_new_event_listener(&mut self, listener: &Listener, scope: ScopeId) { - let Listener { - event, - mounted_node, - .. - } = listener; - - let element_id = mounted_node.get().unwrap().as_u64(); - - self.mutations.edits.push(NewEventListener { - scope, - event_name: event, - mounted_node_id: element_id, - }); - } - - pub(crate) fn edit_remove_event_listener(&mut self, event: &'static str) { - self.mutations.edits.push(RemoveEventListener { event }); - } - - // modify - pub(crate) fn edit_set_text(&mut self, text: &'bump str) { - self.mutations.edits.push(SetText { text }); - } - - pub(crate) fn edit_set_attribute(&mut self, attribute: &'bump Attribute) { - let Attribute { - name, - value, - is_static, - is_volatile, - namespace, - } = attribute; - // field: &'static str, - // value: &'bump str, - // ns: Option<&'static str>, - self.mutations.edits.push(SetAttribute { - field: name, - value, - ns: *namespace, - }); - } - - pub(crate) fn edit_set_attribute_ns( - &mut self, - attribute: &'bump Attribute, - namespace: &'bump str, - ) { - let Attribute { - name, - value, - is_static, - is_volatile, - // namespace, - .. - } = attribute; - // field: &'static str, - // value: &'bump str, - // ns: Option<&'static str>, - self.mutations.edits.push(SetAttribute { - field: name, - value, - ns: Some(namespace), - }); - } - - pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { - let name = attribute.name; - self.mutations.edits.push(RemoveAttribute { name }); - } -} - -// When we create new nodes, we need to propagate some information back up the call chain. -// This gives the caller some information on how to handle things like insertins, appending, and subtree discarding. -#[derive(Debug)] -pub struct CreateMeta { - pub is_static: bool, - pub added_to_stack: u32, -} - -impl CreateMeta { - fn new(is_static: bool, added_to_tack: u32) -> Self { - Self { - is_static, - added_to_stack: added_to_tack, - } - } -} - -enum KeyedPrefixResult { - // Fast path: we finished diffing all the children just by looking at the - // prefix of shared keys! - Finished, - // There is more diffing work to do. Here is a count of how many children at - // the beginning of `new` and `old` we already processed. - MoreWorkToDo(usize), -} - -fn find_first_real_node<'a>( - nodes: impl IntoIterator>, - scopes: &'a SharedResources, -) -> Option<&'a VNode<'a>> { - for node in nodes { - let mut iter = RealChildIterator::new(node, scopes); - if let Some(node) = iter.next() { - return Some(node); - } - } - - None -} - -/// This iterator iterates through a list of virtual children and only returns real children (Elements, Text, Anchors). -/// -/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like -/// "InsertBefore". -pub struct RealChildIterator<'a> { - scopes: &'a SharedResources, - - // Heuristcally we should never bleed into 4 completely nested fragments/components - // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane - // TODO: use const generics instead of the 4 estimation - stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 4]>, -} - -impl<'a> RealChildIterator<'a> { - pub fn new(starter: &'a VNode<'a>, scopes: &'a SharedResources) -> Self { - Self { - scopes, - stack: smallvec::smallvec![(0, starter)], - } - } - // keep the memory around - pub fn reset_with(&mut self, node: &'a VNode<'a>) { - self.stack.clear(); - self.stack.push((0, node)); - } -} - -impl<'a> Iterator for RealChildIterator<'a> { - type Item = &'a VNode<'a>; - - fn next(&mut self) -> Option<&'a VNode<'a>> { - let mut should_pop = false; - let mut returned_node: Option<&'a VNode<'a>> = None; - let mut should_push = None; - - while returned_node.is_none() { - if let Some((count, node)) = self.stack.last_mut() { - match &node.kind { - // We can only exit our looping when we get "real" nodes - // This includes fragments and components when they're empty (have a single root) - VNodeKind::Element(_) | VNodeKind::Text(_) => { - // We've recursed INTO an element/text - // We need to recurse *out* of it and move forward to the next - should_pop = true; - returned_node = Some(&*node); - } - - // If we get a fragment we push the next child - VNodeKind::Fragment(frag) => { - let subcount = *count as usize; - - if frag.children.len() == 0 { - should_pop = true; - returned_node = Some(&*node); - } - - if subcount >= frag.children.len() { - should_pop = true; - } else { - should_push = Some(&frag.children[subcount]); - } - } - // // If we get a fragment we push the next child - // VNodeKind::Fragment(frag) => { - // let subcount = *count as usize; - - // if frag.children.len() == 0 { - // should_pop = true; - // returned_node = Some(&*node); - // } - - // if subcount >= frag.children.len() { - // should_pop = true; - // } else { - // should_push = Some(&frag.children[subcount]); - // } - // } - - // Immediately abort suspended nodes - can't do anything with them yet - VNodeKind::Suspended(node) => { - // VNodeKind::Suspended => should_pop = true, - todo!() - } - - VNodeKind::Anchor(a) => { - todo!() - } - - // For components, we load their root and push them onto the stack - VNodeKind::Component(sc) => { - let scope = - unsafe { self.scopes.get_scope(sc.ass_scope.get().unwrap()) }.unwrap(); - // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); - - // Simply swap the current node on the stack with the root of the component - *node = scope.frames.fin_head(); - } - } - } else { - // If there's no more items on the stack, we're done! - return None; - } - - if should_pop { - self.stack.pop(); - if let Some((id, _)) = self.stack.last_mut() { - *id += 1; - } - should_pop = false; - } - - if let Some(push) = should_push { - self.stack.push((0, push)); - should_push = None; - } - } - - returned_node - } -} - -fn compare_strs(a: &str, b: &str) -> bool { - // Check by pointer, optimizing for static strs - if !std::ptr::eq(a, b) { - // If the pointers are different then check by value - a == b - } else { - true - } -} - -struct DfsIterator<'a> { - idx: usize, - node: Option<(&'a VNode<'a>, &'a VNode<'a>)>, - nodes: Option<(&'a [VNode<'a>], &'a [VNode<'a>])>, -} -impl<'a> Iterator for DfsIterator<'a> { - type Item = (&'a VNode<'a>, &'a VNode<'a>); - - fn next(&mut self) -> Option { - todo!() - } -} diff --git a/packages/web/examples/basic.rs b/packages/web/examples/basic.rs index 0fbecec7c..451a29606 100644 --- a/packages/web/examples/basic.rs +++ b/packages/web/examples/basic.rs @@ -28,25 +28,6 @@ static APP: FC<()> = |cx| { cx.render(rsx! { div { button { onclick: move |_| count += 1, "add" } - div { - div { - div { - div { - div { - Fragment { - a {"wo"} - p {"wo"} - li {"wo"} - em {"wo"} - } - } - } - Child {} - } - Child {} - } - Child {} - } ul { {(0..*count).map(|f| rsx!{ li { "a - {f}" } @@ -54,6 +35,7 @@ static APP: FC<()> = |cx| { li { "c - {f}" } })} } + Child {} } }) }; diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 42850fb3e..429fa0259 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -29,6 +29,11 @@ impl RafLoop { let ric_closure: Closure = Closure::wrap(Box::new(move |v: JsValue| ric_sender.try_send(()).unwrap())); + // execute the polyfill for safari + Function::new_no_args(include_str!("./ricpolyfill.js")) + .call0(&JsValue::NULL) + .unwrap(); + Self { window: web_sys::window().unwrap(), raf_receiver, diff --git a/packages/web/src/ricpolyfill.js b/packages/web/src/ricpolyfill.js new file mode 100644 index 000000000..cd114f4ca --- /dev/null +++ b/packages/web/src/ricpolyfill.js @@ -0,0 +1,28 @@ +const requestIdleCallback = + (typeof self !== 'undefined' && + self.requestIdleCallback && + self.requestIdleCallback.bind(window)) || + function (cb) { + const start = Date.now(); + return setTimeout(() => { + cb({ + didTimeout: false, + timeRemaining: function () { + return Math.max(0, 50 - (Date.now() - start)); + }, + }); + }, 1); + }; + +const cancelIdleCallback = + (typeof self !== 'undefined' && + self.cancelIdleCallback && + self.cancelIdleCallback.bind(window)) || + function (id) { + return clearTimeout(id); + }; + +if (typeof window !== 'undefined') { + window.requestIdleCallback = requestIdleCallback; + window.cancelIdleCallback = cancelIdleCallback; +} From 3bf19d8106e0cca7749b7346e9b0adfce654d782 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 25 Aug 2021 10:49:18 -0400 Subject: [PATCH 29/53] wip: more work on web this commit adds complete event support for web-sys but with a few hole --- Cargo.toml | 15 +- examples/web_tick.rs | 143 +++++++++++ packages/core/Cargo.toml | 2 +- packages/core/src/arena.rs | 20 +- packages/core/src/diff.rs | 14 +- packages/core/src/events.rs | 43 ++-- packages/core/src/scheduler.rs | 86 ++++--- packages/core/src/virtual_dom.rs | 9 +- packages/web/Cargo.toml | 2 +- packages/web/src/dom.rs | 264 +++---------------- packages/web/src/events.rs | 418 +++++++++++++++++++++++++++++++ packages/web/src/lib.rs | 4 + packages/web/src/ric_raf.rs | 44 +--- 13 files changed, 709 insertions(+), 355 deletions(-) create mode 100644 examples/web_tick.rs create mode 100644 packages/web/src/events.rs diff --git a/Cargo.toml b/Cargo.toml index 19f2fae71..1e1a592bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ dioxus-mobile = { path = "./packages/mobile", optional = true } [features] # core -default = ["core", "ssr"] +default = ["core", "ssr", "web"] core = ["macro", "hooks", "html"] macro = ["dioxus-core-macro"] hooks = ["dioxus-hooks"] @@ -56,12 +56,17 @@ gloo-timers = "0.2.1" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] gloo-timers = "0.2.1" -surf = { version = "2.2.0", default-features = false, features = [ - "wasm-client", -], git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm" } +wasm-logger = "0.2.0" +console_error_panic_hook = "0.1.6" +rand = { version = "0.8.4", features = ["small_rng"] } +wasm-bindgen = { version = "0.2.71", features = ["enable-interning"] } + +# surf = { version = "2.2.0", default-features = false, features = [ +# "wasm-client", +# ], git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm" } -[dependencies.getrandom] +[dev-dependencies.getrandom] version = "0.2" features = ["js"] diff --git a/examples/web_tick.rs b/examples/web_tick.rs new file mode 100644 index 000000000..8d58acfec --- /dev/null +++ b/examples/web_tick.rs @@ -0,0 +1,143 @@ +#![allow(non_upper_case_globals, non_snake_case)] +//! Example: Webview Renderer +//! ------------------------- +//! +//! This example shows how to use the dioxus_desktop crate to build a basic desktop application. +//! +//! Under the hood, the dioxus_desktop crate bridges a native Dioxus VirtualDom with a custom prebuit application running +//! in the webview runtime. Custom handlers are provided for the webview instance to consume patches and emit user events +//! into the native VDom instance. +//! +//! Currently, NodeRefs won't work properly, but all other event functionality will. + +use dioxus::prelude::*; + +fn main() { + // Setup logging + // wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + // console_error_panic_hook::set_once(); + for adj in ADJECTIVES { + wasm_bindgen::intern(adj); + } + for col in COLOURS { + wasm_bindgen::intern(col); + } + for no in NOUNS { + wasm_bindgen::intern(no); + } + wasm_bindgen::intern("col-md-1"); + wasm_bindgen::intern("col-md-6"); + wasm_bindgen::intern("glyphicon glyphicon-remove remove"); + wasm_bindgen::intern("remove"); + wasm_bindgen::intern("dioxus"); + wasm_bindgen::intern("lbl"); + wasm_bindgen::intern("true"); + + dioxus::web::launch(App, |c| c); +} + +static App: FC<()> = |cx| { + // let mut count = use_state(cx, || 0); + + let mut rng = SmallRng::from_entropy(); + let rows = (0..1_000).map(|f| { + let label = Label::new(&mut rng); + rsx! { + Row { + row_id: f, + label: label + } + } + }); + cx.render(rsx! { + table { + tbody { + {rows} + } + } + }) + // cx.render(rsx! { + // div { + // // h1 { "Hifive counter: {count}" } + // // {cx.children()} + // // button { onclick: move |_| count += 1, "Up high!" } + // // button { onclick: move |_| count -= 1, "Down low!" } + // {(0..1000).map(|i| rsx!{ div { "Count: {count}" } })} + // } + // }) +}; + +#[derive(PartialEq, Props)] +struct RowProps { + row_id: usize, + label: Label, +} +fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree { + let [adj, col, noun] = cx.label.0; + cx.render(rsx! { + tr { + td { class:"col-md-1", "{cx.row_id}" } + td { class:"col-md-1", onclick: move |_| { /* run onselect */ } + a { class: "lbl", "{adj}" "{col}" "{noun}" } + } + td { class: "col-md-1" + a { class: "remove", onclick: move |_| {/* remove */} + span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" } + } + } + td { class: "col-md-6" } + } + }) +} +use rand::prelude::*; + +#[derive(PartialEq)] +struct Label([&'static str; 3]); + +impl Label { + fn new(rng: &mut SmallRng) -> Self { + Label([ + ADJECTIVES.choose(rng).unwrap(), + COLOURS.choose(rng).unwrap(), + NOUNS.choose(rng).unwrap(), + ]) + } +} + +static ADJECTIVES: &[&str] = &[ + "pretty", + "large", + "big", + "small", + "tall", + "short", + "long", + "handsome", + "plain", + "quaint", + "clean", + "elegant", + "easy", + "angry", + "crazy", + "helpful", + "mushy", + "odd", + "unsightly", + "adorable", + "important", + "inexpensive", + "cheap", + "expensive", + "fancy", +]; + +static COLOURS: &[&str] = &[ + "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", + "orange", +]; + +static NOUNS: &[&str] = &[ + "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", + "pizza", "mouse", "keyboard", +]; diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 005afdd29..c55ddecd1 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -26,7 +26,7 @@ fxhash = "0.2.1" longest-increasing-subsequence = "0.1.0" # internall used -log = "0.4" +log = { verison = "0.4", features = ["release_max_level_off"] } futures-util = "0.3.15" diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 840cb34b8..9ed7a2277 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -90,17 +90,17 @@ impl SharedResources { let comps = unsafe { &*components.get() }; if let Some(scope) = comps.get(idx.0) { - todo!("implement immediates again") + // todo!("implement immediates again") + // + // queue - // .unbounded_send(EventTrigger::new( - // VirtualEvent::ScheduledUpdate { - // height: scope.height, - // }, - // idx, - // None, - // EventPriority::High, - // )) - // .expect("The event queu receiver should *never* be dropped"); + // .unbounded_send(EventTrigger::new( + // V + // idx, + // None, + // EventPriority::High, + // )) + // .expect("The event queu receiver should *never* be dropped"); } }) as Rc }; diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 1646219aa..4aeabd89b 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -138,14 +138,12 @@ impl<'bump> DiffMachine<'bump> { } // - pub async fn diff_scope(&mut self, id: ScopeId) -> Result<()> { - let component = self - .vdom - .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); - Ok(()) + pub async fn diff_scope(&mut self, id: ScopeId) { + if let Some(component) = self.vdom.get_scope_mut(id) { + let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + self.stack.push(DiffInstruction::DiffNode { new, old }); + self.work().await; + } } /// Progress the diffing for this "fiber" diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 5c56f953e..41868aa95 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -153,6 +153,7 @@ pub enum VirtualEvent { MouseEvent(on::MouseEvent), PointerEvent(on::PointerEvent), } + impl VirtualEvent { pub fn is_input_event(&self) -> bool { match self { @@ -215,7 +216,7 @@ pub mod on { #![allow(unused)] use bumpalo::boxed::Box as BumpBox; - use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; + use std::{any::Any, cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; use crate::{ innerlude::NodeFactory, @@ -563,6 +564,8 @@ pub mod on { } pub trait GenericEventInner { + /// Return a reference to the raw event. User will need to downcast the event to the right platform-specific type. + fn raw_event(&self) -> &dyn Any; /// Returns whether or not a specific event is a bubbling event fn bubbles(&self) -> bool; /// Sets or returns whether the event should propagate up the hierarchy or not @@ -571,14 +574,18 @@ pub mod on { fn cancelable(&self) -> bool; /// Returns whether the event is composed or not fn composed(&self) -> bool; - /// Returns the event's path - fn composed_path(&self) -> String; + + // Currently not supported because those no way we could possibly support it + // just cast the event to the right platform-specific type and return it + // /// Returns the event's path + // fn composed_path(&self) -> String; + /// Returns the element whose event listeners triggered the event fn current_target(&self); /// Returns whether or not the preventDefault method was called for the event fn default_prevented(&self) -> bool; /// Returns which phase of the event flow is currently being evaluated - fn event_phase(&self) -> usize; + fn event_phase(&self) -> u16; /// Returns whether or not an event is trusted fn is_trusted(&self) -> bool; /// Cancels the event if it is cancelable, meaning that the default action that belongs to the event will @@ -590,7 +597,7 @@ pub mod on { /// Returns the element that triggered the event fn target(&self); /// Returns the time (in milliseconds relative to the epoch) at which the event was created - fn time_stamp(&self) -> usize; + fn time_stamp(&self) -> f64; } pub trait ClipboardEventInner { @@ -688,8 +695,8 @@ pub mod on { pub trait PointerEventInner { // Mouse only fn alt_key(&self) -> bool; - fn button(&self) -> usize; - fn buttons(&self) -> usize; + fn button(&self) -> i16; + fn buttons(&self) -> u16; fn client_x(&self) -> i32; fn client_y(&self) -> i32; fn ctrl_key(&self) -> bool; @@ -699,12 +706,12 @@ pub mod on { fn screen_x(&self) -> i32; fn screen_y(&self) -> i32; fn shift_key(&self) -> bool; - fn get_modifier_state(&self, key_code: usize) -> bool; - fn pointer_id(&self) -> usize; - fn width(&self) -> usize; - fn height(&self) -> usize; - fn pressure(&self) -> usize; - fn tangential_pressure(&self) -> usize; + fn get_modifier_state(&self, key_code: &str) -> bool; + fn pointer_id(&self) -> i32; + fn width(&self) -> i32; + fn height(&self) -> i32; + fn pressure(&self) -> f32; + fn tangential_pressure(&self) -> f32; fn tilt_x(&self) -> i32; fn tilt_y(&self) -> i32; fn twist(&self) -> i32; @@ -719,7 +726,7 @@ pub mod on { fn ctrl_key(&self) -> bool; fn meta_key(&self) -> bool; fn shift_key(&self) -> bool; - fn get_modifier_state(&self, key_code: usize) -> bool; + fn get_modifier_state(&self, key_code: &str) -> bool; // changedTouches: DOMTouchList, // targetTouches: DOMTouchList, // touches: DOMTouchList, @@ -731,10 +738,10 @@ pub mod on { } pub trait WheelEventInner { - fn delta_mode(&self) -> i32; - fn delta_x(&self) -> i32; - fn delta_y(&self) -> i32; - fn delta_z(&self) -> i32; + fn delta_mode(&self) -> u32; + fn delta_x(&self) -> f64; + fn delta_y(&self) -> f64; + fn delta_z(&self) -> f64; } pub trait MediaEventInner {} diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index b8fc07f15..3c635da04 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -50,15 +50,13 @@ pub struct Scheduler { shared: SharedResources, - waypoints: VecDeque, - high_priorty: PriortySystem, medium_priority: PriortySystem, low_priority: PriortySystem, } pub enum FiberResult<'a> { - Done(&'a mut Mutations<'a>), + Done(Mutations<'a>), Interrupted, } @@ -75,7 +73,6 @@ impl Scheduler { garbage_scopes: HashSet::new(), current_priority: EventPriority::Low, - waypoints: VecDeque::new(), high_priorty: PriortySystem::new(), medium_priority: PriortySystem::new(), @@ -202,7 +199,9 @@ impl Scheduler { } pub fn has_work(&self) -> bool { - self.waypoints.len() > 0 + self.high_priorty.has_work() + || self.medium_priority.has_work() + || self.low_priority.has_work() } pub fn has_pending_garbage(&self) -> bool { @@ -219,10 +218,10 @@ impl Scheduler { } /// 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>>, - ) -> FiberResult { + pub async fn work_with_deadline<'a>( + &'a mut self, + deadline: &mut Pin>>, + ) -> FiberResult<'a> { // check if we need to elevate priority self.current_priority = match ( self.high_priorty.has_work(), @@ -234,11 +233,46 @@ impl Scheduler { (false, false, _) => EventPriority::Low, }; - let mut is_ready = || -> bool { (&mut deadline).now_or_never().is_some() }; + let mut machine = DiffMachine::new_headless(&self.shared); - // TODO: remove this unwrap - proprogate errors out - // self.get_current_fiber().work(is_ready).unwrap() - todo!() + let dirty_root = { + let dirty_roots = match self.current_priority { + EventPriority::High => &self.high_priorty.dirty_scopes, + EventPriority::Medium => &self.medium_priority.dirty_scopes, + EventPriority::Low => &self.low_priority.dirty_scopes, + }; + let mut height = 0; + let mut dirty_root = { + let root = dirty_roots.iter().next(); + if root.is_none() { + return FiberResult::Done(machine.mutations); + } + root.unwrap() + }; + + for root in dirty_roots { + if let Some(scope) = self.shared.get_scope(*root) { + if scope.height < height { + height = scope.height; + dirty_root = root; + } + } + } + dirty_root + }; + + match { + let fut = machine.diff_scope(*dirty_root).fuse(); + pin_mut!(fut); + + match futures_util::future::select(deadline, fut).await { + futures_util::future::Either::Left((deadline, work_fut)) => true, + futures_util::future::Either::Right((_, deadline_fut)) => false, + } + } { + true => FiberResult::Done(machine.mutations), + false => FiberResult::Interrupted, + } } // waits for a trigger, canceling early if the deadline is reached @@ -356,32 +390,6 @@ pub struct DirtyScope { start_tick: u32, } -/* -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. - -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, - - edits: Vec>, - - // 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]>, - - seen_scopes: HashSet, - - invalidate_scopes: HashSet, - - priority_level: EventPriority, -} - pub struct PriortySystem { pub pending_scopes: Vec, pub dirty_scopes: HashSet, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 3b95aec76..76a283154 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -298,11 +298,11 @@ impl VirtualDom { } // Create work from the pending event queue - self.scheduler.consume_pending_events(); + self.scheduler.consume_pending_events().unwrap(); // 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) { + match self.scheduler.work_with_deadline(&mut deadline).await { FiberResult::Done(mut mutations) => { committed_mutations.extend(&mut mutations); @@ -332,7 +332,10 @@ impl VirtualDom { true } - pub async fn wait_for_any_work(&self) {} + pub async fn wait_for_any_work(&mut self) { + let mut timeout = Box::pin(futures_util::future::pending().fuse()); + self.scheduler.wait_for_any_trigger(&mut timeout).await; + } } // TODO! diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index e02b10861..cc588eb91 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -14,9 +14,9 @@ js-sys = "0.3" wasm-bindgen = { version = "0.2.71", features = ["enable-interning"] } lazy_static = "1.4.0" wasm-bindgen-futures = "0.4.20" -wasm-logger = "0.2.0" log = "0.4.14" fxhash = "0.2.1" +wasm-logger = "0.2.0" console_error_panic_hook = "0.1.6" generational-arena = "0.2.8" wasm-bindgen-test = "0.3.21" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index d36e6e2d8..d48e8d187 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -1,7 +1,7 @@ -use std::{collections::HashMap, rc::Rc, sync::Arc}; +use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc}; use dioxus_core::{ - events::{EventTrigger, VirtualEvent}, + events::{on::GenericEventInner, EventTrigger, VirtualEvent}, mutations::NodeRefMutation, DomEdit, ElementId, ScopeId, }; @@ -9,7 +9,7 @@ use fxhash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{ window, CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, - HtmlOptionElement, Node, NodeList, + HtmlOptionElement, Node, NodeList, UiEvent, }; use crate::{nodeslab::NodeSlab, WebConfig}; @@ -436,284 +436,82 @@ impl Stack { } } -fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { +fn virtual_event_from_websys_event(event: web_sys::Event) -> VirtualEvent { + use crate::events::*; use dioxus_core::events::on::*; match event.type_().as_str() { "copy" | "cut" | "paste" => { - struct WebsysClipboardEvent(); - impl ClipboardEventInner for WebsysClipboardEvent {} - VirtualEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent()))) + VirtualEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent(event)))) } - "compositionend" | "compositionstart" | "compositionupdate" => { let evt: web_sys::CompositionEvent = event.clone().dyn_into().unwrap(); - struct WebsysCompositionEvent(web_sys::CompositionEvent); - impl CompositionEventInner for WebsysCompositionEvent { - fn data(&self) -> String { - todo!() - } - } VirtualEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt)))) } - "keydown" | "keypress" | "keyup" => { - struct Event(web_sys::KeyboardEvent); - impl KeyboardEventInner for Event { - fn alt_key(&self) -> bool { - self.0.alt_key() - } - fn char_code(&self) -> u32 { - self.0.char_code() - } - fn key(&self) -> String { - self.0.key() - } - - fn key_code(&self) -> KeyCode { - KeyCode::from_raw_code(self.0.key_code() as u8) - } - - fn ctrl_key(&self) -> bool { - self.0.ctrl_key() - } - - fn get_modifier_state(&self, key_code: &str) -> bool { - self.0.get_modifier_state(key_code) - } - - fn locale(&self) -> String { - todo!("Locale is currently not supported. :(") - } - - fn location(&self) -> usize { - self.0.location() as usize - } - - fn meta_key(&self) -> bool { - self.0.meta_key() - } - - fn repeat(&self) -> bool { - self.0.repeat() - } - - fn shift_key(&self) -> bool { - self.0.shift_key() - } - - fn which(&self) -> usize { - self.0.which() as usize - } - } - let evt: web_sys::KeyboardEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::KeyboardEvent(KeyboardEvent(Rc::new(Event(evt)))) + let evt: web_sys::KeyboardEvent = event.dyn_into().unwrap(); + VirtualEvent::KeyboardEvent(KeyboardEvent(Rc::new(WebsysKeyboardEvent(evt)))) } - "focus" | "blur" => { - struct Event(web_sys::FocusEvent); - impl FocusEventInner for Event {} - let evt: web_sys::FocusEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::FocusEvent(FocusEvent(Rc::new(Event(evt)))) + let evt: web_sys::FocusEvent = event.dyn_into().unwrap(); + VirtualEvent::FocusEvent(FocusEvent(Rc::new(WebsysFocusEvent(evt)))) } - "change" => { - // struct Event(web_sys::Event); - // impl GenericEventInner for Event { - // fn bubbles(&self) -> bool { - // todo!() - // } - - // fn cancel_bubble(&self) { - // todo!() - // } - - // fn cancelable(&self) -> bool { - // todo!() - // } - - // fn composed(&self) -> bool { - // todo!() - // } - - // fn composed_path(&self) -> String { - // todo!() - // } - - // fn current_target(&self) { - // todo!() - // } - - // fn default_prevented(&self) -> bool { - // todo!() - // } - - // fn event_phase(&self) -> usize { - // todo!() - // } - - // fn is_trusted(&self) -> bool { - // todo!() - // } - - // fn prevent_default(&self) { - // todo!() - // } - - // fn stop_immediate_propagation(&self) { - // todo!() - // } - - // fn stop_propagation(&self) { - // todo!() - // } - - // fn target(&self) { - // todo!() - // } - - // fn time_stamp(&self) -> usize { - // todo!() - // } - // } - // let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ"); - // VirtualEvent::Event(GenericEvent(Rc::new(Event(evt)))) - todo!() + let evt = event.dyn_into().unwrap(); + VirtualEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) } - "input" | "invalid" | "reset" | "submit" => { - // is a special react events - let evt: web_sys::InputEvent = event.clone().dyn_into().expect("wrong event type"); - let this: web_sys::EventTarget = evt.target().unwrap(); - - let value = (&this) - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| input.value()) - .or_else(|| { - (&this) - .dyn_ref() - .map(|input: &web_sys::HtmlTextAreaElement| input.value()) - }) - .or_else(|| { - (&this) - .dyn_ref::() - .unwrap() - .text_content() - }) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - - todo!() - // VirtualEvent::FormEvent(FormEvent { value }) + let evt: web_sys::InputEvent = event.clone().dyn_into().unwrap(); + VirtualEvent::FormEvent(FormEvent(Rc::new(WebsysFormEvent(evt)))) } - "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { let evt: web_sys::MouseEvent = event.clone().dyn_into().unwrap(); - - #[derive(Debug)] - pub struct CustomMouseEvent(web_sys::MouseEvent); - impl dioxus_core::events::on::MouseEventInner for CustomMouseEvent { - fn alt_key(&self) -> bool { - self.0.alt_key() - } - fn button(&self) -> i16 { - self.0.button() - } - fn buttons(&self) -> u16 { - self.0.buttons() - } - fn client_x(&self) -> i32 { - self.0.client_x() - } - fn client_y(&self) -> i32 { - self.0.client_y() - } - fn ctrl_key(&self) -> bool { - self.0.ctrl_key() - } - fn meta_key(&self) -> bool { - self.0.meta_key() - } - fn page_x(&self) -> i32 { - self.0.page_x() - } - fn page_y(&self) -> i32 { - self.0.page_y() - } - fn screen_x(&self) -> i32 { - self.0.screen_x() - } - fn screen_y(&self) -> i32 { - self.0.screen_y() - } - fn shift_key(&self) -> bool { - self.0.shift_key() - } - - // yikes - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values - fn get_modifier_state(&self, key_code: &str) -> bool { - self.0.get_modifier_state(key_code) - } - } - VirtualEvent::MouseEvent(MouseEvent(Rc::new(CustomMouseEvent(evt)))) + VirtualEvent::MouseEvent(MouseEvent(Rc::new(WebsysMouseEvent(evt)))) } - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { let evt: web_sys::PointerEvent = event.clone().dyn_into().unwrap(); - todo!() + VirtualEvent::PointerEvent(PointerEvent(Rc::new(WebsysPointerEvent(evt)))) } - "select" => { - // let evt: web_sys::SelectionEvent = event.clone().dyn_into().unwrap(); - // not required to construct anything special beyond standard event stuff - todo!() + let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); + VirtualEvent::SelectionEvent(SelectionEvent(Rc::new(WebsysGenericUiEvent(evt)))) } - "touchcancel" | "touchend" | "touchmove" | "touchstart" => { let evt: web_sys::TouchEvent = event.clone().dyn_into().unwrap(); - todo!() + VirtualEvent::TouchEvent(TouchEvent(Rc::new(WebsysTouchEvent(evt)))) } - "scroll" => { - // let evt: web_sys::UIEvent = event.clone().dyn_into().unwrap(); - todo!() + let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); + VirtualEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) } - "wheel" => { let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap(); - todo!() + VirtualEvent::WheelEvent(WheelEvent(Rc::new(WebsysWheelEvent(evt)))) } "animationstart" | "animationend" | "animationiteration" => { let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap(); - todo!() + VirtualEvent::AnimationEvent(AnimationEvent(Rc::new(WebsysAnimationEvent(evt)))) } - "transitionend" => { let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap(); - todo!() + VirtualEvent::TransitionEvent(TransitionEvent(Rc::new(WebsysTransitionEvent(evt)))) } - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate" | "volumechange" | "waiting" => { - // not required to construct anything special beyond standard event stuff - - // let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap(); - // let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap(); - todo!() + let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); + VirtualEvent::MediaEvent(MediaEvent(Rc::new(WebsysMediaEvent(evt)))) } - "toggle" => { - // not required to construct anything special beyond standard event stuff (target) - - // let evt: web_sys::ToggleEvent = event.clone().dyn_into().unwrap(); - todo!() + let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); + VirtualEvent::ToggleEvent(ToggleEvent(Rc::new(WebsysToggleEvent(evt)))) } _ => { - todo!() + let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); + VirtualEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) } } } @@ -763,7 +561,7 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { // let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into(); log::debug!("Triggered scope is {:#?}", triggered_scope); Ok(EventTrigger::new( - virtual_event_from_websys_event(event), + virtual_event_from_websys_event(event.clone()), ScopeId(triggered_scope as usize), Some(ElementId(real_id as usize)), dioxus_core::events::EventPriority::High, diff --git a/packages/web/src/events.rs b/packages/web/src/events.rs new file mode 100644 index 000000000..3a84f080c --- /dev/null +++ b/packages/web/src/events.rs @@ -0,0 +1,418 @@ +//! Ported events into Dioxus Synthetic Event system +//! +//! event porting is pretty boring, sorry. + +use dioxus_core::events::on::*; +use wasm_bindgen::JsCast; +use web_sys::{Event, UiEvent}; + +/// All events implement the generic event type - they're all UI events +trait WebsysGenericEvent { + fn as_ui_event(&self) -> &UiEvent; +} + +impl GenericEventInner for &dyn WebsysGenericEvent { + /// On WebSys, this returns an &UiEvent which can be casted via dyn_ref into the correct sub type. + fn raw_event(&self) -> &dyn std::any::Any { + self.as_ui_event() + } + + fn bubbles(&self) -> bool { + self.as_ui_event().bubbles() + } + + fn cancel_bubble(&self) { + self.as_ui_event().cancel_bubble(); + } + + fn cancelable(&self) -> bool { + self.as_ui_event().cancelable() + } + + fn composed(&self) -> bool { + self.as_ui_event().composed() + } + + fn current_target(&self) { + if cfg!(debug_assertions) { + todo!("Current target does not return anything useful.\nPlease try casting the event directly."); + } + // self.as_ui_event().current_target(); + } + + fn default_prevented(&self) -> bool { + self.as_ui_event().default_prevented() + } + + fn event_phase(&self) -> u16 { + self.as_ui_event().event_phase() + } + + fn is_trusted(&self) -> bool { + self.as_ui_event().is_trusted() + } + + fn prevent_default(&self) { + self.as_ui_event().prevent_default() + } + + fn stop_immediate_propagation(&self) { + self.as_ui_event().stop_immediate_propagation() + } + + fn stop_propagation(&self) { + self.as_ui_event().stop_propagation() + } + + fn target(&self) { + todo!() + } + + fn time_stamp(&self) -> f64 { + self.as_ui_event().time_stamp() + } +} + +macro_rules! implement_generic_event { + ( + $($event:ident),* + ) => { + $( + impl WebsysGenericEvent for $event { + fn as_ui_event(&self) -> &UiEvent { + self.0.dyn_ref().unwrap() + } + } + )* + }; +} + +implement_generic_event! { + WebsysClipboardEvent, + WebsysCompositionEvent, + WebsysKeyboardEvent, + WebsysGenericUiEvent, + WebsysFocusEvent, + WebsysFormEvent, + WebsysMouseEvent, + WebsysPointerEvent, + WebsysWheelEvent, + WebsysAnimationEvent, + WebsysTransitionEvent, + WebsysTouchEvent, + WebsysMediaEvent, + WebsysToggleEvent +} + +// unfortunately, currently experimental, and web_sys needs to be configured to use it :>( +pub struct WebsysClipboardEvent(pub Event); + +impl ClipboardEventInner for WebsysClipboardEvent {} + +pub struct WebsysCompositionEvent(pub web_sys::CompositionEvent); + +impl CompositionEventInner for WebsysCompositionEvent { + fn data(&self) -> String { + self.0.data().unwrap_or_else(|| String::new()) + } +} + +pub struct WebsysKeyboardEvent(pub web_sys::KeyboardEvent); +impl KeyboardEventInner for WebsysKeyboardEvent { + fn alt_key(&self) -> bool { + self.0.alt_key() + } + fn char_code(&self) -> u32 { + self.0.char_code() + } + fn key(&self) -> String { + self.0.key() + } + + fn key_code(&self) -> KeyCode { + KeyCode::from_raw_code(self.0.key_code() as u8) + } + + fn ctrl_key(&self) -> bool { + self.0.ctrl_key() + } + + fn get_modifier_state(&self, key_code: &str) -> bool { + self.0.get_modifier_state(key_code) + } + + fn locale(&self) -> String { + if cfg!(debug_assertions) { + todo!("Locale is currently not supported. :(") + } else { + String::from("en-US") + } + } + + fn location(&self) -> usize { + self.0.location() as usize + } + + fn meta_key(&self) -> bool { + self.0.meta_key() + } + + fn repeat(&self) -> bool { + self.0.repeat() + } + + fn shift_key(&self) -> bool { + self.0.shift_key() + } + + fn which(&self) -> usize { + self.0.which() as usize + } +} + +pub struct WebsysGenericUiEvent(pub UiEvent); +impl UIEventInner for WebsysGenericUiEvent { + fn detail(&self) -> i32 { + todo!() + } +} +impl SelectionEventInner for WebsysGenericUiEvent {} + +pub struct WebsysFocusEvent(pub web_sys::FocusEvent); +impl FocusEventInner for WebsysFocusEvent {} + +pub struct WebsysFormEvent(pub web_sys::InputEvent); +impl FormEventInner for WebsysFormEvent { + // technically a controlled component, so we need to manually grab out the target data + fn value(&self) -> String { + let this: web_sys::EventTarget = self.0.target().unwrap(); + (&this) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| input.value()) + .or_else(|| { + this + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + .or_else(|| { + this + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener") + } +} + +pub struct WebsysMouseEvent(pub web_sys::MouseEvent); +impl MouseEventInner for WebsysMouseEvent { + fn alt_key(&self) -> bool { + self.0.alt_key() + } + fn button(&self) -> i16 { + self.0.button() + } + fn buttons(&self) -> u16 { + self.0.buttons() + } + fn client_x(&self) -> i32 { + self.0.client_x() + } + fn client_y(&self) -> i32 { + self.0.client_y() + } + fn ctrl_key(&self) -> bool { + self.0.ctrl_key() + } + fn meta_key(&self) -> bool { + self.0.meta_key() + } + fn page_x(&self) -> i32 { + self.0.page_x() + } + fn page_y(&self) -> i32 { + self.0.page_y() + } + fn screen_x(&self) -> i32 { + self.0.screen_x() + } + fn screen_y(&self) -> i32 { + self.0.screen_y() + } + fn shift_key(&self) -> bool { + self.0.shift_key() + } + + // yikes + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + fn get_modifier_state(&self, key_code: &str) -> bool { + self.0.get_modifier_state(key_code) + } +} + +pub struct WebsysPointerEvent(pub web_sys::PointerEvent); +impl PointerEventInner for WebsysPointerEvent { + fn alt_key(&self) -> bool { + self.0.alt_key() + } + fn button(&self) -> i16 { + self.0.button() + } + fn buttons(&self) -> u16 { + self.0.buttons() + } + fn client_x(&self) -> i32 { + self.0.client_x() + } + fn client_y(&self) -> i32 { + self.0.client_y() + } + fn ctrl_key(&self) -> bool { + self.0.ctrl_key() + } + fn meta_key(&self) -> bool { + self.0.meta_key() + } + fn page_x(&self) -> i32 { + self.0.page_x() + } + fn page_y(&self) -> i32 { + self.0.page_y() + } + fn screen_x(&self) -> i32 { + self.0.screen_x() + } + fn screen_y(&self) -> i32 { + self.0.screen_y() + } + fn shift_key(&self) -> bool { + self.0.shift_key() + } + + // yikes + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + fn get_modifier_state(&self, key_code: &str) -> bool { + self.0.get_modifier_state(key_code) + } + + fn pointer_id(&self) -> i32 { + self.0.pointer_id() + } + + fn width(&self) -> i32 { + self.0.width() + } + + fn height(&self) -> i32 { + self.0.height() + } + + fn pressure(&self) -> f32 { + self.0.pressure() + } + + fn tangential_pressure(&self) -> f32 { + self.0.tangential_pressure() + } + + fn tilt_x(&self) -> i32 { + self.0.tilt_x() + } + + fn tilt_y(&self) -> i32 { + self.0.tilt_y() + } + + fn twist(&self) -> i32 { + self.0.twist() + } + + fn pointer_type(&self) -> String { + self.0.pointer_type() + } + + fn is_primary(&self) -> bool { + self.0.is_primary() + } +} + +pub struct WebsysWheelEvent(pub web_sys::WheelEvent); +impl WheelEventInner for WebsysWheelEvent { + fn delta_mode(&self) -> u32 { + self.0.delta_mode() + } + + fn delta_x(&self) -> f64 { + self.0.delta_x() + } + + fn delta_y(&self) -> f64 { + self.0.delta_y() + } + + fn delta_z(&self) -> f64 { + self.0.delta_z() + } +} +pub struct WebsysAnimationEvent(pub web_sys::AnimationEvent); +impl AnimationEventInner for WebsysAnimationEvent { + fn animation_name(&self) -> String { + self.0.animation_name() + } + + fn pseudo_element(&self) -> String { + self.0.pseudo_element() + } + + fn elapsed_time(&self) -> f32 { + self.0.elapsed_time() + } +} + +pub struct WebsysTransitionEvent(pub web_sys::TransitionEvent); +impl TransitionEventInner for WebsysTransitionEvent { + fn property_name(&self) -> String { + self.0.property_name() + } + + fn pseudo_element(&self) -> String { + self.0.pseudo_element() + } + + fn elapsed_time(&self) -> f32 { + self.0.elapsed_time() + } +} + +pub struct WebsysTouchEvent(pub web_sys::TouchEvent); +impl TouchEventInner for WebsysTouchEvent { + fn alt_key(&self) -> bool { + self.0.alt_key() + } + + fn ctrl_key(&self) -> bool { + self.0.ctrl_key() + } + + fn meta_key(&self) -> bool { + self.0.meta_key() + } + + fn shift_key(&self) -> bool { + self.0.shift_key() + } + + fn get_modifier_state(&self, key_code: &str) -> bool { + if cfg!(debug_assertions) { + todo!("get_modifier_state is not currently supported for touch events"); + } else { + false + } + } +} + +pub struct WebsysMediaEvent(pub web_sys::UiEvent); +impl MediaEventInner for WebsysMediaEvent {} + +pub struct WebsysToggleEvent(pub web_sys::UiEvent); +impl ToggleEventInner for WebsysToggleEvent {} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 9b840d74c..e8180ef85 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -56,6 +56,7 @@ use std::rc::Rc; pub use crate::cfg::WebConfig; use crate::dom::load_document; +use cache::intern_cache; use dioxus::prelude::Properties; use dioxus::virtual_dom::VirtualDom; pub use dioxus_core as dioxus; @@ -64,6 +65,7 @@ use dioxus_core::prelude::FC; mod cache; mod cfg; mod dom; +mod events; mod nodeslab; mod ric_raf; @@ -114,6 +116,8 @@ where pub async fn run_with_props(root: FC, root_props: T, cfg: WebConfig) { let mut dom = VirtualDom::new_with_props(root, root_props); + intern_cache(); + let hydrating = cfg.hydrate; let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap(); diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 429fa0259..0a914540c 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -1,10 +1,7 @@ //! RequestAnimationFrame and RequestIdleCallback port and polyfill. -use std::{cell::RefCell, fmt, rc::Rc}; - use gloo_timers::future::TimeoutFuture; use js_sys::Function; -use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen::{prelude::Closure, JsValue}; use web_sys::Window; @@ -21,13 +18,15 @@ impl RafLoop { pub fn new() -> Self { let (raf_sender, raf_receiver) = async_channel::unbounded(); - let raf_closure: Closure = - Closure::wrap(Box::new(move |v: JsValue| raf_sender.try_send(()).unwrap())); + let raf_closure: Closure = Closure::wrap(Box::new(move |_v: JsValue| { + raf_sender.try_send(()).unwrap() + })); let (ric_sender, ric_receiver) = async_channel::unbounded(); - let ric_closure: Closure = - Closure::wrap(Box::new(move |v: JsValue| ric_sender.try_send(()).unwrap())); + let ric_closure: Closure = Closure::wrap(Box::new(move |_v: JsValue| { + ric_sender.try_send(()).unwrap() + })); // execute the polyfill for safari Function::new_no_args(include_str!("./ricpolyfill.js")) @@ -44,45 +43,16 @@ impl RafLoop { } /// waits for some idle time and returns a timeout future that expires after the idle time has passed pub async fn wait_for_idle_time(&self) -> TimeoutFuture { - // comes with its own safari polyfill :) - let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); let deadline: u32 = self.window.request_idle_callback(ric_fn).unwrap(); - self.ric_receiver.recv().await.unwrap(); - let deadline = TimeoutFuture::new(deadline); deadline } pub async fn wait_for_raf(&self) { let raf_fn = self.raf_closure.as_ref().dyn_ref::().unwrap(); - let id: i32 = self.window.request_animation_frame(raf_fn).unwrap(); + let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap(); self.raf_receiver.recv().await.unwrap(); } } - -#[derive(Debug)] -pub struct AnimationFrame { - render_id: i32, - closure: Closure, - callback_wrapper: Rc>>, -} - -struct CallbackWrapper(Box); -impl fmt::Debug for CallbackWrapper { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("CallbackWrapper") - } -} - -impl Drop for AnimationFrame { - fn drop(&mut self) { - if self.callback_wrapper.borrow_mut().is_some() { - web_sys::window() - .unwrap_throw() - .cancel_animation_frame(self.render_id) - .unwrap_throw() - } - } -} From 05e960b6b037d237ddcaf4fef4d33179883b6644 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 25 Aug 2021 12:42:50 -0400 Subject: [PATCH 30/53] wip: refactor to fix some ownership quirks --- packages/core/examples/vdom_usage.rs | 19 ++ packages/core/src/arena.rs | 437 +++++++++++++++++++++--- packages/core/src/childiter.rs | 6 +- packages/core/src/diff.rs | 22 +- packages/core/src/events.rs | 6 +- packages/core/src/lib.rs | 4 +- packages/core/src/mutations.rs | 4 +- packages/core/src/scheduler.rs | 409 ---------------------- packages/core/src/scope.rs | 74 ++-- packages/core/src/virtual_dom.rs | 41 +-- packages/core/tests/create_iterative.rs | 2 +- packages/core/tests/diffing.rs | 6 +- packages/core/tests/eventsystem.rs | 9 +- packages/core/tests/hooks.rs | 2 +- packages/core/tests/set_state_batch.rs | 41 +++ packages/core/tests/work_sync.rs | 2 +- 16 files changed, 549 insertions(+), 535 deletions(-) create mode 100644 packages/core/examples/vdom_usage.rs delete mode 100644 packages/core/src/scheduler.rs create mode 100644 packages/core/tests/set_state_batch.rs diff --git a/packages/core/examples/vdom_usage.rs b/packages/core/examples/vdom_usage.rs new file mode 100644 index 000000000..027452104 --- /dev/null +++ b/packages/core/examples/vdom_usage.rs @@ -0,0 +1,19 @@ +use std::time::Duration; + +use dioxus_core::prelude::*; + +#[async_std::main] +async fn main() { + static App: FC<()> = |cx| cx.render(LazyNodes::new(|f| f.text(format_args!("hello")))); + + let mut dom = VirtualDom::new(App); + + dom.rebuild(); + + let mut deadline = async_std::task::sleep(Duration::from_millis(50)); + let fut = dom.run_with_deadline(deadline); + + if let Some(mutations) = fut.await { + // + } +} diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 9ed7a2277..678bf10cc 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -10,30 +10,19 @@ use fxhash::{FxHashMap, FxHashSet}; use slab::Slab; use smallvec::SmallVec; -// slotmap::new_key_type! { -// // A dedicated key type for the all the scopes -// pub struct ScopeId; -// } -// #[cfg(feature = "serialize", serde::Serialize)] -// #[cfg(feature = "serialize", serde::Serialize)] -#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ScopeId(pub usize); +use std::any::Any; -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ElementId(pub usize); -impl Display for ElementId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} +use std::any::TypeId; +use std::cell::Ref; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; +use std::pin::Pin; -impl ElementId { - pub fn as_u64(self) -> u64 { - self.0 as u64 - } -} +use futures_util::future::FusedFuture; +use futures_util::pin_mut; +use futures_util::Future; +use futures_util::FutureExt; +use futures_util::StreamExt; -type Shared = Rc>; type UiReceiver = UnboundedReceiver; type UiSender = UnboundedSender; @@ -41,38 +30,51 @@ type TaskReceiver = UnboundedReceiver; type TaskSender = UnboundedSender; /// These are resources shared among all the components and the virtualdom itself -#[derive(Clone)] -pub struct SharedResources { - pub components: Rc>>, +pub struct Scheduler { + pub components: UnsafeCell>, - pub(crate) heuristics: Shared, + pub(crate) heuristics: HeuristicsEngine, // Used by "set_state" and co - is its own queue pub immediate_sender: TaskSender, - pub immediate_receiver: Shared, + pub immediate_receiver: TaskReceiver, /// Triggered by event listeners pub ui_event_sender: UiSender, - pub ui_event_receiver: Shared, + pub ui_event_receiver: UiReceiver, // Garbage stored - pub pending_garbage: Shared>, + pub pending_garbage: FxHashSet, // In-flight futures - pub async_tasks: Shared>, + pub async_tasks: FuturesUnordered, /// We use a SlotSet to keep track of the keys that are currently being used. /// However, we don't store any specific data since the "mirror" - pub raw_elements: Shared>, + pub raw_elements: Slab<()>, pub task_setter: Rc, + + // scheduler stuff + pub current_priority: EventPriority, + + pub pending_events: VecDeque, + + pub pending_immediates: VecDeque, + + pub pending_tasks: VecDeque, + + pub garbage_scopes: HashSet, + + pub high_priorty: PriortySystem, + pub medium_priority: PriortySystem, + pub low_priority: PriortySystem, } -impl SharedResources { +impl Scheduler { pub fn new() -> Self { // preallocate 2000 elements and 20 scopes to avoid dynamic allocation - let components: Rc>> = - Rc::new(UnsafeCell::new(Slab::with_capacity(100))); + let components: UnsafeCell> = UnsafeCell::new(Slab::with_capacity(100)); // elements are super cheap - the value takes no space let raw_elements = Slab::with_capacity(2000); @@ -107,19 +109,32 @@ impl SharedResources { Self { components, - async_tasks: Rc::new(RefCell::new(FuturesUnordered::new())), + async_tasks: FuturesUnordered::new(), - ui_event_receiver: Rc::new(RefCell::new(ui_receiver)), + ui_event_receiver: ui_receiver, ui_event_sender: ui_sender, - immediate_receiver: Rc::new(RefCell::new(immediate_receiver)), + immediate_receiver: immediate_receiver, immediate_sender: immediate_sender, - pending_garbage: Rc::new(RefCell::new(FxHashSet::default())), + pending_garbage: FxHashSet::default(), - heuristics: Rc::new(RefCell::new(heuristics)), - raw_elements: Rc::new(RefCell::new(raw_elements)), + heuristics: heuristics, + raw_elements: raw_elements, task_setter, + + // a storage for our receiver to dump into + pending_events: VecDeque::new(), + pending_immediates: VecDeque::new(), + pending_tasks: VecDeque::new(), + + garbage_scopes: HashSet::new(), + + current_priority: EventPriority::Low, + + high_priorty: PriortySystem::new(), + medium_priority: PriortySystem::new(), + low_priority: PriortySystem::new(), } } @@ -198,6 +213,309 @@ impl SharedResources { priority: trigger.priority, } } + + pub fn clean_up_garbage(&mut self) { + let mut scopes_to_kill = Vec::new(); + let mut garbage_list = Vec::new(); + + for scope in self.garbage_scopes.drain() { + let scope = self.get_scope_mut(scope).unwrap(); + for node in scope.consume_garbage() { + garbage_list.push(node); + } + + while let Some(node) = garbage_list.pop() { + match &node { + VNode::Text(_) => { + self.collect_garbage(node.direct_id()); + } + VNode::Anchor(_) => { + self.collect_garbage(node.direct_id()); + } + VNode::Suspended(_) => { + self.collect_garbage(node.direct_id()); + } + + VNode::Element(el) => { + self.collect_garbage(node.direct_id()); + for child in el.children { + garbage_list.push(child); + } + } + + VNode::Fragment(frag) => { + for child in frag.children { + garbage_list.push(child); + } + } + + VNode::Component(comp) => { + // TODO: run the hook destructors and then even delete the scope + + let scope_id = comp.ass_scope.get().unwrap(); + let scope = self.get_scope(scope_id).unwrap(); + let root = scope.root(); + garbage_list.push(root); + scopes_to_kill.push(scope_id); + } + } + } + } + + for scope in scopes_to_kill.drain(..) { + // + // kill em + } + } + + // channels don't have these methods, so we just implement our own wrapper + pub fn next_event(&mut self) -> Option { + // pop the last event off the internal queue + self.pending_events.pop_back().or_else(|| { + self.ui_event_receiver + .borrow_mut() + .try_next() + .ok() + .flatten() + }) + } + + pub fn manually_poll_events(&mut self) { + // 1. Poll the UI event receiver + // 2. Poll the set_state receiver + + // poll the immediates first, creating work out of them + let shared_receiver = self.immediate_receiver.clone(); + let mut receiver = shared_receiver.borrow_mut(); + while let Ok(Some(trigger)) = receiver.try_next() { + self.add_dirty_scope(trigger, EventPriority::Low); + } + + // next poll the UI events, + let mut receiver = self.ui_event_receiver.borrow_mut(); + while let Ok(Some(trigger)) = receiver.try_next() { + self.pending_events.push_back(trigger); + } + } + + // Converts UI events into dirty scopes with various priorities + pub fn consume_pending_events(&mut self) -> Result<()> { + while let Some(trigger) = self.pending_events.pop_back() { + match &trigger.event { + VirtualEvent::AsyncEvent { .. } => {} + + // This suspense system works, but it's not the most elegant solution. + // TODO: Replace this system + VirtualEvent::SuspenseEvent { hook_idx, domnode } => { + todo!("suspense needs to be converted into its own channel"); + + // // Safety: this handler is the only thing that can mutate shared items at this moment in tim + // let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + + // // safety: we are sure that there are no other references to the inner content of suspense hooks + // let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); + + // let cx = Context { scope, props: &() }; + // let scx = SuspendedContext { inner: cx }; + + // // generate the new node! + // let nodes: Option = (&hook.callback)(scx); + + // if let Some(nodes) = nodes { + // // allocate inside the finished frame - not the WIP frame + // let nodes = scope.frames.finished_frame().bump.alloc(nodes); + + // // push the old node's root onto the stack + // let real_id = domnode.get().ok_or(Error::NotMounted)?; + // diff_machine.edit_push_root(real_id); + + // // push these new nodes onto the diff machines stack + // let meta = diff_machine.create_vnode(&*nodes); + + // // replace the placeholder with the new nodes we just pushed on the stack + // diff_machine.edit_replace_with(1, meta.added_to_stack); + // } else { + // log::warn!( + // "Suspense event came through, but there were no generated nodes >:(." + // ); + // } + } + + VirtualEvent::ClipboardEvent(_) + | VirtualEvent::CompositionEvent(_) + | VirtualEvent::KeyboardEvent(_) + | VirtualEvent::FocusEvent(_) + | VirtualEvent::FormEvent(_) + | VirtualEvent::SelectionEvent(_) + | VirtualEvent::TouchEvent(_) + | VirtualEvent::UIEvent(_) + | VirtualEvent::WheelEvent(_) + | VirtualEvent::MediaEvent(_) + | VirtualEvent::AnimationEvent(_) + | VirtualEvent::TransitionEvent(_) + | VirtualEvent::ToggleEvent(_) + | VirtualEvent::MouseEvent(_) + | VirtualEvent::PointerEvent(_) => { + if let Some(scope) = self.get_scope_mut(trigger.originator) { + if let Some(element) = trigger.real_node_id { + scope.call_listener(trigger.event, element)?; + + let receiver = self.immediate_receiver.clone(); + let mut receiver = receiver.borrow_mut(); + + // Drain the immediates into the dirty scopes, setting the appropiate priorities + while let Ok(Some(dirty_scope)) = receiver.try_next() { + self.add_dirty_scope(dirty_scope, trigger.priority) + } + } + } + } + } + } + + Ok(()) + } + + // nothing to do, no events on channels, no work + pub fn has_any_work(&self) -> bool { + self.has_work() || self.has_pending_events() || self.has_pending_garbage() + } + + pub fn has_pending_events(&self) -> bool { + self.pending_events.len() > 0 + } + + pub fn has_work(&self) -> bool { + self.high_priorty.has_work() + || self.medium_priority.has_work() + || self.low_priority.has_work() + } + + 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 async fn work_with_deadline<'a>( + &mut self, + mutations: &mut Mutations<'_>, + deadline: &mut Pin>>, + ) -> FiberResult { + // check if we need to elevate priority + self.current_priority = match ( + self.high_priorty.has_work(), + self.medium_priority.has_work(), + self.low_priority.has_work(), + ) { + (true, _, _) => EventPriority::High, + (false, true, _) => EventPriority::Medium, + (false, false, _) => EventPriority::Low, + }; + + let mut machine = DiffMachine::new(mutations, ScopeId(0), &self); + + let dirty_root = { + let dirty_roots = match self.current_priority { + EventPriority::High => &self.high_priorty.dirty_scopes, + EventPriority::Medium => &self.medium_priority.dirty_scopes, + EventPriority::Low => &self.low_priority.dirty_scopes, + }; + let mut height = 0; + let mut dirty_root = { + let root = dirty_roots.iter().next(); + if root.is_none() { + return FiberResult::Done; + } + root.unwrap() + }; + + for root in dirty_roots { + if let Some(scope) = self.get_scope(*root) { + if scope.height < height { + height = scope.height; + dirty_root = root; + } + } + } + dirty_root + }; + + match { + let fut = machine.diff_scope(*dirty_root).fuse(); + pin_mut!(fut); + + match futures_util::future::select(deadline, fut).await { + futures_util::future::Either::Left((deadline, work_fut)) => true, + futures_util::future::Either::Right((_, deadline_fut)) => false, + } + } { + true => FiberResult::Done, + false => FiberResult::Interrupted, + } + } + + // waits for a trigger, canceling early if the deadline is reached + // returns true if the deadline was reached + // does not return the trigger, but caches it in the scheduler + pub async fn wait_for_any_trigger( + &mut self, + mut deadline: &mut Pin>>, + ) -> bool { + use futures_util::select; + + let _immediates = self.immediate_receiver.clone(); + let mut immediates = _immediates.borrow_mut(); + + let mut _ui_events = self.ui_event_receiver.clone(); + let mut ui_events = _ui_events.borrow_mut(); + + let mut _tasks = self.async_tasks.clone(); + let mut tasks = _tasks.borrow_mut(); + + // set_state called + select! { + dirty_scope = immediates.next() => { + if let Some(scope) = dirty_scope { + self.add_dirty_scope(scope, EventPriority::Low); + } + } + + ui_event = ui_events.next() => { + if let Some(event) = ui_event { + self.pending_events.push_back(event); + } + } + + async_task = tasks.next() => { + if let Some(event) = async_task { + self.pending_events.push_back(event); + } + } + + _ = deadline => { + return true; + } + + } + false + } + + pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { + match priority { + EventPriority::High => self.high_priorty.dirty_scopes.insert(scope), + EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope), + EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), + }; + } } pub struct TaskHandle {} @@ -208,3 +526,44 @@ impl TaskHandle { pub fn stop(&self) {} pub fn restart(&self) {} } + +#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)] +pub struct DirtyScope { + height: u32, + start_tick: u32, +} + +pub struct PriortySystem { + pub pending_scopes: Vec, + pub dirty_scopes: HashSet, +} + +impl PriortySystem { + pub fn new() -> Self { + Self { + pending_scopes: Default::default(), + dirty_scopes: Default::default(), + } + } + + fn has_work(&self) -> bool { + self.pending_scopes.len() > 0 || self.dirty_scopes.len() > 0 + } +} + +#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ScopeId(pub usize); + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ElementId(pub usize); +impl Display for ElementId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl ElementId { + pub fn as_u64(self) -> u64 { + self.0 as u64 + } +} diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs index 936a632be..bfac9503d 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/childiter.rs @@ -5,7 +5,7 @@ use crate::innerlude::*; /// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like /// "InsertBefore". pub struct RealChildIterator<'a> { - scopes: &'a SharedResources, + scopes: &'a Scheduler, // Heuristcally we should never bleed into 4 completely nested fragments/components // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane @@ -14,14 +14,14 @@ pub struct RealChildIterator<'a> { } impl<'a> RealChildIterator<'a> { - pub fn new(starter: &'a VNode<'a>, scopes: &'a SharedResources) -> Self { + pub fn new(starter: &'a VNode<'a>, scopes: &'a Scheduler) -> Self { Self { scopes, stack: smallvec::smallvec![(0, starter)], } } - pub fn new_from_slice(nodes: &'a [VNode<'a>], scopes: &'a SharedResources) -> Self { + pub fn new_from_slice(nodes: &'a [VNode<'a>], scopes: &'a Scheduler) -> Self { let mut stack = smallvec::smallvec![]; for node in nodes { stack.push((0, node)); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 4aeabd89b..a301b7642 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -88,7 +88,7 @@ //! More info on how to improve this diffing algorithm: //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ -use crate::{arena::SharedResources, innerlude::*}; +use crate::{arena::Scheduler, innerlude::*}; use fxhash::{FxHashMap, FxHashSet}; use DomEdit::*; @@ -105,9 +105,9 @@ use DomEdit::*; /// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's /// stack machines all the way down! pub struct DiffMachine<'bump> { - vdom: &'bump SharedResources, + vdom: &'bump Scheduler, - pub mutations: Mutations<'bump>, + pub mutations: &'bump mut Mutations<'bump>, pub stack: DiffStack<'bump>, @@ -118,9 +118,9 @@ pub struct DiffMachine<'bump> { impl<'bump> DiffMachine<'bump> { pub(crate) fn new( - edits: Mutations<'bump>, + edits: &'bump mut Mutations<'bump>, cur_scope: ScopeId, - shared: &'bump SharedResources, + shared: &'bump Scheduler, ) -> Self { Self { stack: DiffStack::new(cur_scope), @@ -131,11 +131,11 @@ impl<'bump> DiffMachine<'bump> { } } - pub fn new_headless(shared: &'bump SharedResources) -> Self { - let edits = Mutations::new(); - let cur_scope = ScopeId(0); - Self::new(edits, cur_scope, shared) - } + // pub fn new_headless(shared: &'bump SharedResources) -> Self { + // let edits = Mutations::new(); + // let cur_scope = ScopeId(0); + // Self::new(edits, cur_scope, shared) + // } // pub async fn diff_scope(&mut self, id: ScopeId) { @@ -320,7 +320,7 @@ impl<'bump> DiffMachine<'bump> { Some(parent_idx), height, ScopeChildren(vcomponent.children), - self.vdom.clone(), + // self.vdom.clone(), vcomponent.name, ) }); diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 41868aa95..87ff359d9 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -68,9 +68,13 @@ impl Ord for EventKey { /// - UserBlocking = 3 /// - HighPriority = 4 /// - ImmediatePriority = 5 +/// +/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. +/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be +/// flushed before proceeding. Multiple discrete events is highly unlikely, though. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] pub enum EventPriority { - /// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work. + /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. /// /// This is typically reserved for things like user interaction. /// diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index ed4e8ce9f..d105f9770 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -43,7 +43,7 @@ pub(crate) mod innerlude { pub use crate::hooks::*; pub use crate::mutations::*; pub use crate::nodes::*; - pub use crate::scheduler::*; + // pub use crate::scheduler::*; pub use crate::scope::*; pub use crate::util::*; pub use crate::virtual_dom::*; @@ -75,7 +75,7 @@ pub mod hooklist; pub mod hooks; pub mod mutations; pub mod nodes; -pub mod scheduler; +// pub mod scheduler; pub mod scope; pub mod signals; pub mod util; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 7b00b2fd0..81909cd1d 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -19,7 +19,9 @@ impl<'a> Mutations<'a> { Self { edits, noderefs } } - pub fn extend(&mut self, other: &mut Mutations) {} + pub fn extend(&mut self, other: &mut Mutations) { + // self.edits.extend(other.edits); + } // Navigation pub(crate) fn push_root(&mut self, root: ElementId) { diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs deleted file mode 100644 index 3c635da04..000000000 --- a/packages/core/src/scheduler.rs +++ /dev/null @@ -1,409 +0,0 @@ -//! 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; -use std::cell::{Ref, RefCell, RefMut}; -use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; -use std::pin::Pin; - -use futures_util::future::FusedFuture; -use futures_util::pin_mut; -use futures_util::Future; -use futures_util::FutureExt; -use futures_util::StreamExt; -use smallvec::SmallVec; - -use crate::innerlude::*; - -pub struct Scheduler { - current_priority: EventPriority, - - pending_events: VecDeque, - - pending_immediates: VecDeque, - - pending_tasks: VecDeque, - - garbage_scopes: HashSet, - - shared: SharedResources, - - high_priorty: PriortySystem, - medium_priority: PriortySystem, - low_priority: PriortySystem, -} - -pub enum FiberResult<'a> { - Done(Mutations<'a>), - Interrupted, -} - -impl Scheduler { - pub fn new(shared: SharedResources) -> Self { - Self { - shared, - - // a storage for our receiver to dump into - pending_events: VecDeque::new(), - pending_immediates: VecDeque::new(), - pending_tasks: VecDeque::new(), - - garbage_scopes: HashSet::new(), - - current_priority: EventPriority::Low, - - high_priorty: PriortySystem::new(), - medium_priority: PriortySystem::new(), - low_priority: PriortySystem::new(), - } - } - - // channels don't have these methods, so we just implement our own wrapper - pub fn next_event(&mut self) -> Option { - // pop the last event off the internal queue - self.pending_events.pop_back().or_else(|| { - self.shared - .ui_event_receiver - .borrow_mut() - .try_next() - .ok() - .flatten() - }) - } - - pub fn manually_poll_events(&mut self) { - // 1. Poll the UI event receiver - // 2. Poll the set_state receiver - - // poll the immediates first, creating work out of them - let shared_receiver = self.shared.immediate_receiver.clone(); - let mut receiver = shared_receiver.borrow_mut(); - while let Ok(Some(trigger)) = receiver.try_next() { - self.add_dirty_scope(trigger, EventPriority::Low); - } - - // next poll the UI events, - let mut receiver = self.shared.ui_event_receiver.borrow_mut(); - while let Ok(Some(trigger)) = receiver.try_next() { - self.pending_events.push_back(trigger); - } - } - - // Converts UI events into dirty scopes with various priorities - pub fn consume_pending_events(&mut self) -> Result<()> { - while let Some(trigger) = self.pending_events.pop_back() { - match &trigger.event { - VirtualEvent::AsyncEvent { .. } => {} - - // This suspense system works, but it's not the most elegant solution. - // TODO: Replace this system - VirtualEvent::SuspenseEvent { hook_idx, domnode } => { - todo!("suspense needs to be converted into its own channel"); - - // // Safety: this handler is the only thing that can mutate shared items at this moment in tim - // let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - // // safety: we are sure that there are no other references to the inner content of suspense hooks - // let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - - // let cx = Context { scope, props: &() }; - // let scx = SuspendedContext { inner: cx }; - - // // generate the new node! - // let nodes: Option = (&hook.callback)(scx); - - // if let Some(nodes) = nodes { - // // allocate inside the finished frame - not the WIP frame - // let nodes = scope.frames.finished_frame().bump.alloc(nodes); - - // // push the old node's root onto the stack - // let real_id = domnode.get().ok_or(Error::NotMounted)?; - // diff_machine.edit_push_root(real_id); - - // // push these new nodes onto the diff machines stack - // let meta = diff_machine.create_vnode(&*nodes); - - // // replace the placeholder with the new nodes we just pushed on the stack - // diff_machine.edit_replace_with(1, meta.added_to_stack); - // } else { - // log::warn!( - // "Suspense event came through, but there were no generated nodes >:(." - // ); - // } - } - - VirtualEvent::ClipboardEvent(_) - | VirtualEvent::CompositionEvent(_) - | VirtualEvent::KeyboardEvent(_) - | VirtualEvent::FocusEvent(_) - | VirtualEvent::FormEvent(_) - | VirtualEvent::SelectionEvent(_) - | VirtualEvent::TouchEvent(_) - | VirtualEvent::UIEvent(_) - | VirtualEvent::WheelEvent(_) - | VirtualEvent::MediaEvent(_) - | VirtualEvent::AnimationEvent(_) - | VirtualEvent::TransitionEvent(_) - | VirtualEvent::ToggleEvent(_) - | VirtualEvent::MouseEvent(_) - | VirtualEvent::PointerEvent(_) => { - if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { - if let Some(element) = trigger.real_node_id { - scope.call_listener(trigger.event, element)?; - - let receiver = self.shared.immediate_receiver.clone(); - let mut receiver = receiver.borrow_mut(); - - // Drain the immediates into the dirty scopes, setting the appropiate priorities - while let Ok(Some(dirty_scope)) = receiver.try_next() { - self.add_dirty_scope(dirty_scope, trigger.priority) - } - } - } - } - } - } - - Ok(()) - } - - // nothing to do, no events on channels, no work - pub fn has_any_work(&self) -> bool { - self.has_work() || self.has_pending_events() || self.has_pending_garbage() - } - - pub fn has_pending_events(&self) -> bool { - self.pending_events.len() > 0 - } - - pub fn has_work(&self) -> bool { - self.high_priorty.has_work() - || self.medium_priority.has_work() - || self.low_priority.has_work() - } - - 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 async fn work_with_deadline<'a>( - &'a mut self, - deadline: &mut Pin>>, - ) -> FiberResult<'a> { - // check if we need to elevate priority - self.current_priority = match ( - self.high_priorty.has_work(), - self.medium_priority.has_work(), - self.low_priority.has_work(), - ) { - (true, _, _) => EventPriority::High, - (false, true, _) => EventPriority::Medium, - (false, false, _) => EventPriority::Low, - }; - - let mut machine = DiffMachine::new_headless(&self.shared); - - let dirty_root = { - let dirty_roots = match self.current_priority { - EventPriority::High => &self.high_priorty.dirty_scopes, - EventPriority::Medium => &self.medium_priority.dirty_scopes, - EventPriority::Low => &self.low_priority.dirty_scopes, - }; - let mut height = 0; - let mut dirty_root = { - let root = dirty_roots.iter().next(); - if root.is_none() { - return FiberResult::Done(machine.mutations); - } - root.unwrap() - }; - - for root in dirty_roots { - if let Some(scope) = self.shared.get_scope(*root) { - if scope.height < height { - height = scope.height; - dirty_root = root; - } - } - } - dirty_root - }; - - match { - let fut = machine.diff_scope(*dirty_root).fuse(); - pin_mut!(fut); - - match futures_util::future::select(deadline, fut).await { - futures_util::future::Either::Left((deadline, work_fut)) => true, - futures_util::future::Either::Right((_, deadline_fut)) => false, - } - } { - true => FiberResult::Done(machine.mutations), - false => FiberResult::Interrupted, - } - } - - // waits for a trigger, canceling early if the deadline is reached - // returns true if the deadline was reached - // does not return the trigger, but caches it in the scheduler - pub async fn wait_for_any_trigger( - &mut self, - mut deadline: &mut Pin>>, - ) -> bool { - use futures_util::select; - - let _immediates = self.shared.immediate_receiver.clone(); - let mut immediates = _immediates.borrow_mut(); - - let mut _ui_events = self.shared.ui_event_receiver.clone(); - let mut ui_events = _ui_events.borrow_mut(); - - let mut _tasks = self.shared.async_tasks.clone(); - let mut tasks = _tasks.borrow_mut(); - - // set_state called - select! { - dirty_scope = immediates.next() => { - if let Some(scope) = dirty_scope { - self.add_dirty_scope(scope, EventPriority::Low); - } - } - - ui_event = ui_events.next() => { - if let Some(event) = ui_event { - self.pending_events.push_back(event); - } - } - - async_task = tasks.next() => { - if let Some(event) = async_task { - self.pending_events.push_back(event); - } - } - - _ = deadline => { - return true; - } - - } - false - } - - pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { - match priority { - EventPriority::High => self.high_priorty.dirty_scopes.insert(scope), - EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope), - EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), - }; - } - - pub fn clean_up_garbage(&mut self) { - let mut scopes_to_kill = Vec::new(); - let mut garbage_list = Vec::new(); - - for scope in self.garbage_scopes.drain() { - let scope = self.shared.get_scope_mut(scope).unwrap(); - for node in scope.consume_garbage() { - garbage_list.push(node); - } - - while let Some(node) = garbage_list.pop() { - match &node { - VNode::Text(_) => { - self.shared.collect_garbage(node.direct_id()); - } - VNode::Anchor(_) => { - self.shared.collect_garbage(node.direct_id()); - } - VNode::Suspended(_) => { - self.shared.collect_garbage(node.direct_id()); - } - - VNode::Element(el) => { - self.shared.collect_garbage(node.direct_id()); - for child in el.children { - garbage_list.push(child); - } - } - - VNode::Fragment(frag) => { - for child in frag.children { - garbage_list.push(child); - } - } - - VNode::Component(comp) => { - // TODO: run the hook destructors and then even delete the scope - - let scope_id = comp.ass_scope.get().unwrap(); - let scope = self.shared.get_scope(scope_id).unwrap(); - let root = scope.root(); - garbage_list.push(root); - scopes_to_kill.push(scope_id); - } - } - } - } - - for scope in scopes_to_kill.drain(..) { - // - // kill em - } - } -} - -#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)] -pub struct DirtyScope { - height: u32, - start_tick: u32, -} - -pub struct PriortySystem { - pub pending_scopes: Vec, - pub dirty_scopes: HashSet, -} - -impl PriortySystem { - pub fn new() -> Self { - Self { - pending_scopes: Default::default(), - dirty_scopes: Default::default(), - } - } - - fn has_work(&self) -> bool { - self.pending_scopes.len() > 0 || self.dirty_scopes.len() > 0 - } -} diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 61945156a..4088a6de7 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -45,9 +45,8 @@ pub struct Scope { // meta pub(crate) function_name: &'static str, - - // A reference to the resources shared by all the comonents - pub(crate) vdom: SharedResources, + // // A reference to the resources shared by all the comonents + // pub(crate) vdom: SharedResources, } // The type of closure that wraps calling components @@ -75,18 +74,17 @@ impl Scope { child_nodes: ScopeChildren, - vdom: SharedResources, - + // vdom: SharedResources, function_name: &'static str, ) -> Self { let child_nodes = unsafe { child_nodes.extend_lifetime() }; - // insert ourself as a descendent of the parent - // when the parent is removed, this map will be traversed, and we will also be cleaned up. - if let Some(parent) = &parent { - let parent = unsafe { vdom.get_scope(*parent) }.unwrap(); - parent.descendents.borrow_mut().insert(arena_idx); - } + // // insert ourself as a descendent of the parent + // // when the parent is removed, this map will be traversed, and we will also be cleaned up. + // if let Some(parent) = &parent { + // let parent = unsafe { vdom.get_scope(*parent) }.unwrap(); + // parent.descendents.borrow_mut().insert(arena_idx); + // } Self { function_name, @@ -95,7 +93,7 @@ impl Scope { parent_idx: parent, our_arena_idx: arena_idx, height, - vdom, + // vdom, frames: ActiveFrame::new(), hooks: Default::default(), @@ -168,33 +166,35 @@ impl Scope { "clean up your garabge please" ); - // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we - // run the hooks (which hold an &mut Referrence) - // right now, we don't drop - let vdom = &self.vdom; - self.borrowed_props - .get_mut() - .drain(..) - .map(|li| unsafe { &*li }) - .for_each(|comp| { - // First drop the component's undropped references - let scope_id = comp.ass_scope.get().unwrap(); - let scope = unsafe { vdom.get_scope_mut(scope_id) }.unwrap(); - scope.ensure_drop_safety(); + todo!("arch changes"); - // Now, drop our own reference - let mut dropper = comp.drop_props.borrow_mut().take().unwrap(); - dropper(); - }); + // // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we + // // run the hooks (which hold an &mut Referrence) + // // right now, we don't drop + // // let vdom = &self.vdom; + // self.borrowed_props + // .get_mut() + // .drain(..) + // .map(|li| unsafe { &*li }) + // .for_each(|comp| { + // // First drop the component's undropped references + // let scope_id = comp.ass_scope.get().unwrap(); + // let scope = unsafe { vdom.get_scope_mut(scope_id) }.unwrap(); + // scope.ensure_drop_safety(); - // Now that all the references are gone, we can safely drop our own references in our listeners. - self.listeners - .get_mut() - .drain(..) - .map(|li| unsafe { &*li }) - .for_each(|listener| { - listener.callback.borrow_mut().take(); - }); + // // Now, drop our own reference + // let mut dropper = comp.drop_props.borrow_mut().take().unwrap(); + // dropper(); + // }); + + // // Now that all the references are gone, we can safely drop our own references in our listeners. + // self.listeners + // .get_mut() + // .drain(..) + // .map(|li| unsafe { &*li }) + // .for_each(|listener| { + // listener.callback.borrow_mut().take(); + // }); } // A safe wrapper around calling listeners diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 76a283154..8f015a784 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -40,14 +40,12 @@ pub struct VirtualDom { /// /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas /// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually. - pub shared: SharedResources, + pub scheduler: Scheduler, /// The index of the root component /// Should always be the first (gen=0, id=0) base_scope: ScopeId, - scheduler: Scheduler, - // for managing the props that were used to create the dom #[doc(hidden)] _root_prop_type: std::any::TypeId, @@ -102,7 +100,7 @@ impl VirtualDom { /// /// Note: the VirtualDOM is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. pub fn new_with_props(root: FC

, root_props: P) -> Self { - let components = SharedResources::new(); + let components = Scheduler::new(); let root_props: Pin> = Box::pin(root_props); let props_ptr = root_props.as_ref().downcast_ref::

().unwrap() as *const P; @@ -119,17 +117,17 @@ impl VirtualDom { base_scope, _root_props: root_props, scheduler: Scheduler::new(components.clone()), - shared: components, + scheduler: components, _root_prop_type: TypeId::of::

(), } } pub fn base_scope(&self) -> &Scope { - self.shared.get_scope(self.base_scope).unwrap() + self.scheduler.get_scope(self.base_scope).unwrap() } pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> { - self.shared.get_scope(id) + self.scheduler.get_scope(id) } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch @@ -154,10 +152,10 @@ impl VirtualDom { /// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This /// method can be useful when needing to perform some crucial periodic tasks. pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> { - let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); + let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.scheduler); let cur_component = self - .shared + .scheduler .get_scope_mut(self.base_scope) .expect("The base scope should never be moved"); @@ -189,10 +187,10 @@ impl VirtualDom { } pub async fn diff_async<'s>(&'s mut self) -> Mutations<'s> { - let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.shared); + let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.scheduler); let cur_component = self - .shared + .scheduler .get_scope_mut(self.base_scope) .expect("The base scope should never be moved"); @@ -275,8 +273,8 @@ impl VirtualDom { pub async fn run_with_deadline<'s>( &'s mut self, deadline: impl Future, - ) -> Option> { - let mut committed_mutations = Mutations::new(); + ) -> Option>> { + let mut committed_mutations = Vec::new(); let mut deadline = Box::pin(deadline.fuse()); // TODO: @@ -302,9 +300,14 @@ impl VirtualDom { // Work through the current subtree, and commit the results when it finishes // When the deadline expires, give back the work + let mut new_mutations = Mutations::new(); match self.scheduler.work_with_deadline(&mut deadline).await { - FiberResult::Done(mut mutations) => { - committed_mutations.extend(&mut mutations); + FiberResult::Done => { + // return Some(mutations); + // for edit in mutations.edits { + // committed_mutations.edits.push(edit); + // } + // committed_mutations.extend(&mut mutations); /* quick return if there's no work left, so we can commit before the deadline expires @@ -315,9 +318,9 @@ impl VirtualDom { It makes sense to try and progress the DOM faster */ - if !self.scheduler.has_any_work() { - return Some(committed_mutations); - } + // if !self.scheduler.has_any_work() { + // return Some(committed_mutations); + // } } FiberResult::Interrupted => return None, } @@ -325,7 +328,7 @@ impl VirtualDom { } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - self.shared.ui_event_sender.clone() + self.scheduler.ui_event_sender.clone() } pub fn has_work(&self) -> bool { diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_iterative.rs index e6303e31e..0d1b6343c 100644 --- a/packages/core/tests/create_iterative.rs +++ b/packages/core/tests/create_iterative.rs @@ -1,7 +1,7 @@ //! tests to prove that the iterative implementation works use anyhow::{Context, Result}; -use dioxus::{arena::SharedResources, diff::DiffMachine, prelude::*, DomEdit, Mutations}; +use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, DomEdit, Mutations}; mod test_logging; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index bd71bc6d7..8f59165bc 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -3,7 +3,7 @@ use bumpalo::Bump; use dioxus::{ - arena::SharedResources, diff::DiffMachine, prelude::*, DiffInstruction, DomEdit, MountType, + arena::Scheduler, diff::DiffMachine, prelude::*, DiffInstruction, DomEdit, MountType, }; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; @@ -17,13 +17,13 @@ const IS_LOGGING_ENABLED: bool = false; struct TestDom { bump: Bump, - resources: SharedResources, + resources: Scheduler, } impl TestDom { fn new() -> TestDom { test_logging::set_up_logging(IS_LOGGING_ENABLED); let bump = Bump::new(); - let resources = SharedResources::new(); + let resources = Scheduler::new(); TestDom { bump, resources } } fn new_factory<'a>(&'a self) -> NodeFactory<'a> { diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs index 677f9445b..869aa8fa3 100644 --- a/packages/core/tests/eventsystem.rs +++ b/packages/core/tests/eventsystem.rs @@ -1,7 +1,7 @@ use bumpalo::Bump; use anyhow::{Context, Result}; -use dioxus::{arena::SharedResources, diff::DiffMachine, prelude::*, DomEdit}; +use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; @@ -17,11 +17,6 @@ async fn event_queue_works() { let edits = dom.rebuild(); async_std::task::spawn_local(async move { - match dom.run_unbounded().await { - Err(_) => todo!(), - Ok(mutations) => { - // - } - } + let mutations = dom.run_unbounded().await; }); } diff --git a/packages/core/tests/hooks.rs b/packages/core/tests/hooks.rs index 42f15db4b..3946b209d 100644 --- a/packages/core/tests/hooks.rs +++ b/packages/core/tests/hooks.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use dioxus::{arena::SharedResources, diff::DiffMachine, prelude::*, DomEdit}; +use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/set_state_batch.rs b/packages/core/tests/set_state_batch.rs new file mode 100644 index 000000000..98d808a96 --- /dev/null +++ b/packages/core/tests/set_state_batch.rs @@ -0,0 +1,41 @@ +use futures_util::StreamExt; + +/* +furtures_channel provides us some batching simply due to how Rust's async works. + +Any hook that uses schedule_update is simply deferring to unbounded_send. Multiple +unbounded_sends can be linked together in succession provided there isn't an "await" +between them. Our internal batching mechanism simply waits for the "schedule_update" +to fire and then pulls any messages off the unbounded_send queue. + +Additionally, due to how our "time slicing" works we'll always come back and check +in for new work if the deadline hasn't expired. On average, our deadline should be +about 10ms, which is way more than enough for diffing/creating to happen. +*/ +#[async_std::test] +async fn batch() { + let (sender, mut recver) = futures_channel::mpsc::unbounded::(); + + let handle = async_std::task::spawn(async move { + let msg = recver.next().await; + while let Ok(msg) = recver.try_next() { + println!("{:#?}", msg); + } + let msg = recver.next().await; + while let Ok(msg) = recver.try_next() { + println!("{:#?}", msg); + } + }); + + sender.unbounded_send(1).unwrap(); + sender.unbounded_send(2).unwrap(); + sender.unbounded_send(3).unwrap(); + sender.unbounded_send(4).unwrap(); + + async_std::task::sleep(std::time::Duration::from_millis(100)).await; + + sender.unbounded_send(5).unwrap(); + sender.unbounded_send(6).unwrap(); + sender.unbounded_send(7).unwrap(); + sender.unbounded_send(8).unwrap(); +} diff --git a/packages/core/tests/work_sync.rs b/packages/core/tests/work_sync.rs index ba2d785d3..93f3589b1 100644 --- a/packages/core/tests/work_sync.rs +++ b/packages/core/tests/work_sync.rs @@ -3,7 +3,7 @@ //! This means you can actually call it synchronously if you want. use anyhow::{Context, Result}; -use dioxus::{arena::SharedResources, diff::DiffMachine, prelude::*, scope::Scope}; +use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, scope::Scope}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; use futures_util::FutureExt; From cef116aa3a435857c9a3a818bcd81de8ef03a771 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 25 Aug 2021 15:54:33 -0400 Subject: [PATCH 31/53] wip: more work on priority --- packages/core/.vscode/settings.json | 2 +- packages/core/src/context.rs | 49 +-- packages/core/src/diff.rs | 8 +- packages/core/src/events.rs | 162 +-------- packages/core/src/hooks.rs | 8 +- packages/core/src/lib.rs | 9 +- packages/core/src/{arena.rs => scheduler.rs} | 342 ++++++++++++------- packages/core/src/scope.rs | 24 +- packages/core/src/virtual_dom.rs | 44 +-- packages/core/tests/create_iterative.rs | 2 +- packages/core/tests/diffing.rs | 2 +- packages/core/tests/eventsystem.rs | 2 +- packages/core/tests/hooks.rs | 2 +- packages/core/tests/work_sync.rs | 2 +- 14 files changed, 279 insertions(+), 379 deletions(-) rename packages/core/src/{arena.rs => scheduler.rs} (61%) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 5f82c31e5..2c53384ab 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -96,13 +96,11 @@ impl<'src, P> Context<'src, P> { /// ## Notice: you should prefer using prepare_update and get_scope_id /// pub fn schedule_update(&self) -> Rc { - let cb = self.scope.vdom.schedule_update(); - let id = self.get_scope_id(); - Rc::new(move || cb(id)) + self.scope.memoized_updater.clone() } pub fn prepare_update(&self) -> Rc { - self.scope.vdom.schedule_update() + self.scope.shared.schedule_any_immediate.clone() } pub fn schedule_effect(&self) -> Rc { @@ -160,7 +158,7 @@ impl<'src, P> Context<'src, P> { /// /// pub fn submit_task(&self, task: FiberTask) -> TaskHandle { - self.scope.vdom.submit_task(task) + (self.scope.shared.submit_task)(task) } /// Add a state globally accessible to child components via tree walking @@ -174,46 +172,11 @@ impl<'src, P> Context<'src, P> { }); } - /// Walk the tree to find a shared state with the TypeId of the generic type - /// pub fn consume_shared_state(self) -> Option> { - let mut scope = Some(self.scope); - let mut parent = None; - + let getter = &self.scope.shared.get_shared_context; let ty = TypeId::of::(); - while let Some(inner) = scope { - log::debug!( - "Searching {:#?} for valid shared_context", - inner.our_arena_idx - ); - let shared_ctx = { - let shared_contexts = inner.shared_contexts.borrow(); - - log::debug!( - "This component has {} shared contexts", - shared_contexts.len() - ); - shared_contexts.get(&ty).map(|f| f.clone()) - }; - - if let Some(shared_cx) = shared_ctx { - log::debug!("found matching cx"); - let rc = shared_cx - .clone() - .downcast::() - .expect("Should not fail, already validated the type from the hashmap"); - parent = Some(rc); - break; - } else { - match inner.parent_idx { - Some(parent_id) => { - scope = unsafe { inner.vdom.get_scope(parent_id) }; - } - None => break, - } - } - } - parent + let idx = self.scope.our_arena_idx; + getter(idx, ty).map(|f| f.downcast().unwrap()) } /// Store a value between renders diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index a301b7642..47d8d378c 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -88,7 +88,7 @@ //! More info on how to improve this diffing algorithm: //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ -use crate::{arena::Scheduler, innerlude::*}; +use crate::{innerlude::*, scheduler::Scheduler}; use fxhash::{FxHashMap, FxHashSet}; use DomEdit::*; @@ -110,9 +110,7 @@ pub struct DiffMachine<'bump> { pub mutations: &'bump mut Mutations<'bump>, pub stack: DiffStack<'bump>, - pub diffed: FxHashSet, - pub seen_scopes: FxHashSet, } @@ -310,6 +308,7 @@ impl<'bump> DiffMachine<'bump> { let parent_idx = self.stack.current_scope().unwrap(); + let shared = self.vdom.channel.clone(); // Insert a new scope into our component list let new_idx = self.vdom.insert_scope_with_key(|new_idx| { let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); @@ -320,8 +319,7 @@ impl<'bump> DiffMachine<'bump> { Some(parent_idx), height, ScopeChildren(vcomponent.children), - // self.vdom.clone(), - vcomponent.name, + shared, ) }); diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 87ff359d9..f74aace14 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -17,169 +17,34 @@ use crate::{ #[derive(Debug)] pub struct EventTrigger { /// The originator of the event trigger - pub originator: ScopeId, + pub scope: ScopeId, /// The optional real node associated with the trigger - pub real_node_id: Option, + pub mounted_dom_id: Option, /// The type of event pub event: VirtualEvent, - /// The priority of the event - pub priority: EventPriority, -} - -#[derive(PartialEq, Eq, Clone, Copy)] -pub struct EventKey { - /// The originator of the event trigger - pub originator: ScopeId, - /// The priority of the event - pub priority: EventPriority, - /// The height of the scope (used for ordering) - pub height: u32, - // TODO: add the time that the event was queued -} - -impl PartialOrd for EventKey { - fn partial_cmp(&self, other: &Self) -> Option { - todo!() - } -} -impl Ord for EventKey { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - todo!() - } -} - -/// Priority of Event Triggers. -/// -/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus -/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers -/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. -/// -/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. -/// -/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, -/// we keep it simple, and just use a 3-tier priority system. -/// -/// - NoPriority = 0 -/// - LowPriority = 1 -/// - NormalPriority = 2 -/// - UserBlocking = 3 -/// - HighPriority = 4 -/// - ImmediatePriority = 5 -/// -/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. -/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be -/// flushed before proceeding. Multiple discrete events is highly unlikely, though. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -pub enum EventPriority { - /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. - /// - /// This is typically reserved for things like user interaction. - /// - /// React calls these "discrete" events, but with an extra category of "user-blocking". - High = 2, - - /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important - /// than "High Priority" events and will take presedence over low priority events. - /// - /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - /// - /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). - Medium = 1, - - /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be - /// advanced to the front of the work queue until completed. - /// - /// The primary user of Low Priority work is the asynchronous work system (suspense). - /// - /// This is considered "idle" work or "background" work. - Low = 0, -} - -impl EventTrigger { - pub fn new( - event: VirtualEvent, - scope: ScopeId, - mounted_dom_id: Option, - priority: EventPriority, - ) -> Self { - Self { - priority, - originator: scope, - real_node_id: mounted_dom_id, - event, - } - } + pub discrete: bool, } pub enum VirtualEvent { - // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" - // - // Async events don't necessarily propagate into a scope being ran. It's up to the event itself - // to force an update for itself. - // - // Most async events should have a low priority. - // - // This type exists for the task/concurrency system to signal that a task is ready. - // However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its - // own re-run. - AsyncEvent { - should_rerender: bool, - }, - - // Suspense events are a type of async event generated when suspended nodes are ready to be processed. - // - // they have the lowest priority - SuspenseEvent { - hook_idx: usize, - domnode: Rc>>, - }, - - // image event has conflicting method types - // ImageEvent(event_data::ImageEvent), - - // Real events + KeyboardEvent(on::KeyboardEvent), + TouchEvent(on::TouchEvent), + MouseEvent(on::MouseEvent), ClipboardEvent(on::ClipboardEvent), CompositionEvent(on::CompositionEvent), - KeyboardEvent(on::KeyboardEvent), FocusEvent(on::FocusEvent), FormEvent(on::FormEvent), SelectionEvent(on::SelectionEvent), - TouchEvent(on::TouchEvent), UIEvent(on::UIEvent), WheelEvent(on::WheelEvent), MediaEvent(on::MediaEvent), AnimationEvent(on::AnimationEvent), TransitionEvent(on::TransitionEvent), ToggleEvent(on::ToggleEvent), - MouseEvent(on::MouseEvent), PointerEvent(on::PointerEvent), -} - -impl VirtualEvent { - pub fn is_input_event(&self) -> bool { - match self { - VirtualEvent::ClipboardEvent(_) - | VirtualEvent::CompositionEvent(_) - | VirtualEvent::KeyboardEvent(_) - | VirtualEvent::FocusEvent(_) - | VirtualEvent::FormEvent(_) - | VirtualEvent::SelectionEvent(_) - | VirtualEvent::TouchEvent(_) - | VirtualEvent::UIEvent(_) - | VirtualEvent::WheelEvent(_) - | VirtualEvent::MediaEvent(_) - | VirtualEvent::AnimationEvent(_) - | VirtualEvent::TransitionEvent(_) - | VirtualEvent::ToggleEvent(_) - | VirtualEvent::MouseEvent(_) - | VirtualEvent::PointerEvent(_) => true, - - VirtualEvent::AsyncEvent { .. } | VirtualEvent::SuspenseEvent { .. } => false, - } - } + // ImageEvent(event_data::ImageEvent), } impl std::fmt::Debug for VirtualEvent { @@ -200,8 +65,6 @@ impl std::fmt::Debug for VirtualEvent { VirtualEvent::ToggleEvent(_) => "ToggleEvent", VirtualEvent::MouseEvent(_) => "MouseEvent", VirtualEvent::PointerEvent(_) => "PointerEvent", - VirtualEvent::AsyncEvent { .. } => "AsyncEvent", - VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent", }; f.debug_struct("VirtualEvent").field("type", &name).finish() @@ -215,8 +78,7 @@ pub mod on { //! Synthetic events are immutable and wrapped in Arc. It is the intention for Dioxus renderers to re-use the underyling //! Arc allocation through "get_mut" //! - //! - //! + //! React recently dropped support for re-using event allocation and just passes the real event along. #![allow(unused)] use bumpalo::boxed::Box as BumpBox; @@ -285,14 +147,6 @@ pub mod on { } // The Dioxus Synthetic event system - // - // - // - // - // - // - // - // event_directory! { ClipboardEventInner(ClipboardEvent): [ /// Called when "copy" diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 3a0510ace..912427efc 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -130,9 +130,9 @@ where event: VirtualEvent::AsyncEvent { should_rerender: false, }, - originator, + scope: originator, priority: EventPriority::Low, - real_node_id: None, + mounted_dom_id: None, } }))); @@ -209,9 +209,9 @@ where *slot.borrow_mut() = Some(Box::new(output) as Box); EventTrigger { event: VirtualEvent::SuspenseEvent { hook_idx, domnode }, - originator, + scope: originator, priority: EventPriority::Low, - real_node_id: None, + mounted_dom_id: None, } }))); diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index d105f9770..5db7e88d4 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -28,7 +28,6 @@ pub mod prelude { // types used internally that are important pub(crate) mod innerlude { - pub use crate::arena::*; pub use crate::bumpframe::*; pub use crate::childiter::*; pub use crate::component::*; @@ -43,6 +42,7 @@ pub(crate) mod innerlude { pub use crate::hooks::*; pub use crate::mutations::*; pub use crate::nodes::*; + pub use crate::scheduler::*; // pub use crate::scheduler::*; pub use crate::scope::*; pub use crate::util::*; @@ -56,11 +56,12 @@ pub(crate) mod innerlude { } pub mod exports { - // export important things here + //! Important dependencies that are used by the rest of the library + + // the foundation of this library pub use bumpalo; } -pub mod arena; pub mod bumpframe; pub mod childiter; pub mod component; @@ -75,7 +76,7 @@ pub mod hooklist; pub mod hooks; pub mod mutations; pub mod nodes; -// pub mod scheduler; +pub mod scheduler; pub mod scope; pub mod signals; pub mod util; diff --git a/packages/core/src/arena.rs b/packages/core/src/scheduler.rs similarity index 61% rename from packages/core/src/arena.rs rename to packages/core/src/scheduler.rs index 678bf10cc..51e3eca85 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/scheduler.rs @@ -1,4 +1,4 @@ -use std::cell::{RefCell, RefMut}; +use std::cell::{Cell, RefCell, RefMut}; use std::fmt::Display; use std::{cell::UnsafeCell, rc::Rc}; @@ -23,25 +23,68 @@ use futures_util::Future; use futures_util::FutureExt; use futures_util::StreamExt; -type UiReceiver = UnboundedReceiver; -type UiSender = UnboundedSender; +#[derive(Clone)] +pub struct EventChannel { + pub task_counter: Rc>, + pub sender: UnboundedSender, + pub schedule_any_immediate: Rc, + pub submit_task: Rc TaskHandle>, + pub get_shared_context: Rc Option>>, +} -type TaskReceiver = UnboundedReceiver; -type TaskSender = UnboundedSender; +pub enum SchedulerMsg { + Immediate(ScopeId), + UiEvent(), + SubmitTask(u64), + ToggleTask(u64), + PauseTask(u64), + ResumeTask(u64), + DropTask(u64), +} -/// These are resources shared among all the components and the virtualdom itself +/// The scheduler holds basically everything around "working" +/// +/// Each scope has the ability to lightly interact with the scheduler (IE, schedule an update) but ultimately the scheduler calls the components. +/// +/// In Dioxus, the scheduler provides 3 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs +/// out. +/// +/// Saved DiffMachine state can be self-referential, so we need to be careful about how we save it. All self-referential data is a link between +/// pending DiffInstructions, Mutations, and their underlying Scope. It's okay for us to be self-referential with this data, provided we don't priority +/// task shift to a higher priority task that needs mutable access to the same scopes. +/// +/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task. +/// +/// pub struct Scheduler { - pub components: UnsafeCell>, + /* + This *has* to be an UnsafeCell. - pub(crate) heuristics: HeuristicsEngine, + Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to + its bumpframe conents immutably. - // Used by "set_state" and co - is its own queue - pub immediate_sender: TaskSender, - pub immediate_receiver: TaskReceiver, + However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly + simple to audit. - /// Triggered by event listeners - pub ui_event_sender: UiSender, - pub ui_event_receiver: UiReceiver, + Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!) + */ + pub components: Rc>>, + + /* + Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy + that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client. + + This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse. + + In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes... + */ + pub raw_elements: Slab<()>, + + pub heuristics: HeuristicsEngine, + + pub channel: EventChannel, + + pub receiver: UnboundedReceiver, // Garbage stored pub pending_garbage: FxHashSet, @@ -49,12 +92,6 @@ pub struct Scheduler { // In-flight futures pub async_tasks: FuturesUnordered, - /// We use a SlotSet to keep track of the keys that are currently being used. - /// However, we don't store any specific data since the "mirror" - pub raw_elements: Slab<()>, - - pub task_setter: Rc, - // scheduler stuff pub current_priority: EventPriority, @@ -66,75 +103,78 @@ pub struct Scheduler { pub garbage_scopes: HashSet, - pub high_priorty: PriortySystem, - pub medium_priority: PriortySystem, - pub low_priority: PriortySystem, + pub fibers: [PriortySystem; 3], } impl Scheduler { pub fn new() -> Self { // preallocate 2000 elements and 20 scopes to avoid dynamic allocation - let components: UnsafeCell> = UnsafeCell::new(Slab::with_capacity(100)); + let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100))); // elements are super cheap - the value takes no space let raw_elements = Slab::with_capacity(2000); - let (ui_sender, ui_receiver) = futures_channel::mpsc::unbounded(); - let (immediate_sender, immediate_receiver) = futures_channel::mpsc::unbounded(); - let heuristics = HeuristicsEngine::new(); - // we allocate this task setter once to save us from having to allocate later - let task_setter = { - let queue = immediate_sender.clone(); - let components = components.clone(); - Rc::new(move |idx: ScopeId| { - let comps = unsafe { &*components.get() }; - - if let Some(scope) = comps.get(idx.0) { - // todo!("implement immediates again") - // - - // queue - // .unbounded_send(EventTrigger::new( - // V - // idx, - // None, - // EventPriority::High, - // )) - // .expect("The event queu receiver should *never* be dropped"); - } - }) as Rc + let (sender, receiver) = futures_channel::mpsc::unbounded::(); + let task_counter = Rc::new(Cell::new(0)); + let channel = EventChannel { + task_counter, + sender: sender.clone(), + schedule_any_immediate: { + Rc::new(move |id| sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap()) + }, + submit_task: Rc::new(|_| { + // + todo!() + // TaskHandle {} + }), + get_shared_context: { + let components = components.clone(); + Rc::new(|id, ty| { + let components = unsafe { &*components.get() }; + let mut search: Option<&Scope> = components.get(id.0); + while let Some(inner) = search.take() { + if let Some(shared) = inner.shared_contexts.borrow().get(&ty) { + return Some(shared.clone()); + } else { + search = inner.parent_idx.map(|id| components.get(id.0)).flatten(); + } + } + None + }) + }, }; Self { + channel, + receiver, + components, async_tasks: FuturesUnordered::new(), - ui_event_receiver: ui_receiver, - ui_event_sender: ui_sender, - - immediate_receiver: immediate_receiver, - immediate_sender: immediate_sender, - pending_garbage: FxHashSet::default(), - heuristics: heuristics, - raw_elements: raw_elements, - task_setter, + heuristics, + raw_elements, // a storage for our receiver to dump into pending_events: VecDeque::new(), + pending_immediates: VecDeque::new(), + pending_tasks: VecDeque::new(), garbage_scopes: HashSet::new(), current_priority: EventPriority::Low, - high_priorty: PriortySystem::new(), - medium_priority: PriortySystem::new(), - low_priority: PriortySystem::new(), + // a dedicated fiber for each priority + fibers: [ + PriortySystem::new(), + PriortySystem::new(), + PriortySystem::new(), + ], } } @@ -176,12 +216,12 @@ impl Scheduler { } pub fn reserve_node(&self) -> ElementId { - ElementId(self.raw_elements.borrow_mut().insert(())) + ElementId(self.raw_elements.insert(())) } /// return the id, freeing the space of the original node pub fn collect_garbage(&self, id: ElementId) { - self.raw_elements.borrow_mut().remove(id.0); + self.raw_elements.remove(id.0); } pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { @@ -192,24 +232,12 @@ impl Scheduler { id } - pub fn schedule_update(&self) -> Rc { - self.task_setter.clone() - } - - pub fn submit_task(&self, task: FiberTask) -> TaskHandle { - self.async_tasks.borrow_mut().push(task); - TaskHandle {} - } - pub fn make_trigger_key(&self, trigger: &EventTrigger) -> EventKey { - let height = self - .get_scope(trigger.originator) - .map(|f| f.height) - .unwrap(); + let height = self.get_scope(trigger.scope).map(|f| f.height).unwrap(); EventKey { height, - originator: trigger.originator, + originator: trigger.scope, priority: trigger.priority, } } @@ -356,8 +384,8 @@ impl Scheduler { | VirtualEvent::ToggleEvent(_) | VirtualEvent::MouseEvent(_) | VirtualEvent::PointerEvent(_) => { - if let Some(scope) = self.get_scope_mut(trigger.originator) { - if let Some(element) = trigger.real_node_id { + if let Some(scope) = self.get_scope_mut(trigger.scope) { + if let Some(element) = trigger.mounted_dom_id { scope.call_listener(trigger.event, element)?; let receiver = self.immediate_receiver.clone(); @@ -406,10 +434,10 @@ impl Scheduler { /// If a the fiber finishes its works (IE needs to be committed) the scheduler will drop the dirty scope pub async fn work_with_deadline<'a>( - &mut self, - mutations: &mut Mutations<'_>, + &'a mut self, + mutations: &mut Mutations<'a>, deadline: &mut Pin>>, - ) -> FiberResult { + ) -> bool { // check if we need to elevate priority self.current_priority = match ( self.high_priorty.has_work(), @@ -421,7 +449,7 @@ impl Scheduler { (false, false, _) => EventPriority::Low, }; - let mut machine = DiffMachine::new(mutations, ScopeId(0), &self); + // let mut machine = DiffMachine::new(mutations, ScopeId(0), &self); let dirty_root = { let dirty_roots = match self.current_priority { @@ -433,7 +461,7 @@ impl Scheduler { let mut dirty_root = { let root = dirty_roots.iter().next(); if root.is_none() { - return FiberResult::Done; + return true; } root.unwrap() }; @@ -449,17 +477,12 @@ impl Scheduler { dirty_root }; - match { - let fut = machine.diff_scope(*dirty_root).fuse(); - pin_mut!(fut); + let fut = machine.diff_scope(*dirty_root).fuse(); + pin_mut!(fut); - match futures_util::future::select(deadline, fut).await { - futures_util::future::Either::Left((deadline, work_fut)) => true, - futures_util::future::Either::Right((_, deadline_fut)) => false, - } - } { - true => FiberResult::Done, - false => FiberResult::Interrupted, + match futures_util::future::select(deadline, fut).await { + futures_util::future::Either::Left((deadline, work_fut)) => true, + futures_util::future::Either::Right((_, deadline_fut)) => false, } } @@ -468,45 +491,31 @@ impl Scheduler { // does not return the trigger, but caches it in the scheduler pub async fn wait_for_any_trigger( &mut self, - mut deadline: &mut Pin>>, + deadline: &mut Pin>>, ) -> bool { - use futures_util::select; + use futures_util::future::{select, Either}; - let _immediates = self.immediate_receiver.clone(); - let mut immediates = _immediates.borrow_mut(); - - let mut _ui_events = self.ui_event_receiver.clone(); - let mut ui_events = _ui_events.borrow_mut(); - - let mut _tasks = self.async_tasks.clone(); - let mut tasks = _tasks.borrow_mut(); - - // set_state called - select! { - dirty_scope = immediates.next() => { - if let Some(scope) = dirty_scope { - self.add_dirty_scope(scope, EventPriority::Low); + let event_fut = async { + match select(self.receiver.next(), self.async_tasks.next()).await { + Either::Left((msg, _other)) => { + self.handle_channel_msg(msg.unwrap()); + } + Either::Right((task, _other)) => { + // do nothing, async task will likely generate a set of scheduler messages } } + }; - ui_event = ui_events.next() => { - if let Some(event) = ui_event { - self.pending_events.push_back(event); - } - } - - async_task = tasks.next() => { - if let Some(event) = async_task { - self.pending_events.push_back(event); - } - } - - _ = deadline => { - return true; - } + pin_mut!(event_fut); + match select(event_fut, deadline).await { + Either::Left((msg, _other)) => false, + Either::Right((deadline, _)) => true, } - false + } + + pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { + // } pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { @@ -518,12 +527,24 @@ impl Scheduler { } } -pub struct TaskHandle {} +pub struct TaskHandle { + channel: EventChannel, + our_id: u64, +} impl TaskHandle { + /// Toggles this coroutine off/on. + /// + /// This method is not synchronous - your task will not stop immediately. pub fn toggle(&self) {} + + /// This method is not synchronous - your task will not stop immediately. pub fn start(&self) {} + + /// This method is not synchronous - your task will not stop immediately. pub fn stop(&self) {} + + /// This method is not synchronous - your task will not stop immediately. pub fn restart(&self) {} } @@ -567,3 +588,72 @@ impl ElementId { self.0 as u64 } } + +// // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" +// // +// // Async events don't necessarily propagate into a scope being ran. It's up to the event itself +// // to force an update for itself. +// // +// // Most async events should have a low priority. +// // +// // This type exists for the task/concurrency system to signal that a task is ready. +// // However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its +// // own re-run. +// AsyncEvent { +// should_rerender: bool, +// }, + +// // Suspense events are a type of async event generated when suspended nodes are ready to be processed. +// // +// // they have the lowest priority +// SuspenseEvent { +// hook_idx: usize, +// domnode: Rc>>, +// }, + +/// Priority of Event Triggers. +/// +/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus +/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers +/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. +/// +/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. +/// +/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, +/// we keep it simple, and just use a 3-tier priority system. +/// +/// - NoPriority = 0 +/// - LowPriority = 1 +/// - NormalPriority = 2 +/// - UserBlocking = 3 +/// - HighPriority = 4 +/// - ImmediatePriority = 5 +/// +/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. +/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be +/// flushed before proceeding. Multiple discrete events is highly unlikely, though. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum EventPriority { + /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. + /// + /// This is typically reserved for things like user interaction. + /// + /// React calls these "discrete" events, but with an extra category of "user-blocking". + High = 2, + + /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important + /// than "High Priority" events and will take presedence over low priority events. + /// + /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. + /// + /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). + Medium = 1, + + /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be + /// advanced to the front of the work queue until completed. + /// + /// The primary user of Low Priority work is the asynchronous work system (suspense). + /// + /// This is considered "idle" work or "background" work. + Low = 0, +} diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 4088a6de7..fda9960f5 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,5 +1,6 @@ use crate::innerlude::*; use bumpalo::boxed::Box as BumpBox; +use futures_channel::mpsc::UnboundedSender; use fxhash::FxHashSet; use std::{ any::{Any, TypeId}, @@ -43,10 +44,9 @@ pub struct Scope { pub(crate) hooks: HookList, pub(crate) shared_contexts: RefCell>>, - // meta - pub(crate) function_name: &'static str, - // // A reference to the resources shared by all the comonents - // pub(crate) vdom: SharedResources, + pub(crate) memoized_updater: Rc, + + pub(crate) shared: EventChannel, } // The type of closure that wraps calling components @@ -74,28 +74,22 @@ impl Scope { child_nodes: ScopeChildren, - // vdom: SharedResources, - function_name: &'static str, + shared: EventChannel, ) -> Self { let child_nodes = unsafe { child_nodes.extend_lifetime() }; - // // insert ourself as a descendent of the parent - // // when the parent is removed, this map will be traversed, and we will also be cleaned up. - // if let Some(parent) = &parent { - // let parent = unsafe { vdom.get_scope(*parent) }.unwrap(); - // parent.descendents.borrow_mut().insert(arena_idx); - // } + let up = shared.schedule_any_immediate.clone(); + let memoized_updater = Rc::new(move || up(arena_idx)); Self { - function_name, + memoized_updater, + shared, child_nodes, caller, parent_idx: parent, our_arena_idx: arena_idx, height, - // vdom, frames: ActiveFrame::new(), - hooks: Default::default(), shared_contexts: Default::default(), listeners: Default::default(), diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 8f015a784..558bb4129 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -105,18 +105,17 @@ impl VirtualDom { let root_props: Pin> = Box::pin(root_props); let props_ptr = root_props.as_ref().downcast_ref::

().unwrap() as *const P; - let link = components.clone(); + let update_sender = components.immediate_sender.clone(); let base_scope = components.insert_scope_with_key(move |myidx| { let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); let name = type_name_of(root); - Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), link, name) + Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), update_sender) }); Self { base_scope, _root_props: root_props, - scheduler: Scheduler::new(components.clone()), scheduler: components, _root_prop_type: TypeId::of::

(), } @@ -152,28 +151,29 @@ impl VirtualDom { /// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This /// method can be useful when needing to perform some crucial periodic tasks. pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> { - let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.scheduler); + todo!() + // let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.scheduler); - let cur_component = self - .scheduler - .get_scope_mut(self.base_scope) - .expect("The base scope should never be moved"); + // let cur_component = self + // .scheduler + // .get_scope_mut(self.base_scope) + // .expect("The base scope should never be moved"); - // // 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 - .stack - .create_node(cur_component.frames.fin_head(), MountType::Append); - diff_machine.work().await; - } else { - // todo: should this be a hard error? - log::warn!( - "Component failed to run succesfully during rebuild. - This does not result in a failed rebuild, but indicates a logic failure within your app." - ); - } + // // // 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 + // .stack + // .create_node(cur_component.frames.fin_head(), MountType::Append); + // diff_machine.work().await; + // } else { + // // todo: should this be a hard error? + // log::warn!( + // "Component failed to run succesfully during rebuild. + // This does not result in a failed rebuild, but indicates a logic failure within your app." + // ); + // } - diff_machine.mutations + // diff_machine.mutations } pub fn diff_sync<'s>(&'s mut self) -> Mutations<'s> { diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_iterative.rs index 0d1b6343c..445e990ed 100644 --- a/packages/core/tests/create_iterative.rs +++ b/packages/core/tests/create_iterative.rs @@ -1,7 +1,7 @@ //! tests to prove that the iterative implementation works use anyhow::{Context, Result}; -use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, DomEdit, Mutations}; +use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, DomEdit, Mutations}; mod test_logging; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 8f59165bc..e8a018e42 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -3,7 +3,7 @@ use bumpalo::Bump; use dioxus::{ - arena::Scheduler, diff::DiffMachine, prelude::*, DiffInstruction, DomEdit, MountType, + diff::DiffMachine, prelude::*, scheduler::Scheduler, DiffInstruction, DomEdit, MountType, }; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs index 869aa8fa3..2b90ca256 100644 --- a/packages/core/tests/eventsystem.rs +++ b/packages/core/tests/eventsystem.rs @@ -1,7 +1,7 @@ use bumpalo::Bump; use anyhow::{Context, Result}; -use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, DomEdit}; +use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/hooks.rs b/packages/core/tests/hooks.rs index 3946b209d..3fcc0bccd 100644 --- a/packages/core/tests/hooks.rs +++ b/packages/core/tests/hooks.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, DomEdit}; +use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/work_sync.rs b/packages/core/tests/work_sync.rs index 93f3589b1..075060c20 100644 --- a/packages/core/tests/work_sync.rs +++ b/packages/core/tests/work_sync.rs @@ -3,7 +3,7 @@ //! This means you can actually call it synchronously if you want. use anyhow::{Context, Result}; -use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, scope::Scope}; +use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, scope::Scope}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; use futures_util::FutureExt; From 41cc42919d42453f8f2560aa852211364af4ad3d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 25 Aug 2021 16:40:18 -0400 Subject: [PATCH 32/53] wip: more overhaul on virtualevents --- packages/core/src/diff.rs | 10 +-- packages/core/src/events.rs | 60 ++++++++--------- packages/core/src/hooks.rs | 111 +++++++++++++++---------------- packages/core/src/lib.rs | 2 +- packages/core/src/nodes.rs | 19 +++--- packages/core/src/scheduler.rs | 50 ++++++-------- packages/core/src/scope.rs | 14 +++- packages/core/src/virtual_dom.rs | 38 +++++------ packages/desktop/src/events.rs | 4 +- packages/web/src/dom.rs | 38 +++++------ 10 files changed, 165 insertions(+), 181 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 47d8d378c..a922cceaf 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -251,7 +251,7 @@ impl<'bump> DiffMachine<'bump> { fn create_suspended_node(&mut self, suspended: &'bump VSuspended) { let real_id = self.vdom.reserve_node(); self.mutations.create_placeholder(real_id); - suspended.node.set(Some(real_id)); + suspended.dom_id.set(Some(real_id)); self.stack.add_child_count(1); } @@ -372,7 +372,7 @@ impl<'bump> DiffMachine<'bump> { (Component(old), Component(new)) => self.diff_component_nodes(old, new), (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), (Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()), - (Suspended(old), Suspended(new)) => new.node.set(old.node.get()), + (Suspended(old), Suspended(new)) => new.dom_id.set(old.dom_id.get()), (Element(old), Element(new)) => self.diff_element_nodes(old, new), // Anything else is just a basic replace and create @@ -952,7 +952,7 @@ impl<'bump> DiffMachine<'bump> { match &search_node.take().unwrap() { VNode::Text(t) => break t.dom_id.get(), VNode::Element(t) => break t.dom_id.get(), - VNode::Suspended(t) => break t.node.get(), + VNode::Suspended(t) => break t.dom_id.get(), VNode::Anchor(t) => break t.dom_id.get(), VNode::Fragment(frag) => { @@ -983,7 +983,7 @@ impl<'bump> DiffMachine<'bump> { } VNode::Text(t) => break t.dom_id.get(), VNode::Element(t) => break t.dom_id.get(), - VNode::Suspended(t) => break t.node.get(), + VNode::Suspended(t) => break t.dom_id.get(), VNode::Anchor(t) => break t.dom_id.get(), } } @@ -1016,7 +1016,7 @@ impl<'bump> DiffMachine<'bump> { }); } VNode::Suspended(s) => { - s.node.get().map(|id| { + s.dom_id.get().map(|id| { self.mutations.remove(id.as_u64()); self.vdom.collect_garbage(id); }); diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index f74aace14..454975a7d 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -3,16 +3,7 @@ //! //! 3rd party renderers are responsible for converting their native events into these virtual event types. Events might //! be heavy or need to interact through FFI, so the events themselves are designed to be lazy. - -use std::{ - cell::{Cell, RefCell}, - rc::Rc, -}; - -use crate::{ - innerlude::{ElementId, ScopeId}, - VNode, -}; +use crate::innerlude::{ElementId, ScopeId}; #[derive(Debug)] pub struct EventTrigger { @@ -23,12 +14,15 @@ pub struct EventTrigger { pub mounted_dom_id: Option, /// The type of event - pub event: VirtualEvent, + pub event: SyntheticEvent, + /// Is consistency important for this event? + /// UI events are the only events where consistency is important. + /// All else may be batched. pub discrete: bool, } -pub enum VirtualEvent { +pub enum SyntheticEvent { KeyboardEvent(on::KeyboardEvent), TouchEvent(on::TouchEvent), MouseEvent(on::MouseEvent), @@ -47,24 +41,24 @@ pub enum VirtualEvent { // ImageEvent(event_data::ImageEvent), } -impl std::fmt::Debug for VirtualEvent { +impl std::fmt::Debug for SyntheticEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let name = match self { - VirtualEvent::ClipboardEvent(_) => "ClipboardEvent", - VirtualEvent::CompositionEvent(_) => "CompositionEvent", - VirtualEvent::KeyboardEvent(_) => "KeyboardEvent", - VirtualEvent::FocusEvent(_) => "FocusEvent", - VirtualEvent::FormEvent(_) => "FormEvent", - VirtualEvent::SelectionEvent(_) => "SelectionEvent", - VirtualEvent::TouchEvent(_) => "TouchEvent", - VirtualEvent::UIEvent(_) => "UIEvent", - VirtualEvent::WheelEvent(_) => "WheelEvent", - VirtualEvent::MediaEvent(_) => "MediaEvent", - VirtualEvent::AnimationEvent(_) => "AnimationEvent", - VirtualEvent::TransitionEvent(_) => "TransitionEvent", - VirtualEvent::ToggleEvent(_) => "ToggleEvent", - VirtualEvent::MouseEvent(_) => "MouseEvent", - VirtualEvent::PointerEvent(_) => "PointerEvent", + SyntheticEvent::ClipboardEvent(_) => "ClipboardEvent", + SyntheticEvent::CompositionEvent(_) => "CompositionEvent", + SyntheticEvent::KeyboardEvent(_) => "KeyboardEvent", + SyntheticEvent::FocusEvent(_) => "FocusEvent", + SyntheticEvent::FormEvent(_) => "FormEvent", + SyntheticEvent::SelectionEvent(_) => "SelectionEvent", + SyntheticEvent::TouchEvent(_) => "TouchEvent", + SyntheticEvent::UIEvent(_) => "UIEvent", + SyntheticEvent::WheelEvent(_) => "WheelEvent", + SyntheticEvent::MediaEvent(_) => "MediaEvent", + SyntheticEvent::AnimationEvent(_) => "AnimationEvent", + SyntheticEvent::TransitionEvent(_) => "TransitionEvent", + SyntheticEvent::ToggleEvent(_) => "ToggleEvent", + SyntheticEvent::MouseEvent(_) => "MouseEvent", + SyntheticEvent::PointerEvent(_) => "PointerEvent", }; f.debug_struct("VirtualEvent").field("type", &name).finish() @@ -90,7 +84,7 @@ pub mod on { }; use std::cell::Cell; - use super::VirtualEvent; + use super::SyntheticEvent; macro_rules! event_directory { ( $( @@ -126,12 +120,12 @@ pub mod on { { let bump = &c.bump(); - let cb: &mut dyn FnMut(VirtualEvent) = bump.alloc(move |evt: VirtualEvent| match evt { - VirtualEvent::$wrapper(event) => callback(event), - _ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!") + let cb: &mut dyn FnMut(SyntheticEvent) = bump.alloc(move |evt: SyntheticEvent| match evt { + SyntheticEvent::$wrapper(event) => callback(event), + _ => unreachable!("Downcasted SyntheticEvent to wrong event type - this is an internal bug!") }); - let callback: BumpBox = unsafe { BumpBox::from_raw(cb) }; + let callback: BumpBox = unsafe { BumpBox::from_raw(cb) }; let event_name = stringify!($name); let shortname: &'static str = &event_name[2..]; diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 912427efc..ebe448ed5 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -126,14 +126,7 @@ where let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move { *slot.as_ref().borrow_mut() = Some(output); updater(update_id); - EventTrigger { - event: VirtualEvent::AsyncEvent { - should_rerender: false, - }, - scope: originator, - priority: EventPriority::Low, - mounted_dom_id: None, - } + originator }))); TaskHook { @@ -172,71 +165,75 @@ where Out: 'static, Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> DomTree<'a> + 'static, { + /* + General strategy: + - Create a slot for the future to dump its output into + - Create a new future feeding off the user's future that feeds the output into that slot + - Submit that future as a task + - Take the task handle id and attach that to our suspended node + - when the hook runs, check if the value exists + - if it does, then we can render the node directly + - if it doesn't, then we render a suspended node along with with the callback and task id + */ cx.use_hook( move |hook_idx| { let value = Rc::new(RefCell::new(None)); - - let dom_node_id = Rc::new(empty_cell()); - let domnode = dom_node_id.clone(); - let slot = value.clone(); - let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| { - let v: std::cell::Ref>> = slot.as_ref().borrow(); - match v.as_ref() { - Some(a) => { - let v: &dyn Any = a.as_ref(); - let real_val = v.downcast_ref::().unwrap(); - user_callback(ctx, real_val) - } - None => { - // - Some(VNode::Suspended(VSuspended { - node: domnode.clone(), - })) - } - } - }); - let originator = cx.scope.our_arena_idx.clone(); - let task_fut = task_initializer(); - let domnode = dom_node_id.clone(); - let slot = value.clone(); - cx.submit_task(Box::pin(task_fut.then(move |output| async move { - // When the new value arrives, set the hooks internal slot - // Dioxus will call the user's callback to generate new nodes outside of the diffing system - *slot.borrow_mut() = Some(Box::new(output) as Box); - EventTrigger { - event: VirtualEvent::SuspenseEvent { hook_idx, domnode }, - scope: originator, - priority: EventPriority::Low, - mounted_dom_id: None, - } - }))); + let handle = cx.submit_task(Box::pin(task_initializer().then( + move |output| async move { + *slot.borrow_mut() = Some(Box::new(output) as Box); + originator + }, + ))); - SuspenseHook { - value, - callback, - dom_node_id, - } + SuspenseHook { handle, value } }, - move |hook| { - let cx = Context { - scope: &cx.scope, - props: &(), - }; - let csx = SuspendedContext { inner: cx }; - (&hook.callback)(csx) + move |hook| match hook.value.borrow().as_ref() { + Some(value) => { + let out = value.downcast_ref::().unwrap(); + let sus = SuspendedContext { + inner: Context { + props: &(), + scope: cx.scope, + }, + }; + user_callback(sus, out) + } + None => { + let value = hook.value.clone(); + + cx.render(LazyNodes::new(|f| { + let bump = f.bump(); + let g: &dyn FnOnce(SuspendedContext<'src>) -> DomTree<'src> = + bump.alloc(|sus| { + let out = value + .borrow() + .as_ref() + .unwrap() + .as_ref() + .downcast_ref::() + .unwrap(); + user_callback(sus, out) + }); + + VNode::Suspended(bump.alloc(VSuspended { + dom_id: empty_cell(), + task_id: hook.handle.our_id, + callback: RefCell::new(Some(g)), + })) + })) + } }, |_| {}, ) } pub(crate) struct SuspenseHook { + pub handle: TaskHandle, pub value: Rc>>>, - pub callback: SuspendedCallback, - pub dom_node_id: Rc>>, } type SuspendedCallback = Box Fn(SuspendedContext<'a>) -> DomTree<'a>>; pub struct SuspendedContext<'a> { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5db7e88d4..d6d4fece6 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -13,7 +13,7 @@ pub use crate::innerlude::{ format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, EventTrigger, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, - SuspendedContext, VNode, VirtualDom, VirtualEvent, FC, + SuspendedContext, SyntheticEvent, VNode, VirtualDom, FC, }; pub mod prelude { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 8671cf45d..5e2db6ef8 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,8 +4,9 @@ //! //! These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick. use crate::{ - events::VirtualEvent, + events::SyntheticEvent, innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC}, + SuspendedContext, }; use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ @@ -30,7 +31,7 @@ pub enum VNode<'src> { Component(&'src VComponent<'src>), - Suspended(VSuspended), + Suspended(&'src VSuspended<'src>), Anchor(VAnchor), } @@ -127,7 +128,7 @@ pub struct Listener<'bump> { pub mounted_node: Cell>, - pub(crate) callback: RefCell>>, + pub(crate) callback: RefCell>>, } impl Listener<'_> { @@ -168,8 +169,10 @@ pub struct VComponent<'src> { pub(crate) user_fc: *const (), } -pub struct VSuspended { - pub node: Rc>>, +pub struct VSuspended<'a> { + pub dom_id: Cell>, + pub task_id: u64, + pub callback: RefCell) -> DomTree<'a>>>, } /// This struct provides an ergonomic API to quickly build VNodes. @@ -301,12 +304,6 @@ impl<'a> NodeFactory<'a> { })) } - pub fn suspended() -> VNode<'static> { - VNode::Suspended(VSuspended { - node: Rc::new(empty_cell()), - }) - } - pub fn attr( &self, name: &'static str, diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 51e3eca85..49d4bb656 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -34,7 +34,7 @@ pub struct EventChannel { pub enum SchedulerMsg { Immediate(ScopeId), - UiEvent(), + UiEvent(EventTrigger), SubmitTask(u64), ToggleTask(u64), PauseTask(u64), @@ -232,16 +232,6 @@ impl Scheduler { id } - pub fn make_trigger_key(&self, trigger: &EventTrigger) -> EventKey { - let height = self.get_scope(trigger.scope).map(|f| f.height).unwrap(); - - EventKey { - height, - originator: trigger.scope, - priority: trigger.priority, - } - } - pub fn clean_up_garbage(&mut self) { let mut scopes_to_kill = Vec::new(); let mut garbage_list = Vec::new(); @@ -330,11 +320,11 @@ impl Scheduler { pub fn consume_pending_events(&mut self) -> Result<()> { while let Some(trigger) = self.pending_events.pop_back() { match &trigger.event { - VirtualEvent::AsyncEvent { .. } => {} + SyntheticEvent::AsyncEvent { .. } => {} // This suspense system works, but it's not the most elegant solution. // TODO: Replace this system - VirtualEvent::SuspenseEvent { hook_idx, domnode } => { + SyntheticEvent::SuspenseEvent { hook_idx, domnode } => { todo!("suspense needs to be converted into its own channel"); // // Safety: this handler is the only thing that can mutate shared items at this moment in tim @@ -369,21 +359,21 @@ impl Scheduler { // } } - VirtualEvent::ClipboardEvent(_) - | VirtualEvent::CompositionEvent(_) - | VirtualEvent::KeyboardEvent(_) - | VirtualEvent::FocusEvent(_) - | VirtualEvent::FormEvent(_) - | VirtualEvent::SelectionEvent(_) - | VirtualEvent::TouchEvent(_) - | VirtualEvent::UIEvent(_) - | VirtualEvent::WheelEvent(_) - | VirtualEvent::MediaEvent(_) - | VirtualEvent::AnimationEvent(_) - | VirtualEvent::TransitionEvent(_) - | VirtualEvent::ToggleEvent(_) - | VirtualEvent::MouseEvent(_) - | VirtualEvent::PointerEvent(_) => { + SyntheticEvent::ClipboardEvent(_) + | SyntheticEvent::CompositionEvent(_) + | SyntheticEvent::KeyboardEvent(_) + | SyntheticEvent::FocusEvent(_) + | SyntheticEvent::FormEvent(_) + | SyntheticEvent::SelectionEvent(_) + | SyntheticEvent::TouchEvent(_) + | SyntheticEvent::UIEvent(_) + | SyntheticEvent::WheelEvent(_) + | SyntheticEvent::MediaEvent(_) + | SyntheticEvent::AnimationEvent(_) + | SyntheticEvent::TransitionEvent(_) + | SyntheticEvent::ToggleEvent(_) + | SyntheticEvent::MouseEvent(_) + | SyntheticEvent::PointerEvent(_) => { if let Some(scope) = self.get_scope_mut(trigger.scope) { if let Some(element) = trigger.mounted_dom_id { scope.call_listener(trigger.event, element)?; @@ -528,8 +518,8 @@ impl Scheduler { } pub struct TaskHandle { - channel: EventChannel, - our_id: u64, + pub channel: EventChannel, + pub our_id: u64, } impl TaskHandle { diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index fda9960f5..2222a7335 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -40,6 +40,8 @@ pub struct Scope { pub(crate) listeners: RefCell>>, pub(crate) borrowed_props: RefCell>>, + pub(crate) suspended_nodes: RefCell>>, + // State pub(crate) hooks: HookList, pub(crate) shared_contexts: RefCell>>, @@ -52,8 +54,9 @@ pub struct Scope { // The type of closure that wraps calling components pub type WrappedCaller = dyn for<'b> Fn(&'b Scope) -> DomTree<'b>; -// The type of task that gets sent to the task scheduler -pub type FiberTask = Pin>>; +/// The type of task that gets sent to the task scheduler +/// Submitting a fiber task returns a handle to that task, which can be used to wake up suspended nodes +pub type FiberTask = Pin>>; impl Scope { // we are being created in the scope of an existing component (where the creator_node lifetime comes into play) @@ -91,6 +94,7 @@ impl Scope { height, frames: ActiveFrame::new(), hooks: Default::default(), + suspended_nodes: Default::default(), shared_contexts: Default::default(), listeners: Default::default(), borrowed_props: Default::default(), @@ -194,7 +198,11 @@ impl Scope { // A safe wrapper around calling listeners // // - pub(crate) fn call_listener(&mut self, event: VirtualEvent, element: ElementId) -> Result<()> { + pub(crate) fn call_listener( + &mut self, + event: SyntheticEvent, + element: ElementId, + ) -> Result<()> { let listners = self.listeners.borrow_mut(); let raw_listener = listners.iter().find(|lis| { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 558bb4129..524a5ff4c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -100,24 +100,31 @@ impl VirtualDom { /// /// Note: the VirtualDOM is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. pub fn new_with_props(root: FC

, root_props: P) -> Self { - let components = Scheduler::new(); + let scheduler = Scheduler::new(); - let root_props: Pin> = Box::pin(root_props); - let props_ptr = root_props.as_ref().downcast_ref::

().unwrap() as *const P; + let _root_props: Pin> = Box::pin(root_props); + let _root_prop_type = TypeId::of::

(); - let update_sender = components.immediate_sender.clone(); + let props_ptr = _root_props.as_ref().downcast_ref::

().unwrap() as *const P; - let base_scope = components.insert_scope_with_key(move |myidx| { + let base_scope = scheduler.insert_scope_with_key(|myidx| { let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); let name = type_name_of(root); - Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), update_sender) + Scope::new( + caller, + myidx, + None, + 0, + ScopeChildren(&[]), + scheduler.channel.clone(), + ) }); Self { base_scope, - _root_props: root_props, - scheduler: components, - _root_prop_type: TypeId::of::

(), + _root_props, + scheduler, + _root_prop_type, } } @@ -218,15 +225,6 @@ impl VirtualDom { // .expect("this future will always resolve immediately") } - /// Runs the virtualdom with no time limit. - /// - /// If there are pending tasks, they will be progressed before returning. This is useful when rendering an application - /// that has suspended nodes or suspended tasks. Be warned - any async tasks running forever will prevent this method - /// from completing. Consider using `run` and specifing a deadline. - pub async fn run_unbounded<'s>(&'s mut self) -> Mutations<'s> { - self.run_with_deadline(async {}).await.unwrap() - } - /// Run the virtualdom with a deadline. /// /// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline, @@ -327,8 +325,8 @@ impl VirtualDom { } } - pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - self.scheduler.ui_event_sender.clone() + pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { + self.scheduler.channel.sender.clone() } pub fn has_work(&self) -> bool { diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 308726f51..6d4c8ddd5 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use dioxus_core::{ events::{ on::{MouseEvent, MouseEventInner}, - VirtualEvent, + SyntheticEvent, }, ElementId, EventPriority, EventTrigger, ScopeId, }; @@ -21,7 +21,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> EventTrigger { let mut data: Vec = serde_json::from_value(val).unwrap(); let data = data.drain(..).next().unwrap(); - let event = VirtualEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent))); + let event = SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent))); let scope = ScopeId(data.scope as usize); let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize)); let priority = EventPriority::High; diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index d48e8d187..cb907cdc6 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc}; use dioxus_core::{ - events::{on::GenericEventInner, EventTrigger, VirtualEvent}, + events::{on::GenericEventInner, EventTrigger, SyntheticEvent}, mutations::NodeRefMutation, DomEdit, ElementId, ScopeId, }; @@ -436,82 +436,82 @@ impl Stack { } } -fn virtual_event_from_websys_event(event: web_sys::Event) -> VirtualEvent { +fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent { use crate::events::*; use dioxus_core::events::on::*; match event.type_().as_str() { "copy" | "cut" | "paste" => { - VirtualEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent(event)))) + SyntheticEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent(event)))) } "compositionend" | "compositionstart" | "compositionupdate" => { let evt: web_sys::CompositionEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt)))) + SyntheticEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt)))) } "keydown" | "keypress" | "keyup" => { let evt: web_sys::KeyboardEvent = event.dyn_into().unwrap(); - VirtualEvent::KeyboardEvent(KeyboardEvent(Rc::new(WebsysKeyboardEvent(evt)))) + SyntheticEvent::KeyboardEvent(KeyboardEvent(Rc::new(WebsysKeyboardEvent(evt)))) } "focus" | "blur" => { let evt: web_sys::FocusEvent = event.dyn_into().unwrap(); - VirtualEvent::FocusEvent(FocusEvent(Rc::new(WebsysFocusEvent(evt)))) + SyntheticEvent::FocusEvent(FocusEvent(Rc::new(WebsysFocusEvent(evt)))) } "change" => { let evt = event.dyn_into().unwrap(); - VirtualEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) } "input" | "invalid" | "reset" | "submit" => { let evt: web_sys::InputEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::FormEvent(FormEvent(Rc::new(WebsysFormEvent(evt)))) + SyntheticEvent::FormEvent(FormEvent(Rc::new(WebsysFormEvent(evt)))) } "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { let evt: web_sys::MouseEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::MouseEvent(MouseEvent(Rc::new(WebsysMouseEvent(evt)))) + SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebsysMouseEvent(evt)))) } "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { let evt: web_sys::PointerEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::PointerEvent(PointerEvent(Rc::new(WebsysPointerEvent(evt)))) + SyntheticEvent::PointerEvent(PointerEvent(Rc::new(WebsysPointerEvent(evt)))) } "select" => { let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::SelectionEvent(SelectionEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::SelectionEvent(SelectionEvent(Rc::new(WebsysGenericUiEvent(evt)))) } "touchcancel" | "touchend" | "touchmove" | "touchstart" => { let evt: web_sys::TouchEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::TouchEvent(TouchEvent(Rc::new(WebsysTouchEvent(evt)))) + SyntheticEvent::TouchEvent(TouchEvent(Rc::new(WebsysTouchEvent(evt)))) } "scroll" => { let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) } "wheel" => { let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::WheelEvent(WheelEvent(Rc::new(WebsysWheelEvent(evt)))) + SyntheticEvent::WheelEvent(WheelEvent(Rc::new(WebsysWheelEvent(evt)))) } "animationstart" | "animationend" | "animationiteration" => { let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::AnimationEvent(AnimationEvent(Rc::new(WebsysAnimationEvent(evt)))) + SyntheticEvent::AnimationEvent(AnimationEvent(Rc::new(WebsysAnimationEvent(evt)))) } "transitionend" => { let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::TransitionEvent(TransitionEvent(Rc::new(WebsysTransitionEvent(evt)))) + SyntheticEvent::TransitionEvent(TransitionEvent(Rc::new(WebsysTransitionEvent(evt)))) } "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate" | "volumechange" | "waiting" => { let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::MediaEvent(MediaEvent(Rc::new(WebsysMediaEvent(evt)))) + SyntheticEvent::MediaEvent(MediaEvent(Rc::new(WebsysMediaEvent(evt)))) } "toggle" => { let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::ToggleEvent(ToggleEvent(Rc::new(WebsysToggleEvent(evt)))) + SyntheticEvent::ToggleEvent(ToggleEvent(Rc::new(WebsysToggleEvent(evt)))) } _ => { let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); - VirtualEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) } } } From 07efb6a1b656b24eb1ff25c62f84bb529df936b2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 25 Aug 2021 17:09:16 -0400 Subject: [PATCH 33/53] wip: it compiles again --- packages/core/.vscode/settings.json | 2 +- packages/core/src/diff.rs | 4 +- packages/core/src/hooks.rs | 8 +- packages/core/src/scheduler.rs | 380 +++++++++++++--------------- packages/core/src/virtual_dom.rs | 53 +--- 5 files changed, 190 insertions(+), 257 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 79505b018..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index a922cceaf..4c8a2177d 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -107,7 +107,7 @@ use DomEdit::*; pub struct DiffMachine<'bump> { vdom: &'bump Scheduler, - pub mutations: &'bump mut Mutations<'bump>, + pub mutations: Mutations<'bump>, pub stack: DiffStack<'bump>, pub diffed: FxHashSet, @@ -116,7 +116,7 @@ pub struct DiffMachine<'bump> { impl<'bump> DiffMachine<'bump> { pub(crate) fn new( - edits: &'bump mut Mutations<'bump>, + edits: Mutations<'bump>, cur_scope: ScopeId, shared: &'bump Scheduler, ) -> Self { diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index ebe448ed5..b8808993e 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -208,14 +208,16 @@ where cx.render(LazyNodes::new(|f| { let bump = f.bump(); let g: &dyn FnOnce(SuspendedContext<'src>) -> DomTree<'src> = - bump.alloc(|sus| { - let out = value - .borrow() + bump.alloc(move |sus| { + let val = value.borrow(); + + let out = val .as_ref() .unwrap() .as_ref() .downcast_ref::() .unwrap(); + user_callback(sus, out) }); diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 49d4bb656..94d0c53cd 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -95,7 +95,7 @@ pub struct Scheduler { // scheduler stuff pub current_priority: EventPriority, - pub pending_events: VecDeque, + pub ui_events: VecDeque, pub pending_immediates: VecDeque, @@ -131,7 +131,7 @@ impl Scheduler { }), get_shared_context: { let components = components.clone(); - Rc::new(|id, ty| { + Rc::new(move |id, ty| { let components = unsafe { &*components.get() }; let mut search: Option<&Scope> = components.get(id.0); while let Some(inner) = search.take() { @@ -159,7 +159,7 @@ impl Scheduler { raw_elements, // a storage for our receiver to dump into - pending_events: VecDeque::new(), + ui_events: VecDeque::new(), pending_immediates: VecDeque::new(), @@ -216,12 +216,14 @@ impl Scheduler { } pub fn reserve_node(&self) -> ElementId { - ElementId(self.raw_elements.insert(())) + todo!("reserving wip until it's fast enough again") + // ElementId(self.raw_elements.insert(())) } /// return the id, freeing the space of the original node pub fn collect_garbage(&self, id: ElementId) { - self.raw_elements.remove(id.0); + todo!("garabge collection currently WIP") + // self.raw_elements.remove(id.0); } pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { @@ -233,163 +235,101 @@ impl Scheduler { } pub fn clean_up_garbage(&mut self) { - let mut scopes_to_kill = Vec::new(); - let mut garbage_list = Vec::new(); + // let mut scopes_to_kill = Vec::new(); + // let mut garbage_list = Vec::new(); - for scope in self.garbage_scopes.drain() { - let scope = self.get_scope_mut(scope).unwrap(); - for node in scope.consume_garbage() { - garbage_list.push(node); - } + todo!("garbage collection is currently immediate") + // for scope in self.garbage_scopes.drain() { + // let scope = self.get_scope_mut(scope).unwrap(); + // for node in scope.consume_garbage() { + // garbage_list.push(node); + // } - while let Some(node) = garbage_list.pop() { - match &node { - VNode::Text(_) => { - self.collect_garbage(node.direct_id()); - } - VNode::Anchor(_) => { - self.collect_garbage(node.direct_id()); - } - VNode::Suspended(_) => { - self.collect_garbage(node.direct_id()); - } + // while let Some(node) = garbage_list.pop() { + // match &node { + // VNode::Text(_) => { + // self.collect_garbage(node.direct_id()); + // } + // VNode::Anchor(_) => { + // self.collect_garbage(node.direct_id()); + // } + // VNode::Suspended(_) => { + // self.collect_garbage(node.direct_id()); + // } - VNode::Element(el) => { - self.collect_garbage(node.direct_id()); - for child in el.children { - garbage_list.push(child); - } - } + // VNode::Element(el) => { + // self.collect_garbage(node.direct_id()); + // for child in el.children { + // garbage_list.push(child); + // } + // } - VNode::Fragment(frag) => { - for child in frag.children { - garbage_list.push(child); - } - } + // VNode::Fragment(frag) => { + // for child in frag.children { + // garbage_list.push(child); + // } + // } - VNode::Component(comp) => { - // TODO: run the hook destructors and then even delete the scope + // VNode::Component(comp) => { + // // TODO: run the hook destructors and then even delete the scope - let scope_id = comp.ass_scope.get().unwrap(); - let scope = self.get_scope(scope_id).unwrap(); - let root = scope.root(); - garbage_list.push(root); - scopes_to_kill.push(scope_id); - } - } - } - } + // let scope_id = comp.ass_scope.get().unwrap(); + // let scope = self.get_scope(scope_id).unwrap(); + // let root = scope.root(); + // garbage_list.push(root); + // scopes_to_kill.push(scope_id); + // } + // } + // } + // } - for scope in scopes_to_kill.drain(..) { - // - // kill em - } - } - - // channels don't have these methods, so we just implement our own wrapper - pub fn next_event(&mut self) -> Option { - // pop the last event off the internal queue - self.pending_events.pop_back().or_else(|| { - self.ui_event_receiver - .borrow_mut() - .try_next() - .ok() - .flatten() - }) + // for scope in scopes_to_kill.drain(..) { + // // + // // kill em + // } } pub fn manually_poll_events(&mut self) { - // 1. Poll the UI event receiver - // 2. Poll the set_state receiver - - // poll the immediates first, creating work out of them - let shared_receiver = self.immediate_receiver.clone(); - let mut receiver = shared_receiver.borrow_mut(); - while let Ok(Some(trigger)) = receiver.try_next() { - self.add_dirty_scope(trigger, EventPriority::Low); - } - - // next poll the UI events, - let mut receiver = self.ui_event_receiver.borrow_mut(); - while let Ok(Some(trigger)) = receiver.try_next() { - self.pending_events.push_back(trigger); + while let Ok(Some(msg)) = self.receiver.try_next() { + self.handle_channel_msg(msg); } } // Converts UI events into dirty scopes with various priorities pub fn consume_pending_events(&mut self) -> Result<()> { - while let Some(trigger) = self.pending_events.pop_back() { - match &trigger.event { - SyntheticEvent::AsyncEvent { .. } => {} + // while let Some(trigger) = self.ui_events.pop_back() { + // match &trigger.event { + // SyntheticEvent::ClipboardEvent(_) + // | SyntheticEvent::CompositionEvent(_) + // | SyntheticEvent::KeyboardEvent(_) + // | SyntheticEvent::FocusEvent(_) + // | SyntheticEvent::FormEvent(_) + // | SyntheticEvent::SelectionEvent(_) + // | SyntheticEvent::TouchEvent(_) + // | SyntheticEvent::UIEvent(_) + // | SyntheticEvent::WheelEvent(_) + // | SyntheticEvent::MediaEvent(_) + // | SyntheticEvent::AnimationEvent(_) + // | SyntheticEvent::TransitionEvent(_) + // | SyntheticEvent::ToggleEvent(_) + // | SyntheticEvent::MouseEvent(_) + // | SyntheticEvent::PointerEvent(_) => { + // if let Some(scope) = self.get_scope_mut(trigger.scope) { + // if let Some(element) = trigger.mounted_dom_id { + // scope.call_listener(trigger.event, element)?; - // This suspense system works, but it's not the most elegant solution. - // TODO: Replace this system - SyntheticEvent::SuspenseEvent { hook_idx, domnode } => { - todo!("suspense needs to be converted into its own channel"); + // // let receiver = self.immediate_receiver.clone(); + // // let mut receiver = receiver.borrow_mut(); - // // Safety: this handler is the only thing that can mutate shared items at this moment in tim - // let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - // // safety: we are sure that there are no other references to the inner content of suspense hooks - // let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - - // let cx = Context { scope, props: &() }; - // let scx = SuspendedContext { inner: cx }; - - // // generate the new node! - // let nodes: Option = (&hook.callback)(scx); - - // if let Some(nodes) = nodes { - // // allocate inside the finished frame - not the WIP frame - // let nodes = scope.frames.finished_frame().bump.alloc(nodes); - - // // push the old node's root onto the stack - // let real_id = domnode.get().ok_or(Error::NotMounted)?; - // diff_machine.edit_push_root(real_id); - - // // push these new nodes onto the diff machines stack - // let meta = diff_machine.create_vnode(&*nodes); - - // // replace the placeholder with the new nodes we just pushed on the stack - // diff_machine.edit_replace_with(1, meta.added_to_stack); - // } else { - // log::warn!( - // "Suspense event came through, but there were no generated nodes >:(." - // ); - // } - } - - SyntheticEvent::ClipboardEvent(_) - | SyntheticEvent::CompositionEvent(_) - | SyntheticEvent::KeyboardEvent(_) - | SyntheticEvent::FocusEvent(_) - | SyntheticEvent::FormEvent(_) - | SyntheticEvent::SelectionEvent(_) - | SyntheticEvent::TouchEvent(_) - | SyntheticEvent::UIEvent(_) - | SyntheticEvent::WheelEvent(_) - | SyntheticEvent::MediaEvent(_) - | SyntheticEvent::AnimationEvent(_) - | SyntheticEvent::TransitionEvent(_) - | SyntheticEvent::ToggleEvent(_) - | SyntheticEvent::MouseEvent(_) - | SyntheticEvent::PointerEvent(_) => { - if let Some(scope) = self.get_scope_mut(trigger.scope) { - if let Some(element) = trigger.mounted_dom_id { - scope.call_listener(trigger.event, element)?; - - let receiver = self.immediate_receiver.clone(); - let mut receiver = receiver.borrow_mut(); - - // Drain the immediates into the dirty scopes, setting the appropiate priorities - while let Ok(Some(dirty_scope)) = receiver.try_next() { - self.add_dirty_scope(dirty_scope, trigger.priority) - } - } - } - } - } - } + // // // Drain the immediates into the dirty scopes, setting the appropiate priorities + // // while let Ok(Some(dirty_scope)) = receiver.try_next() { + // // self.add_dirty_scope(dirty_scope, trigger.priority) + // // } + // } + // } + // } + // } + // } Ok(()) } @@ -400,13 +340,14 @@ impl Scheduler { } pub fn has_pending_events(&self) -> bool { - self.pending_events.len() > 0 + self.ui_events.len() > 0 } pub fn has_work(&self) -> bool { - self.high_priorty.has_work() - || self.medium_priority.has_work() - || self.low_priority.has_work() + todo!() + // self.high_priorty.has_work() + // || self.medium_priority.has_work() + // || self.low_priority.has_work() } pub fn has_pending_garbage(&self) -> bool { @@ -414,66 +355,104 @@ impl Scheduler { } 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) } + todo!() + // 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 async fn work_with_deadline<'a>( &'a mut self, - mutations: &mut Mutations<'a>, deadline: &mut Pin>>, - ) -> bool { - // check if we need to elevate priority - self.current_priority = match ( - self.high_priorty.has_work(), - self.medium_priority.has_work(), - self.low_priority.has_work(), - ) { - (true, _, _) => EventPriority::High, - (false, true, _) => EventPriority::Medium, - (false, false, _) => EventPriority::Low, - }; + ) -> Vec> { + let mut committed_mutations = Vec::new(); - // let mut machine = DiffMachine::new(mutations, ScopeId(0), &self); + // TODO: + // the scheduler uses a bunch of different receivers to mimic a "topic" queue system. The futures-channel implementation + // doesn't really have a concept of a "topic" queue, so there's a lot of noise in the hand-rolled scheduler. We should + // explore abstracting the scheduler into a topic-queue channel system - similar to Kafka or something similar. + loop { + // Internalize any pending work since the last time we ran + self.manually_poll_events(); - let dirty_root = { - let dirty_roots = match self.current_priority { - EventPriority::High => &self.high_priorty.dirty_scopes, - EventPriority::Medium => &self.medium_priority.dirty_scopes, - EventPriority::Low => &self.low_priority.dirty_scopes, - }; - let mut height = 0; - let mut dirty_root = { - let root = dirty_roots.iter().next(); - if root.is_none() { - return true; - } - root.unwrap() - }; + // Wait for any new events if we have nothing to do + if !self.has_any_work() { + self.clean_up_garbage(); + let deadline_expired = self.wait_for_any_trigger(deadline).await; - for root in dirty_roots { - if let Some(scope) = self.get_scope(*root) { - if scope.height < height { - height = scope.height; - dirty_root = root; - } + if deadline_expired { + return committed_mutations; } } - dirty_root - }; - let fut = machine.diff_scope(*dirty_root).fuse(); - pin_mut!(fut); + // Create work from the pending event queue + self.consume_pending_events().unwrap(); - match futures_util::future::select(deadline, fut).await { - futures_util::future::Either::Left((deadline, work_fut)) => true, - futures_util::future::Either::Right((_, deadline_fut)) => false, + // Work through the current subtree, and commit the results when it finishes + // When the deadline expires, give back the work + let mut new_mutations = Mutations::new(); + // match self.work_with_deadline(&mut deadline).await { + // Some(mutations) => { + // // safety: the scheduler will never let us mutate + // let extended: Mutations<'static> = unsafe { std::mem::transmute(mutations) }; + // committed_mutations.push(extended) + // } + // None => return committed_mutations, + // } } + // // check if we need to elevate priority + // self.current_priority = match ( + // self.high_priorty.has_work(), + // self.medium_priority.has_work(), + // self.low_priority.has_work(), + // ) { + // (true, _, _) => EventPriority::High, + // (false, true, _) => EventPriority::Medium, + // (false, false, _) => EventPriority::Low, + // }; + + // // let mut machine = DiffMachine::new(mutations, ScopeId(0), &self); + + // let dirty_root = { + // let dirty_roots = match self.current_priority { + // EventPriority::High => &self.high_priorty.dirty_scopes, + // EventPriority::Medium => &self.medium_priority.dirty_scopes, + // EventPriority::Low => &self.low_priority.dirty_scopes, + // }; + // let mut height = 0; + // let mut dirty_root = { + // let root = dirty_roots.iter().next(); + // if root.is_none() { + // return true; + // } + // root.unwrap() + // }; + + // for root in dirty_roots { + // if let Some(scope) = self.get_scope(*root) { + // if scope.height < height { + // height = scope.height; + // dirty_root = root; + // } + // } + // } + // dirty_root + // }; + + // let fut = machine.diff_scope(*dirty_root).fuse(); + // pin_mut!(fut); + + // match futures_util::future::select(deadline, fut).await { + // futures_util::future::Either::Left((deadline, work_fut)) => true, + // futures_util::future::Either::Right((_, deadline_fut)) => false, + // } } // waits for a trigger, canceling early if the deadline is reached @@ -509,11 +488,12 @@ impl Scheduler { } pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { - match priority { - EventPriority::High => self.high_priorty.dirty_scopes.insert(scope), - EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope), - EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), - }; + todo!() + // match priority { + // EventPriority::High => self.high_priorty.dirty_scopes.insert(scope), + // EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope), + // EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), + // }; } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 524a5ff4c..c37a70a46 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -271,58 +271,9 @@ impl VirtualDom { pub async fn run_with_deadline<'s>( &'s mut self, deadline: impl Future, - ) -> Option>> { - let mut committed_mutations = Vec::new(); + ) -> Vec> { let mut deadline = Box::pin(deadline.fuse()); - - // TODO: - // the scheduler uses a bunch of different receivers to mimic a "topic" queue system. The futures-channel implementation - // doesn't really have a concept of a "topic" queue, so there's a lot of noise in the hand-rolled scheduler. We should - // explore abstracting the scheduler into a topic-queue channel system - similar to Kafka or something similar. - loop { - // Internalize any pending work since the last time we ran - self.scheduler.manually_poll_events(); - - // Wait for any new events if we have nothing to do - if !self.scheduler.has_any_work() { - self.scheduler.clean_up_garbage(); - let deadline_expired = self.scheduler.wait_for_any_trigger(&mut deadline).await; - - if deadline_expired { - return Some(committed_mutations); - } - } - - // Create work from the pending event queue - self.scheduler.consume_pending_events().unwrap(); - - // Work through the current subtree, and commit the results when it finishes - // When the deadline expires, give back the work - let mut new_mutations = Mutations::new(); - match self.scheduler.work_with_deadline(&mut deadline).await { - FiberResult::Done => { - // return Some(mutations); - // for edit in mutations.edits { - // committed_mutations.edits.push(edit); - // } - // committed_mutations.extend(&mut mutations); - - /* - quick return if there's no work left, so we can commit before the deadline expires - When we loop over again, we'll re-wait for any new work. - - I'm not quite sure how this *should* work. - - It makes sense to try and progress the DOM faster - */ - - // if !self.scheduler.has_any_work() { - // return Some(committed_mutations); - // } - } - FiberResult::Interrupted => return None, - } - } + self.scheduler.work_with_deadline(&mut deadline).await } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { From d4d7114bebe09a5e265a29105a9bdf3b369ecd67 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 25 Aug 2021 17:17:44 -0400 Subject: [PATCH 34/53] wip: more work on scheduler --- packages/core/.vscode/settings.json | 2 +- packages/core/src/scheduler.rs | 34 +++++++++++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 94d0c53cd..9e48e4db4 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -35,7 +35,7 @@ pub struct EventChannel { pub enum SchedulerMsg { Immediate(ScopeId), UiEvent(EventTrigger), - SubmitTask(u64), + SubmitTask(FiberTask, u64), ToggleTask(u64), PauseTask(u64), ResumeTask(u64), @@ -108,27 +108,39 @@ pub struct Scheduler { impl Scheduler { pub fn new() -> Self { - // preallocate 2000 elements and 20 scopes to avoid dynamic allocation + /* + Preallocate 2000 elements and 100 scopes to avoid dynamic allocation. + Perhaps this should be configurable? + */ let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100))); - - // elements are super cheap - the value takes no space let raw_elements = Slab::with_capacity(2000); let heuristics = HeuristicsEngine::new(); let (sender, receiver) = futures_channel::mpsc::unbounded::(); let task_counter = Rc::new(Cell::new(0)); + let channel = EventChannel { - task_counter, + task_counter: task_counter.clone(), sender: sender.clone(), schedule_any_immediate: { + let sender = sender.clone(); Rc::new(move |id| sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap()) }, - submit_task: Rc::new(|_| { - // - todo!() - // TaskHandle {} - }), + submit_task: { + let sender = sender.clone(); + Rc::new(move |fiber_task| { + let task_id = task_counter.get(); + task_counter.set(task_id + 1); + sender + .unbounded_send(SchedulerMsg::SubmitTask(fiber_task, task_id)) + .unwrap(); + TaskHandle { + our_id: task_id, + sender: sender.clone(), + } + }) + }, get_shared_context: { let components = components.clone(); Rc::new(move |id, ty| { @@ -498,7 +510,7 @@ impl Scheduler { } pub struct TaskHandle { - pub channel: EventChannel, + pub sender: UnboundedSender, pub our_id: u64, } From ac3a7b1fbc1665482354ddf4f0de3d6ceb5b400a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 26 Aug 2021 01:52:50 -0400 Subject: [PATCH 35/53] wip: considering making vdom passed verywehre --- packages/core/.vscode/settings.json | 2 +- packages/core/Cargo.toml | 2 + packages/core/src/diff.rs | 13 +- packages/core/src/diff_stack.rs | 4 +- packages/core/src/scheduler.rs | 176 ++++++++++++++++++++-------- packages/core/src/virtual_dom.rs | 9 +- 6 files changed, 141 insertions(+), 65 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 79505b018..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index c55ddecd1..96aa0da97 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -42,6 +42,8 @@ once_cell = "1.8.0" # # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } +indexmap = "1.7.0" + [dev-dependencies] anyhow = "1.0.42" diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 4c8a2177d..da1cd5673 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -105,23 +105,21 @@ use DomEdit::*; /// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's /// stack machines all the way down! pub struct DiffMachine<'bump> { - vdom: &'bump Scheduler, + pub vdom: &'bump Scheduler, pub mutations: Mutations<'bump>, pub stack: DiffStack<'bump>, + pub diffed: FxHashSet, + pub seen_scopes: FxHashSet, } impl<'bump> DiffMachine<'bump> { - pub(crate) fn new( - edits: Mutations<'bump>, - cur_scope: ScopeId, - shared: &'bump Scheduler, - ) -> Self { + pub(crate) fn new(edits: Mutations<'bump>, shared: &'bump Scheduler) -> Self { Self { - stack: DiffStack::new(cur_scope), + stack: DiffStack::new(), mutations: edits, vdom: shared, diffed: FxHashSet::default(), @@ -138,6 +136,7 @@ impl<'bump> DiffMachine<'bump> { // pub async fn diff_scope(&mut self, id: ScopeId) { if let Some(component) = self.vdom.get_scope_mut(id) { + self.stack.scope_stack.push(id); let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); self.stack.push(DiffInstruction::DiffNode { new, old }); self.work().await; diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index 0150d8d51..98e41c88b 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -47,11 +47,11 @@ pub struct DiffStack<'bump> { } impl<'bump> DiffStack<'bump> { - pub fn new(cur_scope: ScopeId) -> Self { + pub fn new() -> Self { Self { instructions: Vec::with_capacity(1000), nodes_created_stack: smallvec![], - scope_stack: smallvec![cur_scope], + scope_stack: smallvec![], } } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 9e48e4db4..b3c0c148a 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -1,3 +1,73 @@ +/* +Welcome to Dioxus's cooperative, priority-based scheduler. + +I hope you enjoy your stay. + +Some essential reading: +- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200 +- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440 +- https://github.com/WICG/is-input-pending +- https://web.dev/rail/ +- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react + +# What's going on? + +Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI +snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no +point if being "fast" if you can't also be "responsive." + +As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned +priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms). +The controller responsible for this priority management is called the "scheduler" and is responsible for juggle many +different types of work simultaneously. + +# How does it work? + +Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked. +React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of +React, and the UI still stays snappy! + +While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do +this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser +has time to paint and run animations. When the browser is finished, we can step in and continue our work. + +React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not +necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially +"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause +our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields +so Rust's async machinery can take over, allowing us to customize when exactly to pause it. + +React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For +now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes +before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes +are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine +future is paused and saved (self-referentially). + +# Priorty System + +So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that +doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where +priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as +a "lower" priority. That way, we can interrupt long-running low-prioty work with short-running high-priority work. + +React's priority system is quite complex. + +There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only +uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on. + +For Dioxus, we're going with a 4 tier priorty system: +- Sync: Things that need to be done by the next frame, like TextInput on controlled elements +- High: for events that block all others - clicks, keyboard, and hovers +- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events) +- Low: set_state called asynchronously, and anything generated by suspense + +In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed +work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave +as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to +do anything too arduous from onInput. + +For the rest, we defer to the rIC period and work down each queue from high to low. +*/ use std::cell::{Cell, RefCell, RefMut}; use std::fmt::Display; use std::{cell::UnsafeCell, rc::Rc}; @@ -7,6 +77,7 @@ use crate::innerlude::*; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::stream::FuturesUnordered; use fxhash::{FxHashMap, FxHashSet}; +use indexmap::IndexSet; use slab::Slab; use smallvec::SmallVec; @@ -376,14 +447,42 @@ impl Scheduler { // unsafe { std::mem::transmute(fib) } } - /// If a the fiber finishes its works (IE needs to be committed) the scheduler will drop the dirty scope + /// The primary workhorse of the VirtualDOM. /// + /// Uses some fairly complex logic to schedule what work should be produced. + /// + /// Returns a list of successful mutations. /// /// pub async fn work_with_deadline<'a>( &'a mut self, deadline: &mut Pin>>, ) -> Vec> { + /* + Strategy: + - When called, check for any UI events that might've been received since the last frame. + - Dump all UI events into a "pending discrete" queue and a "pending continuous" queue. + + - If there are any pending discrete events, then elevate our priorty level. If our priority level is already "high," + then we need to finish the high priority work first. If the current work is "low" then analyze what scopes + will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need + to bump the other work out of the way, or choose to process it so we don't have any conflicts. + 'static components have a leg up here since their work can be re-used among multiple scopes. + "High priority" is only for blocking! Should only be used on "clicks" + + - If there are no pending discrete events, then check for continuous events. These can be completely batched + + + Open questions: + - what if we get two clicks from the component during the same slice? + - should we batch? + - react says no - they are continuous + - but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff? + + + + + */ let mut committed_mutations = Vec::new(); // TODO: @@ -509,6 +608,28 @@ impl Scheduler { } } +pub struct PriortySystem { + pub dirty_scopes: IndexSet, + pub machine: DiffMachine<'static>, +} + +impl PriortySystem { + pub fn new() -> Self { + Self { + machine: DiffMachine::new(edits, shared), + dirty_scopes: Default::default(), + } + } + + fn has_work(&self) -> bool { + todo!() + } + + fn work(&mut self) { + let scope = self.dirty_scopes.pop(); + } +} + pub struct TaskHandle { pub sender: UnboundedSender, pub our_id: u64, @@ -530,30 +651,6 @@ impl TaskHandle { pub fn restart(&self) {} } -#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)] -pub struct DirtyScope { - height: u32, - start_tick: u32, -} - -pub struct PriortySystem { - pub pending_scopes: Vec, - pub dirty_scopes: HashSet, -} - -impl PriortySystem { - pub fn new() -> Self { - Self { - pending_scopes: Default::default(), - dirty_scopes: Default::default(), - } - } - - fn has_work(&self) -> bool { - self.pending_scopes.len() > 0 || self.dirty_scopes.len() > 0 - } -} - #[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct ScopeId(pub usize); @@ -571,28 +668,6 @@ impl ElementId { } } -// // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" -// // -// // Async events don't necessarily propagate into a scope being ran. It's up to the event itself -// // to force an update for itself. -// // -// // Most async events should have a low priority. -// // -// // This type exists for the task/concurrency system to signal that a task is ready. -// // However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its -// // own re-run. -// AsyncEvent { -// should_rerender: bool, -// }, - -// // Suspense events are a type of async event generated when suspended nodes are ready to be processed. -// // -// // they have the lowest priority -// SuspenseEvent { -// hook_idx: usize, -// domnode: Rc>>, -// }, - /// Priority of Event Triggers. /// /// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus @@ -616,11 +691,16 @@ impl ElementId { /// flushed before proceeding. Multiple discrete events is highly unlikely, though. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] pub enum EventPriority { + /// Work that must be completed during the EventHandler phase + /// + /// + Immediate = 3, + /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. /// /// This is typically reserved for things like user interaction. /// - /// React calls these "discrete" events, but with an extra category of "user-blocking". + /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). High = 2, /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index c37a70a46..861642f03 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -194,7 +194,7 @@ impl VirtualDom { } pub async fn diff_async<'s>(&'s mut self) -> Mutations<'s> { - let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.scheduler); + let mut diff_machine = DiffMachine::new(Mutations::new(), &self.scheduler); let cur_component = self .scheduler @@ -203,12 +203,7 @@ impl VirtualDom { cur_component.run_scope().unwrap(); - diff_machine.stack.push(DiffInstruction::DiffNode { - old: cur_component.frames.wip_head(), - new: cur_component.frames.fin_head(), - }); - - diff_machine.work().await; + diff_machine.diff_scope(self.base_scope).await; diff_machine.mutations } From bcaa76a37fb7ff48ab3345c594a20fbdcda6074a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 26 Aug 2021 02:41:30 -0400 Subject: [PATCH 36/53] wip: some structure --- packages/core/src/diff.rs | 101 ++++++++++++++++++++++------ packages/core/src/scheduler.rs | 112 +++++++++++++++++++++++++------ packages/core/src/virtual_dom.rs | 4 +- 3 files changed, 173 insertions(+), 44 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index da1cd5673..f90fa98eb 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -90,6 +90,7 @@ use crate::{innerlude::*, scheduler::Scheduler}; use fxhash::{FxHashMap, FxHashSet}; +use slab::Slab; use DomEdit::*; /// Our DiffMachine is an iterative tree differ. @@ -105,19 +106,69 @@ use DomEdit::*; /// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's /// stack machines all the way down! pub struct DiffMachine<'bump> { - pub vdom: &'bump Scheduler, - + vdom: SharedVdom<'bump>, pub mutations: Mutations<'bump>, - pub stack: DiffStack<'bump>, - pub diffed: FxHashSet, - pub seen_scopes: FxHashSet, } +pub struct SharedVdom<'bump> { + pub components: &'bump mut Slab, + pub elements: &'bump mut Slab<()>, + pub channel: EventChannel, +} + +impl<'bump> SharedVdom<'bump> { + fn get_scope_mut(&mut self, scope: ScopeId) -> Option<&'bump mut Scope> { + todo!() + } + + fn get_scope(&mut self, scope: ScopeId) -> Option<&'bump Scope> { + todo!() + } + + fn reserve_node(&mut self) -> ElementId { + todo!() + } + fn collect_garbage(&mut self, element: ElementId) {} + + pub fn insert_scope_with_key(&mut self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { + let entry = self.components.vacant_entry(); + let id = ScopeId(entry.key()); + entry.insert(f(id)); + id + } +} + +/// a "saved" form of a diff machine +/// in regular diff machine, the &'bump reference is a stack borrow, but the +/// bump lifetimes are heap borrows. +pub struct SavedDiffWork<'bump> { + pub mutations: Mutations<'bump>, + pub stack: DiffStack<'bump>, + pub diffed: FxHashSet, + pub seen_scopes: FxHashSet, +} + +impl<'a> SavedDiffWork<'a> { + pub unsafe fn extend(self: SavedDiffWork<'a>) -> SavedDiffWork<'static> { + std::mem::transmute(self) + } + pub unsafe fn promote<'b>(self, vdom: SharedVdom<'b>) -> DiffMachine<'b> { + let extended: SavedDiffWork<'b> = std::mem::transmute(self); + DiffMachine { + vdom, + mutations: extended.mutations, + stack: extended.stack, + diffed: extended.diffed, + seen_scopes: extended.seen_scopes, + } + } +} + impl<'bump> DiffMachine<'bump> { - pub(crate) fn new(edits: Mutations<'bump>, shared: &'bump Scheduler) -> Self { + pub(crate) fn new(edits: Mutations<'bump>, shared: SharedVdom<'bump>) -> Self { Self { stack: DiffStack::new(), mutations: edits, @@ -127,6 +178,15 @@ impl<'bump> DiffMachine<'bump> { } } + pub fn save(self) -> SavedDiffWork<'bump> { + SavedDiffWork { + mutations: self.mutations, + stack: self.stack, + diffed: self.diffed, + seen_scopes: self.seen_scopes, + } + } + // pub fn new_headless(shared: &'bump SharedResources) -> Self { // let edits = Mutations::new(); // let cur_scope = ScopeId(0); @@ -136,7 +196,6 @@ impl<'bump> DiffMachine<'bump> { // pub async fn diff_scope(&mut self, id: ScopeId) { if let Some(component) = self.vdom.get_scope_mut(id) { - self.stack.scope_stack.push(id); let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); self.stack.push(DiffInstruction::DiffNode { new, old }); self.work().await; @@ -152,9 +211,6 @@ impl<'bump> DiffMachine<'bump> { // 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.stack.pop() { - // todo: call this less frequently, there is a bit of overhead involved - yield_now().await; - match instruction { DiffInstruction::PopScope => { self.stack.pop_scope(); @@ -178,12 +234,16 @@ impl<'bump> DiffMachine<'bump> { DiffInstruction::PrepareMoveNode { node } => { log::debug!("Preparing to move node: {:?}", node); - for el in RealChildIterator::new(node, self.vdom) { - self.mutations.push_root(el.direct_id()); - self.stack.add_child_count(1); - } + todo!(); + // for el in RealChildIterator::new(node, self.vdom) { + // self.mutations.push_root(el.direct_id()); + // self.stack.add_child_count(1); + // } } }; + + // todo: call this less frequently, there is a bit of overhead involved + yield_now().await; } } @@ -202,11 +262,12 @@ impl<'bump> DiffMachine<'bump> { } MountType::Replace { old } => { - let mut iter = RealChildIterator::new(old, self.vdom); - let first = iter.next().unwrap(); - self.mutations - .replace_with(first.direct_id(), nodes_created as u32); - self.remove_nodes(iter); + todo!() + // let mut iter = RealChildIterator::new(old, self.vdom); + // let first = iter.next().unwrap(); + // self.mutations + // .replace_with(first.direct_id(), nodes_created as u32); + // self.remove_nodes(iter); } MountType::ReplaceByElementId { el: old } => { @@ -309,8 +370,8 @@ impl<'bump> DiffMachine<'bump> { let shared = self.vdom.channel.clone(); // Insert a new scope into our component list + let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); let new_idx = self.vdom.insert_scope_with_key(|new_idx| { - let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); let height = parent_scope.height + 1; Scope::new( caller, diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index b3c0c148a..ac01bbfa4 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -70,6 +70,7 @@ For the rest, we defer to the rIC period and work down each queue from high to l */ use std::cell::{Cell, RefCell, RefMut}; use std::fmt::Display; +use std::intrinsics::transmute; use std::{cell::UnsafeCell, rc::Rc}; use crate::heuristics::*; @@ -174,7 +175,7 @@ pub struct Scheduler { pub garbage_scopes: HashSet, - pub fibers: [PriortySystem; 3], + pub lanes: [PriortySystem; 4], } impl Scheduler { @@ -253,7 +254,8 @@ impl Scheduler { current_priority: EventPriority::Low, // a dedicated fiber for each priority - fibers: [ + lanes: [ + PriortySystem::new(), PriortySystem::new(), PriortySystem::new(), PriortySystem::new(), @@ -456,7 +458,7 @@ impl Scheduler { /// pub async fn work_with_deadline<'a>( &'a mut self, - deadline: &mut Pin>>, + mut deadline: Pin>>, ) -> Vec> { /* Strategy: @@ -478,12 +480,8 @@ impl Scheduler { - should we batch? - react says no - they are continuous - but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff? - - - - */ - let mut committed_mutations = Vec::new(); + let mut committed_mutations = Vec::>::new(); // TODO: // the scheduler uses a bunch of different receivers to mimic a "topic" queue system. The futures-channel implementation @@ -496,7 +494,7 @@ impl Scheduler { // Wait for any new events if we have nothing to do if !self.has_any_work() { self.clean_up_garbage(); - let deadline_expired = self.wait_for_any_trigger(deadline).await; + let deadline_expired = self.wait_for_any_trigger(&mut deadline).await; if deadline_expired { return committed_mutations; @@ -508,7 +506,76 @@ impl Scheduler { // Work through the current subtree, and commit the results when it finishes // When the deadline expires, give back the work - let mut new_mutations = Mutations::new(); + self.current_priority = match ( + self.lanes[0].has_work(), + self.lanes[1].has_work(), + self.lanes[2].has_work(), + self.lanes[3].has_work(), + ) { + (true, _, _, _) => EventPriority::Immediate, + (false, true, _, _) => EventPriority::High, + (false, false, true, _) => EventPriority::Medium, + (false, false, false, _) => EventPriority::Low, + }; + + let current_lane = match self.current_priority { + EventPriority::Immediate => &mut self.lanes[0], + EventPriority::High => &mut self.lanes[1], + EventPriority::Medium => &mut self.lanes[2], + EventPriority::Low => &mut self.lanes[3], + }; + + if self.current_priority == EventPriority::Immediate { + // IDGAF - get this out the door right now. loop poll if we need to + } + + use futures_util::future::{select, Either}; + + // if let Some(state) = current_lane.saved_state.take() { + // let mut machine = unsafe { state.promote(&self) }; + // machine.work().await; + // } else { + if let Some(scope) = current_lane.dirty_scopes.pop() { + let (mut saved_work, work_complete): (SavedDiffWork<'static>, bool) = { + let shared = SharedVdom { + channel: self.channel.clone(), + components: unsafe { &mut *self.components.get() }, + elements: &mut self.raw_elements, + }; + let mut machine = DiffMachine::new(Mutations::new(), shared); + + let work_complete = { + let fut = machine.diff_scope(scope); + pin_mut!(fut); + match select(fut, &mut deadline).await { + Either::Left((work, _other)) => { + // + true + } + Either::Right((deadline, _other)) => { + // + false + } + } + }; + + let saved = machine.save(); + let extended: SavedDiffWork<'static> = unsafe { transmute(saved) }; + (extended, work_complete) + }; + + // release the stack borrow of ourself + + if work_complete { + for scope in saved_work.seen_scopes.drain() { + current_lane.dirty_scopes.remove(&scope); + } + } else { + } + // } + }; + + // let mut new_mutations = Mutations::new(); // match self.work_with_deadline(&mut deadline).await { // Some(mutations) => { // // safety: the scheduler will never let us mutate @@ -519,15 +586,6 @@ impl Scheduler { // } } // // check if we need to elevate priority - // self.current_priority = match ( - // self.high_priorty.has_work(), - // self.medium_priority.has_work(), - // self.low_priority.has_work(), - // ) { - // (true, _, _) => EventPriority::High, - // (false, true, _) => EventPriority::Medium, - // (false, false, _) => EventPriority::Low, - // }; // // let mut machine = DiffMachine::new(mutations, ScopeId(0), &self); @@ -595,7 +653,17 @@ impl Scheduler { } pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { - // + match msg { + SchedulerMsg::Immediate(_) => todo!(), + SchedulerMsg::UiEvent(_) => todo!(), + + // + SchedulerMsg::SubmitTask(_, _) => todo!(), + SchedulerMsg::ToggleTask(_) => todo!(), + SchedulerMsg::PauseTask(_) => todo!(), + SchedulerMsg::ResumeTask(_) => todo!(), + SchedulerMsg::DropTask(_) => todo!(), + } } pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { @@ -610,13 +678,13 @@ impl Scheduler { pub struct PriortySystem { pub dirty_scopes: IndexSet, - pub machine: DiffMachine<'static>, + pub saved_state: Option>, } impl PriortySystem { pub fn new() -> Self { Self { - machine: DiffMachine::new(edits, shared), + saved_state: None, dirty_scopes: Default::default(), } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 861642f03..17cc056e2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -194,7 +194,7 @@ impl VirtualDom { } pub async fn diff_async<'s>(&'s mut self) -> Mutations<'s> { - let mut diff_machine = DiffMachine::new(Mutations::new(), &self.scheduler); + let mut diff_machine = DiffMachine::new(Mutations::new(), todo!()); let cur_component = self .scheduler @@ -268,7 +268,7 @@ impl VirtualDom { deadline: impl Future, ) -> Vec> { let mut deadline = Box::pin(deadline.fuse()); - self.scheduler.work_with_deadline(&mut deadline).await + self.scheduler.work_with_deadline(deadline).await } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { From 5f63eda29473b82ce708ec66e4dd862c3717e958 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 26 Aug 2021 03:33:38 -0400 Subject: [PATCH 37/53] wip: feeling goodish about scheduler --- packages/core/src/diff_stack.rs | 4 ++ packages/core/src/scheduler.rs | 85 +++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index 98e41c88b..c09668169 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -55,6 +55,10 @@ impl<'bump> DiffStack<'bump> { } } + pub fn is_empty(&self) -> bool { + self.instructions.is_empty() + } + pub fn pop(&mut self) -> Option> { self.instructions.pop() } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index ac01bbfa4..d1172f64f 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -531,49 +531,60 @@ impl Scheduler { use futures_util::future::{select, Either}; + // We have to split away some parts of ourself - current lane is borrowed mutably + + let shared = SharedVdom { + channel: self.channel.clone(), + components: unsafe { &mut *self.components.get() }, + elements: &mut self.raw_elements, + }; + + let mut state = current_lane.saved_state.take().unwrap(); + let mut machine = unsafe { state.promote(shared) }; + + if machine.stack.is_empty() { + // if let Some(scope) = current_lane.dirty_scopes.pop() { + // let component = self.components.get_mut().get_mut(scope.0).unwrap(); + // let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + // machine.stack.push(DiffInstruction::DiffNode { new, old }); + // } else { + // } + } else { + } + // if let Some(state) = current_lane.saved_state.take() { // let mut machine = unsafe { state.promote(&self) }; // machine.work().await; // } else { - if let Some(scope) = current_lane.dirty_scopes.pop() { - let (mut saved_work, work_complete): (SavedDiffWork<'static>, bool) = { - let shared = SharedVdom { - channel: self.channel.clone(), - components: unsafe { &mut *self.components.get() }, - elements: &mut self.raw_elements, - }; - let mut machine = DiffMachine::new(Mutations::new(), shared); + // if let Some(scope) = current_lane.dirty_scopes.pop() { + // - let work_complete = { - let fut = machine.diff_scope(scope); - pin_mut!(fut); - match select(fut, &mut deadline).await { - Either::Left((work, _other)) => { - // - true - } - Either::Right((deadline, _other)) => { - // - false - } - } - }; + // let work_complete = { + // let fut = machine.diff_scope(scope); + // pin_mut!(fut); + // match select(fut, &mut deadline).await { + // Either::Left((work, _other)) => { + // // + // true + // } + // Either::Right((deadline, _other)) => { + // // + // false + // } + // } + // }; - let saved = machine.save(); - let extended: SavedDiffWork<'static> = unsafe { transmute(saved) }; - (extended, work_complete) - }; + // let mut saved = unsafe { machine.save().extend() }; - // release the stack borrow of ourself - - if work_complete { - for scope in saved_work.seen_scopes.drain() { - current_lane.dirty_scopes.remove(&scope); - } - } else { - } - // } - }; + // // release the stack borrow of ourself + // if work_complete { + // for scope in saved.seen_scopes.drain() { + // current_lane.dirty_scopes.remove(&scope); + // } + // } else { + // } + // } + // }; // let mut new_mutations = Mutations::new(); // match self.work_with_deadline(&mut deadline).await { @@ -679,6 +690,7 @@ impl Scheduler { pub struct PriortySystem { pub dirty_scopes: IndexSet, pub saved_state: Option>, + pub in_progress: bool, } impl PriortySystem { @@ -686,6 +698,7 @@ impl PriortySystem { Self { saved_state: None, dirty_scopes: Default::default(), + in_progress: false, } } From 9ad5e4965409464c6ca63b3434bbe182dde26943 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 26 Aug 2021 17:05:28 -0400 Subject: [PATCH 38/53] wip: algo for scheduler finished --- packages/core/src/childiter.rs | 6 +- packages/core/src/diff.rs | 53 +- packages/core/src/mutations.rs | 6 +- packages/core/src/scheduler.rs | 896 +++++++++++++++---------------- packages/core/src/virtual_dom.rs | 9 +- 5 files changed, 463 insertions(+), 507 deletions(-) diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs index bfac9503d..3b762c547 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/childiter.rs @@ -5,7 +5,7 @@ use crate::innerlude::*; /// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like /// "InsertBefore". pub struct RealChildIterator<'a> { - scopes: &'a Scheduler, + scopes: &'a ResourcePool, // Heuristcally we should never bleed into 4 completely nested fragments/components // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane @@ -14,14 +14,14 @@ pub struct RealChildIterator<'a> { } impl<'a> RealChildIterator<'a> { - pub fn new(starter: &'a VNode<'a>, scopes: &'a Scheduler) -> Self { + pub fn new(starter: &'a VNode<'a>, scopes: &'a ResourcePool) -> Self { Self { scopes, stack: smallvec::smallvec![(0, starter)], } } - pub fn new_from_slice(nodes: &'a [VNode<'a>], scopes: &'a Scheduler) -> Self { + pub fn new_from_slice(nodes: &'a [VNode<'a>], scopes: &'a ResourcePool) -> Self { let mut stack = smallvec::smallvec![]; for node in nodes { stack.push((0, node)); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f90fa98eb..d8e423c9f 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -106,48 +106,20 @@ use DomEdit::*; /// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's /// stack machines all the way down! pub struct DiffMachine<'bump> { - vdom: SharedVdom<'bump>, + pub channel: EventChannel, + pub vdom: &'bump ResourcePool, pub mutations: Mutations<'bump>, pub stack: DiffStack<'bump>, - pub diffed: FxHashSet, pub seen_scopes: FxHashSet, } -pub struct SharedVdom<'bump> { - pub components: &'bump mut Slab, - pub elements: &'bump mut Slab<()>, - pub channel: EventChannel, -} - -impl<'bump> SharedVdom<'bump> { - fn get_scope_mut(&mut self, scope: ScopeId) -> Option<&'bump mut Scope> { - todo!() - } - - fn get_scope(&mut self, scope: ScopeId) -> Option<&'bump Scope> { - todo!() - } - - fn reserve_node(&mut self) -> ElementId { - todo!() - } - fn collect_garbage(&mut self, element: ElementId) {} - - pub fn insert_scope_with_key(&mut self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { - let entry = self.components.vacant_entry(); - let id = ScopeId(entry.key()); - entry.insert(f(id)); - id - } -} - /// a "saved" form of a diff machine /// in regular diff machine, the &'bump reference is a stack borrow, but the /// bump lifetimes are heap borrows. pub struct SavedDiffWork<'bump> { + pub channel: EventChannel, pub mutations: Mutations<'bump>, pub stack: DiffStack<'bump>, - pub diffed: FxHashSet, pub seen_scopes: FxHashSet, } @@ -155,34 +127,38 @@ impl<'a> SavedDiffWork<'a> { pub unsafe fn extend(self: SavedDiffWork<'a>) -> SavedDiffWork<'static> { std::mem::transmute(self) } - pub unsafe fn promote<'b>(self, vdom: SharedVdom<'b>) -> DiffMachine<'b> { + pub unsafe fn promote<'b>(self, vdom: &'b mut ResourcePool) -> DiffMachine<'b> { let extended: SavedDiffWork<'b> = std::mem::transmute(self); DiffMachine { vdom, + channel: extended.channel, mutations: extended.mutations, stack: extended.stack, - diffed: extended.diffed, seen_scopes: extended.seen_scopes, } } } impl<'bump> DiffMachine<'bump> { - pub(crate) fn new(edits: Mutations<'bump>, shared: SharedVdom<'bump>) -> Self { + pub(crate) fn new( + edits: Mutations<'bump>, + shared: &'bump mut ResourcePool, + channel: EventChannel, + ) -> Self { Self { + channel, stack: DiffStack::new(), mutations: edits, vdom: shared, - diffed: FxHashSet::default(), seen_scopes: FxHashSet::default(), } } pub fn save(self) -> SavedDiffWork<'bump> { SavedDiffWork { + channel: self.channel, mutations: self.mutations, stack: self.stack, - diffed: self.diffed, seen_scopes: self.seen_scopes, } } @@ -194,11 +170,10 @@ impl<'bump> DiffMachine<'bump> { // } // - pub async fn diff_scope(&mut self, id: ScopeId) { + pub async fn diff_scope(&'bump mut self, id: ScopeId) { if let Some(component) = self.vdom.get_scope_mut(id) { let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); self.stack.push(DiffInstruction::DiffNode { new, old }); - self.work().await; } } @@ -368,7 +343,7 @@ impl<'bump> DiffMachine<'bump> { let parent_idx = self.stack.current_scope().unwrap(); - let shared = self.vdom.channel.clone(); + let shared = self.channel.clone(); // Insert a new scope into our component list let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); let new_idx = self.vdom.insert_scope_with_key(|new_idx| { diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 81909cd1d..e16126cdc 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -13,16 +13,12 @@ pub struct Mutations<'a> { use DomEdit::*; impl<'a> Mutations<'a> { - pub fn new() -> Self { + pub(crate) fn new() -> Self { let edits = Vec::new(); let noderefs = Vec::new(); Self { edits, noderefs } } - pub fn extend(&mut self, other: &mut Mutations) { - // self.edits.extend(other.edits); - } - // Navigation pub(crate) fn push_root(&mut self, root: ElementId) { let id = root.as_u64(); diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index d1172f64f..0850181ed 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -129,28 +129,7 @@ pub enum SchedulerMsg { /// /// pub struct Scheduler { - /* - This *has* to be an UnsafeCell. - - Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to - its bumpframe conents immutably. - - However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly - simple to audit. - - Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!) - */ - pub components: Rc>>, - - /* - Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy - that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client. - - This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse. - - In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes... - */ - pub raw_elements: Slab<()>, + pub pool: ResourcePool, pub heuristics: HeuristicsEngine, @@ -175,7 +154,7 @@ pub struct Scheduler { pub garbage_scopes: HashSet, - pub lanes: [PriortySystem; 4], + pub lanes: [PriorityLane; 4], } impl Scheduler { @@ -185,7 +164,12 @@ impl Scheduler { Perhaps this should be configurable? */ let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100))); - let raw_elements = Slab::with_capacity(2000); + let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(2000))); + + let pool = ResourcePool { + components: components.clone(), + raw_elements, + }; let heuristics = HeuristicsEngine::new(); @@ -231,16 +215,15 @@ impl Scheduler { }; Self { + pool, channel, receiver, - components, async_tasks: FuturesUnordered::new(), pending_garbage: FxHashSet::default(), heuristics, - raw_elements, // a storage for our receiver to dump into ui_events: VecDeque::new(), @@ -255,14 +238,441 @@ impl Scheduler { // a dedicated fiber for each priority lanes: [ - PriortySystem::new(), - PriortySystem::new(), - PriortySystem::new(), - PriortySystem::new(), + PriorityLane::new(), + PriorityLane::new(), + PriorityLane::new(), + PriorityLane::new(), ], } } + pub fn manually_poll_events(&mut self) { + while let Ok(Some(msg)) = self.receiver.try_next() { + self.handle_channel_msg(msg); + } + } + + // Converts UI events into dirty scopes with various priorities + pub fn consume_pending_events(&mut self) -> Result<()> { + // while let Some(trigger) = self.ui_events.pop_back() { + // match &trigger.event { + // SyntheticEvent::ClipboardEvent(_) + // | SyntheticEvent::CompositionEvent(_) + // | SyntheticEvent::KeyboardEvent(_) + // | SyntheticEvent::FocusEvent(_) + // | SyntheticEvent::FormEvent(_) + // | SyntheticEvent::SelectionEvent(_) + // | SyntheticEvent::TouchEvent(_) + // | SyntheticEvent::UIEvent(_) + // | SyntheticEvent::WheelEvent(_) + // | SyntheticEvent::MediaEvent(_) + // | SyntheticEvent::AnimationEvent(_) + // | SyntheticEvent::TransitionEvent(_) + // | SyntheticEvent::ToggleEvent(_) + // | SyntheticEvent::MouseEvent(_) + // | SyntheticEvent::PointerEvent(_) => { + // if let Some(scope) = self.get_scope_mut(trigger.scope) { + // if let Some(element) = trigger.mounted_dom_id { + // scope.call_listener(trigger.event, element)?; + + // // let receiver = self.immediate_receiver.clone(); + // // let mut receiver = receiver.borrow_mut(); + + // // // Drain the immediates into the dirty scopes, setting the appropiate priorities + // // while let Ok(Some(dirty_scope)) = receiver.try_next() { + // // self.add_dirty_scope(dirty_scope, trigger.priority) + // // } + // } + // } + // } + // } + // } + + Ok(()) + } + + // nothing to do, no events on channels, no work + pub fn has_any_work(&self) -> bool { + self.has_work() || self.has_pending_events() || self.has_pending_garbage() + } + + pub fn has_pending_events(&self) -> bool { + self.ui_events.len() > 0 + } + + pub fn has_work(&self) -> bool { + todo!() + // self.high_priorty.has_work() + // || self.medium_priority.has_work() + // || self.low_priority.has_work() + } + + pub fn has_pending_garbage(&self) -> bool { + !self.garbage_scopes.is_empty() + } + + fn get_current_fiber<'a>(&'a mut self) -> &mut DiffMachine<'a> { + todo!() + // 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) } + } + + fn shift_priorities(&mut self) { + self.current_priority = match ( + self.lanes[0].has_work(), + self.lanes[1].has_work(), + self.lanes[2].has_work(), + self.lanes[3].has_work(), + ) { + (true, _, _, _) => EventPriority::Immediate, + (false, true, _, _) => EventPriority::High, + (false, false, true, _) => EventPriority::Medium, + (false, false, false, _) => EventPriority::Low, + }; + } + + fn load_current_lane(&mut self) -> &mut PriorityLane { + match self.current_priority { + EventPriority::Immediate => todo!(), + EventPriority::High => todo!(), + EventPriority::Medium => todo!(), + EventPriority::Low => todo!(), + } + } + + fn save_work(&mut self, lane: SavedDiffWork) { + let saved: SavedDiffWork<'static> = unsafe { std::mem::transmute(lane) }; + self.load_current_lane().saved_state = Some(saved); + } + + fn load_work(&mut self) -> SavedDiffWork<'static> { + match self.current_priority { + EventPriority::Immediate => todo!(), + EventPriority::High => todo!(), + EventPriority::Medium => todo!(), + EventPriority::Low => todo!(), + } + } + + /// The primary workhorse of the VirtualDOM. + /// + /// Uses some fairly complex logic to schedule what work should be produced. + /// + /// Returns a list of successful mutations. + /// + /// + pub async fn work_with_deadline<'a>( + &'a mut self, + mut deadline: Pin>>, + ) -> Vec> { + /* + Strategy: + - When called, check for any UI events that might've been received since the last frame. + - Dump all UI events into a "pending discrete" queue and a "pending continuous" queue. + + - If there are any pending discrete events, then elevate our priorty level. If our priority level is already "high," + then we need to finish the high priority work first. If the current work is "low" then analyze what scopes + will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need + to bump the other work out of the way, or choose to process it so we don't have any conflicts. + 'static components have a leg up here since their work can be re-used among multiple scopes. + "High priority" is only for blocking! Should only be used on "clicks" + + - If there are no pending discrete events, then check for continuous events. These can be completely batched + + + Open questions: + - what if we get two clicks from the component during the same slice? + - should we batch? + - react says no - they are continuous + - but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff? + */ + let mut committed_mutations = Vec::>::new(); + + // TODO: + // the scheduler uses a bunch of different receivers to mimic a "topic" queue system. The futures-channel implementation + // doesn't really have a concept of a "topic" queue, so there's a lot of noise in the hand-rolled scheduler. We should + // explore abstracting the scheduler into a topic-queue channel system - similar to Kafka or something similar. + loop { + // Internalize any pending work since the last time we ran + self.manually_poll_events(); + + // Wait for any new events if we have nothing to do + if !self.has_any_work() { + self.pool.clean_up_garbage(); + let deadline_expired = self.wait_for_any_trigger(&mut deadline).await; + + if deadline_expired { + return committed_mutations; + } + } + + // Create work from the pending event queue + self.consume_pending_events().unwrap(); + + // Work through the current subtree, and commit the results when it finishes + // When the deadline expires, give back the work + self.shift_priorities(); + + let saved_state = self.load_work(); + + // We have to split away some parts of ourself - current lane is borrowed mutably + let mut shared = self.pool.clone(); + let mut machine = unsafe { saved_state.promote(&mut shared) }; + + if machine.stack.is_empty() { + let shared = self.pool.clone(); + self.current_lane().dirty_scopes.sort_by(|a, b| { + let h1 = shared.get_scope(*a).unwrap().height; + let h2 = shared.get_scope(*b).unwrap().height; + h1.cmp(&h2) + }); + + if let Some(scope) = self.current_lane().dirty_scopes.pop() { + let component = self.pool.get_scope(scope).unwrap(); + let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + machine.stack.push(DiffInstruction::DiffNode { new, old }); + } + } + + let completed = { + let fut = machine.work(); + pin_mut!(fut); + use futures_util::future::{select, Either}; + match select(fut, &mut deadline).await { + Either::Left((work, _other)) => true, + Either::Right((deadline, _other)) => false, + } + }; + + let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) }; + let mut saved = machine.save(); + + if completed { + for node in saved.seen_scopes.drain() { + self.current_lane().dirty_scopes.remove(&node); + } + + let mut new_mutations = Mutations::new(); + std::mem::swap(&mut new_mutations, &mut saved.mutations); + + committed_mutations.push(new_mutations); + } + self.save_work(saved); + + if !completed { + break; + } + } + + committed_mutations + } + + // waits for a trigger, canceling early if the deadline is reached + // returns true if the deadline was reached + // does not return the trigger, but caches it in the scheduler + pub async fn wait_for_any_trigger( + &mut self, + deadline: &mut Pin>>, + ) -> bool { + use futures_util::future::{select, Either}; + + let event_fut = async { + match select(self.receiver.next(), self.async_tasks.next()).await { + Either::Left((msg, _other)) => { + self.handle_channel_msg(msg.unwrap()); + } + Either::Right((task, _other)) => { + // do nothing, async task will likely generate a set of scheduler messages + } + } + }; + + pin_mut!(event_fut); + + match select(event_fut, deadline).await { + Either::Left((msg, _other)) => false, + Either::Right((deadline, _)) => true, + } + } + + pub fn current_lane(&mut self) -> &mut PriorityLane { + match self.current_priority { + EventPriority::Immediate => &mut self.lanes[0], + EventPriority::High => &mut self.lanes[1], + EventPriority::Medium => &mut self.lanes[2], + EventPriority::Low => &mut self.lanes[3], + } + } + + pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { + match msg { + SchedulerMsg::Immediate(_) => todo!(), + SchedulerMsg::UiEvent(_) => todo!(), + + // + SchedulerMsg::SubmitTask(_, _) => todo!(), + SchedulerMsg::ToggleTask(_) => todo!(), + SchedulerMsg::PauseTask(_) => todo!(), + SchedulerMsg::ResumeTask(_) => todo!(), + SchedulerMsg::DropTask(_) => todo!(), + } + } + + fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { + todo!() + // match priority { + // EventPriority::High => self.high_priorty.dirty_scopes.insert(scope), + // EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope), + // EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), + // }; + } +} + +pub struct PriorityLane { + pub dirty_scopes: IndexSet, + pub saved_state: Option>, + pub in_progress: bool, +} + +impl PriorityLane { + pub fn new() -> Self { + Self { + saved_state: None, + dirty_scopes: Default::default(), + in_progress: false, + } + } + + fn has_work(&self) -> bool { + todo!() + } + + fn work(&mut self) { + let scope = self.dirty_scopes.pop(); + } +} + +pub struct TaskHandle { + pub sender: UnboundedSender, + pub our_id: u64, +} + +impl TaskHandle { + /// Toggles this coroutine off/on. + /// + /// This method is not synchronous - your task will not stop immediately. + pub fn toggle(&self) {} + + /// This method is not synchronous - your task will not stop immediately. + pub fn start(&self) {} + + /// This method is not synchronous - your task will not stop immediately. + pub fn stop(&self) {} + + /// This method is not synchronous - your task will not stop immediately. + pub fn restart(&self) {} +} + +#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ScopeId(pub usize); + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ElementId(pub usize); +impl Display for ElementId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl ElementId { + pub fn as_u64(self) -> u64 { + self.0 as u64 + } +} + +/// Priority of Event Triggers. +/// +/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus +/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers +/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. +/// +/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. +/// +/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, +/// we keep it simple, and just use a 3-tier priority system. +/// +/// - NoPriority = 0 +/// - LowPriority = 1 +/// - NormalPriority = 2 +/// - UserBlocking = 3 +/// - HighPriority = 4 +/// - ImmediatePriority = 5 +/// +/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. +/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be +/// flushed before proceeding. Multiple discrete events is highly unlikely, though. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum EventPriority { + /// Work that must be completed during the EventHandler phase + /// + /// + Immediate = 3, + + /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. + /// + /// This is typically reserved for things like user interaction. + /// + /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). + High = 2, + + /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important + /// than "High Priority" events and will take presedence over low priority events. + /// + /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. + /// + /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). + Medium = 1, + + /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be + /// advanced to the front of the work queue until completed. + /// + /// The primary user of Low Priority work is the asynchronous work system (suspense). + /// + /// This is considered "idle" work or "background" work. + Low = 0, +} + +#[derive(Clone)] +pub struct ResourcePool { + /* + This *has* to be an UnsafeCell. + + Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to + its bumpframe conents immutably. + + However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly + simple to audit. + + Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!) + */ + pub components: Rc>>, + + /* + Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy + that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client. + + This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse. + + In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes... + */ + pub raw_elements: Rc>>, +} + +impl ResourcePool { /// this is unsafe because the caller needs to track which other scopes it's already using pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> { let inner = unsafe { &*self.components.get() }; @@ -373,430 +783,4 @@ impl Scheduler { // // kill em // } } - - pub fn manually_poll_events(&mut self) { - while let Ok(Some(msg)) = self.receiver.try_next() { - self.handle_channel_msg(msg); - } - } - - // Converts UI events into dirty scopes with various priorities - pub fn consume_pending_events(&mut self) -> Result<()> { - // while let Some(trigger) = self.ui_events.pop_back() { - // match &trigger.event { - // SyntheticEvent::ClipboardEvent(_) - // | SyntheticEvent::CompositionEvent(_) - // | SyntheticEvent::KeyboardEvent(_) - // | SyntheticEvent::FocusEvent(_) - // | SyntheticEvent::FormEvent(_) - // | SyntheticEvent::SelectionEvent(_) - // | SyntheticEvent::TouchEvent(_) - // | SyntheticEvent::UIEvent(_) - // | SyntheticEvent::WheelEvent(_) - // | SyntheticEvent::MediaEvent(_) - // | SyntheticEvent::AnimationEvent(_) - // | SyntheticEvent::TransitionEvent(_) - // | SyntheticEvent::ToggleEvent(_) - // | SyntheticEvent::MouseEvent(_) - // | SyntheticEvent::PointerEvent(_) => { - // if let Some(scope) = self.get_scope_mut(trigger.scope) { - // if let Some(element) = trigger.mounted_dom_id { - // scope.call_listener(trigger.event, element)?; - - // // let receiver = self.immediate_receiver.clone(); - // // let mut receiver = receiver.borrow_mut(); - - // // // Drain the immediates into the dirty scopes, setting the appropiate priorities - // // while let Ok(Some(dirty_scope)) = receiver.try_next() { - // // self.add_dirty_scope(dirty_scope, trigger.priority) - // // } - // } - // } - // } - // } - // } - - Ok(()) - } - - // nothing to do, no events on channels, no work - pub fn has_any_work(&self) -> bool { - self.has_work() || self.has_pending_events() || self.has_pending_garbage() - } - - pub fn has_pending_events(&self) -> bool { - self.ui_events.len() > 0 - } - - pub fn has_work(&self) -> bool { - todo!() - // self.high_priorty.has_work() - // || self.medium_priority.has_work() - // || self.low_priority.has_work() - } - - pub fn has_pending_garbage(&self) -> bool { - !self.garbage_scopes.is_empty() - } - - fn get_current_fiber<'a>(&'a mut self) -> &mut DiffMachine<'a> { - todo!() - // 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) } - } - - /// The primary workhorse of the VirtualDOM. - /// - /// Uses some fairly complex logic to schedule what work should be produced. - /// - /// Returns a list of successful mutations. - /// - /// - pub async fn work_with_deadline<'a>( - &'a mut self, - mut deadline: Pin>>, - ) -> Vec> { - /* - Strategy: - - When called, check for any UI events that might've been received since the last frame. - - Dump all UI events into a "pending discrete" queue and a "pending continuous" queue. - - - If there are any pending discrete events, then elevate our priorty level. If our priority level is already "high," - then we need to finish the high priority work first. If the current work is "low" then analyze what scopes - will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need - to bump the other work out of the way, or choose to process it so we don't have any conflicts. - 'static components have a leg up here since their work can be re-used among multiple scopes. - "High priority" is only for blocking! Should only be used on "clicks" - - - If there are no pending discrete events, then check for continuous events. These can be completely batched - - - Open questions: - - what if we get two clicks from the component during the same slice? - - should we batch? - - react says no - they are continuous - - but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff? - */ - let mut committed_mutations = Vec::>::new(); - - // TODO: - // the scheduler uses a bunch of different receivers to mimic a "topic" queue system. The futures-channel implementation - // doesn't really have a concept of a "topic" queue, so there's a lot of noise in the hand-rolled scheduler. We should - // explore abstracting the scheduler into a topic-queue channel system - similar to Kafka or something similar. - loop { - // Internalize any pending work since the last time we ran - self.manually_poll_events(); - - // Wait for any new events if we have nothing to do - if !self.has_any_work() { - self.clean_up_garbage(); - let deadline_expired = self.wait_for_any_trigger(&mut deadline).await; - - if deadline_expired { - return committed_mutations; - } - } - - // Create work from the pending event queue - self.consume_pending_events().unwrap(); - - // Work through the current subtree, and commit the results when it finishes - // When the deadline expires, give back the work - self.current_priority = match ( - self.lanes[0].has_work(), - self.lanes[1].has_work(), - self.lanes[2].has_work(), - self.lanes[3].has_work(), - ) { - (true, _, _, _) => EventPriority::Immediate, - (false, true, _, _) => EventPriority::High, - (false, false, true, _) => EventPriority::Medium, - (false, false, false, _) => EventPriority::Low, - }; - - let current_lane = match self.current_priority { - EventPriority::Immediate => &mut self.lanes[0], - EventPriority::High => &mut self.lanes[1], - EventPriority::Medium => &mut self.lanes[2], - EventPriority::Low => &mut self.lanes[3], - }; - - if self.current_priority == EventPriority::Immediate { - // IDGAF - get this out the door right now. loop poll if we need to - } - - use futures_util::future::{select, Either}; - - // We have to split away some parts of ourself - current lane is borrowed mutably - - let shared = SharedVdom { - channel: self.channel.clone(), - components: unsafe { &mut *self.components.get() }, - elements: &mut self.raw_elements, - }; - - let mut state = current_lane.saved_state.take().unwrap(); - let mut machine = unsafe { state.promote(shared) }; - - if machine.stack.is_empty() { - // if let Some(scope) = current_lane.dirty_scopes.pop() { - // let component = self.components.get_mut().get_mut(scope.0).unwrap(); - // let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); - // machine.stack.push(DiffInstruction::DiffNode { new, old }); - // } else { - // } - } else { - } - - // if let Some(state) = current_lane.saved_state.take() { - // let mut machine = unsafe { state.promote(&self) }; - // machine.work().await; - // } else { - // if let Some(scope) = current_lane.dirty_scopes.pop() { - // - - // let work_complete = { - // let fut = machine.diff_scope(scope); - // pin_mut!(fut); - // match select(fut, &mut deadline).await { - // Either::Left((work, _other)) => { - // // - // true - // } - // Either::Right((deadline, _other)) => { - // // - // false - // } - // } - // }; - - // let mut saved = unsafe { machine.save().extend() }; - - // // release the stack borrow of ourself - // if work_complete { - // for scope in saved.seen_scopes.drain() { - // current_lane.dirty_scopes.remove(&scope); - // } - // } else { - // } - // } - // }; - - // let mut new_mutations = Mutations::new(); - // match self.work_with_deadline(&mut deadline).await { - // Some(mutations) => { - // // safety: the scheduler will never let us mutate - // let extended: Mutations<'static> = unsafe { std::mem::transmute(mutations) }; - // committed_mutations.push(extended) - // } - // None => return committed_mutations, - // } - } - // // check if we need to elevate priority - - // // let mut machine = DiffMachine::new(mutations, ScopeId(0), &self); - - // let dirty_root = { - // let dirty_roots = match self.current_priority { - // EventPriority::High => &self.high_priorty.dirty_scopes, - // EventPriority::Medium => &self.medium_priority.dirty_scopes, - // EventPriority::Low => &self.low_priority.dirty_scopes, - // }; - // let mut height = 0; - // let mut dirty_root = { - // let root = dirty_roots.iter().next(); - // if root.is_none() { - // return true; - // } - // root.unwrap() - // }; - - // for root in dirty_roots { - // if let Some(scope) = self.get_scope(*root) { - // if scope.height < height { - // height = scope.height; - // dirty_root = root; - // } - // } - // } - // dirty_root - // }; - - // let fut = machine.diff_scope(*dirty_root).fuse(); - // pin_mut!(fut); - - // match futures_util::future::select(deadline, fut).await { - // futures_util::future::Either::Left((deadline, work_fut)) => true, - // futures_util::future::Either::Right((_, deadline_fut)) => false, - // } - } - - // waits for a trigger, canceling early if the deadline is reached - // returns true if the deadline was reached - // does not return the trigger, but caches it in the scheduler - pub async fn wait_for_any_trigger( - &mut self, - deadline: &mut Pin>>, - ) -> bool { - use futures_util::future::{select, Either}; - - let event_fut = async { - match select(self.receiver.next(), self.async_tasks.next()).await { - Either::Left((msg, _other)) => { - self.handle_channel_msg(msg.unwrap()); - } - Either::Right((task, _other)) => { - // do nothing, async task will likely generate a set of scheduler messages - } - } - }; - - pin_mut!(event_fut); - - match select(event_fut, deadline).await { - Either::Left((msg, _other)) => false, - Either::Right((deadline, _)) => true, - } - } - - pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { - match msg { - SchedulerMsg::Immediate(_) => todo!(), - SchedulerMsg::UiEvent(_) => todo!(), - - // - SchedulerMsg::SubmitTask(_, _) => todo!(), - SchedulerMsg::ToggleTask(_) => todo!(), - SchedulerMsg::PauseTask(_) => todo!(), - SchedulerMsg::ResumeTask(_) => todo!(), - SchedulerMsg::DropTask(_) => todo!(), - } - } - - pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { - todo!() - // match priority { - // EventPriority::High => self.high_priorty.dirty_scopes.insert(scope), - // EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope), - // EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), - // }; - } -} - -pub struct PriortySystem { - pub dirty_scopes: IndexSet, - pub saved_state: Option>, - pub in_progress: bool, -} - -impl PriortySystem { - pub fn new() -> Self { - Self { - saved_state: None, - dirty_scopes: Default::default(), - in_progress: false, - } - } - - fn has_work(&self) -> bool { - todo!() - } - - fn work(&mut self) { - let scope = self.dirty_scopes.pop(); - } -} - -pub struct TaskHandle { - pub sender: UnboundedSender, - pub our_id: u64, -} - -impl TaskHandle { - /// Toggles this coroutine off/on. - /// - /// This method is not synchronous - your task will not stop immediately. - pub fn toggle(&self) {} - - /// This method is not synchronous - your task will not stop immediately. - pub fn start(&self) {} - - /// This method is not synchronous - your task will not stop immediately. - pub fn stop(&self) {} - - /// This method is not synchronous - your task will not stop immediately. - pub fn restart(&self) {} -} - -#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ScopeId(pub usize); - -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ElementId(pub usize); -impl Display for ElementId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl ElementId { - pub fn as_u64(self) -> u64 { - self.0 as u64 - } -} - -/// Priority of Event Triggers. -/// -/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus -/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers -/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. -/// -/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. -/// -/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, -/// we keep it simple, and just use a 3-tier priority system. -/// -/// - NoPriority = 0 -/// - LowPriority = 1 -/// - NormalPriority = 2 -/// - UserBlocking = 3 -/// - HighPriority = 4 -/// - ImmediatePriority = 5 -/// -/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. -/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be -/// flushed before proceeding. Multiple discrete events is highly unlikely, though. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -pub enum EventPriority { - /// Work that must be completed during the EventHandler phase - /// - /// - Immediate = 3, - - /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. - /// - /// This is typically reserved for things like user interaction. - /// - /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). - High = 2, - - /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important - /// than "High Priority" events and will take presedence over low priority events. - /// - /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - /// - /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). - Medium = 1, - - /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be - /// advanced to the front of the work queue until completed. - /// - /// The primary user of Low Priority work is the asynchronous work system (suspense). - /// - /// This is considered "idle" work or "background" work. - Low = 0, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 17cc056e2..3d7d6d58c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -107,7 +107,7 @@ impl VirtualDom { let props_ptr = _root_props.as_ref().downcast_ref::

().unwrap() as *const P; - let base_scope = scheduler.insert_scope_with_key(|myidx| { + let base_scope = scheduler.pool.insert_scope_with_key(|myidx| { let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); let name = type_name_of(root); Scope::new( @@ -129,11 +129,11 @@ impl VirtualDom { } pub fn base_scope(&self) -> &Scope { - self.scheduler.get_scope(self.base_scope).unwrap() + self.scheduler.pool.get_scope(self.base_scope).unwrap() } pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> { - self.scheduler.get_scope(id) + self.scheduler.pool.get_scope(id) } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch @@ -194,10 +194,11 @@ impl VirtualDom { } pub async fn diff_async<'s>(&'s mut self) -> Mutations<'s> { - let mut diff_machine = DiffMachine::new(Mutations::new(), todo!()); + let mut diff_machine = DiffMachine::new(Mutations::new(), todo!(), todo!()); let cur_component = self .scheduler + .pool .get_scope_mut(self.base_scope) .expect("The base scope should never be moved"); From 8b0eb87c72ea9d444dee99a8b05643f19fea2634 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 26 Aug 2021 22:05:09 -0400 Subject: [PATCH 39/53] wip: refactor --- examples/hydration.rs | 2 +- examples/reference/tostring.rs | 2 +- examples/web_tick.rs | 53 ++++++++++++---------------- packages/core/examples/vdom_usage.rs | 4 --- packages/core/src/diff.rs | 34 ++++++------------ packages/core/src/events.rs | 5 --- packages/core/src/scheduler.rs | 37 ++++++++----------- packages/core/src/scope.rs | 2 +- packages/core/src/virtual_dom.rs | 48 +++++++++++++------------ packages/core/tests/eventsystem.rs | 2 +- packages/desktop/src/events.rs | 7 ++-- packages/web/src/dom.rs | 18 +++++----- packages/web/src/lib.rs | 10 +++--- 13 files changed, 96 insertions(+), 128 deletions(-) diff --git a/examples/hydration.rs b/examples/hydration.rs index dc9188f5a..0f083d933 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -13,7 +13,7 @@ use dioxus::prelude::*; use dioxus::ssr; fn main() { - let mut vdom = VirtualDom::launch_in_place(App); + let vdom = VirtualDom::new(App); let content = ssr::render_vdom(&vdom, |f| f.pre_render(true)); dioxus::desktop::launch(App, |c| c.with_prerendered(content)).unwrap(); diff --git a/examples/reference/tostring.rs b/examples/reference/tostring.rs index 8827932f2..02b1daf81 100644 --- a/examples/reference/tostring.rs +++ b/examples/reference/tostring.rs @@ -6,7 +6,7 @@ pub static Example: FC<()> = |cx| { // Currently, SSR is only supported for whole VirtualDOMs // This is an easy/low hanging fruit to improve upon let mut dom = VirtualDom::new(SomeApp); - dom.rebuild_in_place().unwrap(); + dom.rebuild(); ssr::render_vdom(&dom, |c| c) }); diff --git a/examples/web_tick.rs b/examples/web_tick.rs index 8d58acfec..e82f131ec 100644 --- a/examples/web_tick.rs +++ b/examples/web_tick.rs @@ -13,32 +13,13 @@ use dioxus::prelude::*; fn main() { - // Setup logging - // wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); - // console_error_panic_hook::set_once(); - for adj in ADJECTIVES { - wasm_bindgen::intern(adj); - } - for col in COLOURS { - wasm_bindgen::intern(col); - } - for no in NOUNS { - wasm_bindgen::intern(no); - } - wasm_bindgen::intern("col-md-1"); - wasm_bindgen::intern("col-md-6"); - wasm_bindgen::intern("glyphicon glyphicon-remove remove"); - wasm_bindgen::intern("remove"); - wasm_bindgen::intern("dioxus"); - wasm_bindgen::intern("lbl"); - wasm_bindgen::intern("true"); + #[cfg(target_arch = "wasm32")] + intern_strings(); dioxus::web::launch(App, |c| c); } static App: FC<()> = |cx| { - // let mut count = use_state(cx, || 0); - let mut rng = SmallRng::from_entropy(); let rows = (0..1_000).map(|f| { let label = Label::new(&mut rng); @@ -49,6 +30,7 @@ static App: FC<()> = |cx| { } } }); + cx.render(rsx! { table { tbody { @@ -56,15 +38,6 @@ static App: FC<()> = |cx| { } } }) - // cx.render(rsx! { - // div { - // // h1 { "Hifive counter: {count}" } - // // {cx.children()} - // // button { onclick: move |_| count += 1, "Up high!" } - // // button { onclick: move |_| count -= 1, "Down low!" } - // {(0..1000).map(|i| rsx!{ div { "Count: {count}" } })} - // } - // }) }; #[derive(PartialEq, Props)] @@ -141,3 +114,23 @@ static NOUNS: &[&str] = &[ "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard", ]; + +#[cfg(target_arch = "wasm32")] +fn intern_strings() { + for adj in ADJECTIVES { + wasm_bindgen::intern(adj); + } + for col in COLOURS { + wasm_bindgen::intern(col); + } + for no in NOUNS { + wasm_bindgen::intern(no); + } + wasm_bindgen::intern("col-md-1"); + wasm_bindgen::intern("col-md-6"); + wasm_bindgen::intern("glyphicon glyphicon-remove remove"); + wasm_bindgen::intern("remove"); + wasm_bindgen::intern("dioxus"); + wasm_bindgen::intern("lbl"); + wasm_bindgen::intern("true"); +} diff --git a/packages/core/examples/vdom_usage.rs b/packages/core/examples/vdom_usage.rs index 027452104..4dde570d8 100644 --- a/packages/core/examples/vdom_usage.rs +++ b/packages/core/examples/vdom_usage.rs @@ -12,8 +12,4 @@ async fn main() { let mut deadline = async_std::task::sleep(Duration::from_millis(50)); let fut = dom.run_with_deadline(deadline); - - if let Some(mutations) = fut.await { - // - } } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index d8e423c9f..7c6ef2751 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -106,7 +106,6 @@ use DomEdit::*; /// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's /// stack machines all the way down! pub struct DiffMachine<'bump> { - pub channel: EventChannel, pub vdom: &'bump ResourcePool, pub mutations: Mutations<'bump>, pub stack: DiffStack<'bump>, @@ -117,7 +116,6 @@ pub struct DiffMachine<'bump> { /// in regular diff machine, the &'bump reference is a stack borrow, but the /// bump lifetimes are heap borrows. pub struct SavedDiffWork<'bump> { - pub channel: EventChannel, pub mutations: Mutations<'bump>, pub stack: DiffStack<'bump>, pub seen_scopes: FxHashSet, @@ -131,7 +129,6 @@ impl<'a> SavedDiffWork<'a> { let extended: SavedDiffWork<'b> = std::mem::transmute(self); DiffMachine { vdom, - channel: extended.channel, mutations: extended.mutations, stack: extended.stack, seen_scopes: extended.seen_scopes, @@ -140,13 +137,8 @@ impl<'a> SavedDiffWork<'a> { } impl<'bump> DiffMachine<'bump> { - pub(crate) fn new( - edits: Mutations<'bump>, - shared: &'bump mut ResourcePool, - channel: EventChannel, - ) -> Self { + pub(crate) fn new(edits: Mutations<'bump>, shared: &'bump mut ResourcePool) -> Self { Self { - channel, stack: DiffStack::new(), mutations: edits, vdom: shared, @@ -156,7 +148,6 @@ impl<'bump> DiffMachine<'bump> { pub fn save(self) -> SavedDiffWork<'bump> { SavedDiffWork { - channel: self.channel, mutations: self.mutations, stack: self.stack, seen_scopes: self.seen_scopes, @@ -208,12 +199,10 @@ impl<'bump> DiffMachine<'bump> { } DiffInstruction::PrepareMoveNode { node } => { - log::debug!("Preparing to move node: {:?}", node); - todo!(); - // for el in RealChildIterator::new(node, self.vdom) { - // self.mutations.push_root(el.direct_id()); - // self.stack.add_child_count(1); - // } + for el in RealChildIterator::new(node, self.vdom) { + self.mutations.push_root(el.direct_id()); + self.stack.add_child_count(1); + } } }; @@ -237,12 +226,11 @@ impl<'bump> DiffMachine<'bump> { } MountType::Replace { old } => { - todo!() - // let mut iter = RealChildIterator::new(old, self.vdom); - // let first = iter.next().unwrap(); - // self.mutations - // .replace_with(first.direct_id(), nodes_created as u32); - // self.remove_nodes(iter); + let mut iter = RealChildIterator::new(old, self.vdom); + let first = iter.next().unwrap(); + self.mutations + .replace_with(first.direct_id(), nodes_created as u32); + self.remove_nodes(iter); } MountType::ReplaceByElementId { el: old } => { @@ -343,7 +331,7 @@ impl<'bump> DiffMachine<'bump> { let parent_idx = self.stack.current_scope().unwrap(); - let shared = self.channel.clone(); + let shared = self.vdom.channel.clone(); // Insert a new scope into our component list let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); let new_idx = self.vdom.insert_scope_with_key(|new_idx| { diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 454975a7d..a52c24f53 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -15,11 +15,6 @@ pub struct EventTrigger { /// The type of event pub event: SyntheticEvent, - - /// Is consistency important for this event? - /// UI events are the only events where consistency is important. - /// All else may be batched. - pub discrete: bool, } pub enum SyntheticEvent { diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 0850181ed..910ba3ea6 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -133,8 +133,6 @@ pub struct Scheduler { pub heuristics: HeuristicsEngine, - pub channel: EventChannel, - pub receiver: UnboundedReceiver, // Garbage stored @@ -166,11 +164,6 @@ impl Scheduler { let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100))); let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(2000))); - let pool = ResourcePool { - components: components.clone(), - raw_elements, - }; - let heuristics = HeuristicsEngine::new(); let (sender, receiver) = futures_channel::mpsc::unbounded::(); @@ -214,9 +207,14 @@ impl Scheduler { }, }; + let pool = ResourcePool { + components: components.clone(), + raw_elements, + channel, + }; + Self { pool, - channel, receiver, async_tasks: FuturesUnordered::new(), @@ -253,7 +251,7 @@ impl Scheduler { } // Converts UI events into dirty scopes with various priorities - pub fn consume_pending_events(&mut self) -> Result<()> { + pub fn consume_pending_events(&mut self) { // while let Some(trigger) = self.ui_events.pop_back() { // match &trigger.event { // SyntheticEvent::ClipboardEvent(_) @@ -287,8 +285,6 @@ impl Scheduler { // } // } // } - - Ok(()) } // nothing to do, no events on channels, no work @@ -363,8 +359,6 @@ impl Scheduler { /// Uses some fairly complex logic to schedule what work should be produced. /// /// Returns a list of successful mutations. - /// - /// pub async fn work_with_deadline<'a>( &'a mut self, mut deadline: Pin>>, @@ -392,10 +386,6 @@ impl Scheduler { */ let mut committed_mutations = Vec::>::new(); - // TODO: - // the scheduler uses a bunch of different receivers to mimic a "topic" queue system. The futures-channel implementation - // doesn't really have a concept of a "topic" queue, so there's a lot of noise in the hand-rolled scheduler. We should - // explore abstracting the scheduler into a topic-queue channel system - similar to Kafka or something similar. loop { // Internalize any pending work since the last time we ran self.manually_poll_events(); @@ -411,7 +401,7 @@ impl Scheduler { } // Create work from the pending event queue - self.consume_pending_events().unwrap(); + self.consume_pending_events(); // Work through the current subtree, and commit the results when it finishes // When the deadline expires, give back the work @@ -443,8 +433,8 @@ impl Scheduler { pin_mut!(fut); use futures_util::future::{select, Either}; match select(fut, &mut deadline).await { - Either::Left((work, _other)) => true, - Either::Right((deadline, _other)) => false, + Either::Left((_, _other)) => true, + Either::Right((_, _other)) => false, } }; @@ -461,6 +451,7 @@ impl Scheduler { committed_mutations.push(new_mutations); } + self.save_work(saved); if !completed { @@ -670,6 +661,8 @@ pub struct ResourcePool { In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes... */ pub raw_elements: Rc>>, + + pub channel: EventChannel, } impl ResourcePool { @@ -711,8 +704,8 @@ impl ResourcePool { } pub fn reserve_node(&self) -> ElementId { - todo!("reserving wip until it's fast enough again") - // ElementId(self.raw_elements.insert(())) + let els = unsafe { &mut *self.raw_elements.get() }; + ElementId(els.insert(())) } /// return the id, freeing the space of the original node diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 2222a7335..40bfd43ec 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -164,7 +164,7 @@ impl Scope { "clean up your garabge please" ); - todo!("arch changes"); + // todo!("arch changes"); // // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we // // run the hooks (which hold an &mut Referrence) diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 3d7d6d58c..6c75899ba 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -116,7 +116,7 @@ impl VirtualDom { None, 0, ScopeChildren(&[]), - scheduler.channel.clone(), + scheduler.pool.channel.clone(), ) }); @@ -158,29 +158,31 @@ impl VirtualDom { /// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This /// method can be useful when needing to perform some crucial periodic tasks. pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> { - todo!() - // let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.scheduler); + let mut shared = self.scheduler.pool.clone(); + let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared); - // let cur_component = self - // .scheduler - // .get_scope_mut(self.base_scope) - // .expect("The base scope should never be moved"); + let cur_component = self + .scheduler + .pool + .get_scope_mut(self.base_scope) + .expect("The base scope should never be moved"); - // // // 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 - // .stack - // .create_node(cur_component.frames.fin_head(), MountType::Append); - // diff_machine.work().await; - // } else { - // // todo: should this be a hard error? - // log::warn!( - // "Component failed to run succesfully during rebuild. - // This does not result in a failed rebuild, but indicates a logic failure within your app." - // ); - // } + // // 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 + .stack + .create_node(cur_component.frames.fin_head(), MountType::Append); + diff_machine.stack.scope_stack.push(self.base_scope); + diff_machine.work().await; + } else { + // todo: should this be a hard error? + log::warn!( + "Component failed to run succesfully during rebuild. + This does not result in a failed rebuild, but indicates a logic failure within your app." + ); + } - // diff_machine.mutations + unsafe { std::mem::transmute(diff_machine.mutations) } } pub fn diff_sync<'s>(&'s mut self) -> Mutations<'s> { @@ -194,7 +196,7 @@ impl VirtualDom { } pub async fn diff_async<'s>(&'s mut self) -> Mutations<'s> { - let mut diff_machine = DiffMachine::new(Mutations::new(), todo!(), todo!()); + let mut diff_machine = DiffMachine::new(Mutations::new(), todo!()); let cur_component = self .scheduler @@ -273,7 +275,7 @@ impl VirtualDom { } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - self.scheduler.channel.sender.clone() + self.scheduler.pool.channel.sender.clone() } pub fn has_work(&self) -> bool { diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs index 2b90ca256..db57118b4 100644 --- a/packages/core/tests/eventsystem.rs +++ b/packages/core/tests/eventsystem.rs @@ -17,6 +17,6 @@ async fn event_queue_works() { let edits = dom.rebuild(); async_std::task::spawn_local(async move { - let mutations = dom.run_unbounded().await; + // let mutations = dom.run_unbounded().await; }); } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 6d4c8ddd5..ee26d2fbc 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -24,8 +24,11 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> EventTrigger { let event = SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent))); let scope = ScopeId(data.scope as usize); let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize)); - let priority = EventPriority::High; - EventTrigger::new(event, scope, mounted_dom_id, priority) + EventTrigger { + event, + scope, + mounted_dom_id, + } } #[derive(Debug)] diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index cb907cdc6..5a62bfe76 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc}; use dioxus_core::{ events::{on::GenericEventInner, EventTrigger, SyntheticEvent}, mutations::NodeRefMutation, + scheduler::SchedulerMsg, DomEdit, ElementId, ScopeId, }; use fxhash::FxHashMap; @@ -24,7 +25,7 @@ pub struct WebsysDom { root: Element, - sender_callback: Rc, + sender_callback: Rc, // map of listener types to number of those listeners // This is roughly a delegater @@ -39,7 +40,7 @@ pub struct WebsysDom { last_node_was_text: bool, } impl WebsysDom { - pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc) -> Self { + pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc) -> Self { let document = load_document(); let mut nodes = NodeSlab::new(2000); @@ -277,7 +278,7 @@ impl WebsysDom { // "Result" cannot be received from JS // Instead, we just build and immediately execute a closure that returns result match decode_trigger(event) { - Ok(synthetic_event) => trigger.as_ref()(synthetic_event), + Ok(synthetic_event) => trigger.as_ref()(SchedulerMsg::UiEvent(synthetic_event)), Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), }; }) as Box); @@ -560,12 +561,11 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { let triggered_scope = gi_id; // let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into(); log::debug!("Triggered scope is {:#?}", triggered_scope); - Ok(EventTrigger::new( - virtual_event_from_websys_event(event.clone()), - ScopeId(triggered_scope as usize), - Some(ElementId(real_id as usize)), - dioxus_core::events::EventPriority::High, - )) + Ok(EventTrigger { + event: virtual_event_from_websys_event(event.clone()), + mounted_dom_id: Some(ElementId(real_id as usize)), + scope: ScopeId(triggered_scope as usize), + }) } pub fn prepare_websys_dom() -> Element { diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index e8180ef85..b22481676 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -97,8 +97,7 @@ where F: FnOnce(WebConfig) -> WebConfig, { let config = config(WebConfig::default()); - let fut = run_with_props(root, root_props, config); - wasm_bindgen_futures::spawn_local(fut); + wasm_bindgen_futures::spawn_local(run_with_props(root, root_props, config)); } /// This method is the primary entrypoint for Websys Dioxus apps. Will panic if an error occurs while rendering. /// See DioxusErrors for more information on how these errors could occour. @@ -143,9 +142,8 @@ pub async fn run_with_props(root: FC, root_props: T, let deadline = work_loop.wait_for_idle_time().await; - if let Some(mut mutations) = dom.run_with_deadline(deadline).await { - work_loop.wait_for_raf().await; - websys_dom.process_edits(&mut mutations.edits); - } + let mut mutations = dom.run_with_deadline(deadline).await; + work_loop.wait_for_raf().await; + websys_dom.process_edits(&mut mutations[0].edits); } } From 927b05f358221aa4b9c440608704e86c2400ab64 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 27 Aug 2021 09:40:04 -0400 Subject: [PATCH 40/53] wip: cleanup public apis --- packages/core/Cargo.toml | 3 - packages/core/README.md | 39 ----------- packages/core/src/debug_renderer.rs | 78 --------------------- packages/core/src/diff.rs | 61 ++++++---------- packages/core/src/diff_stack.rs | 2 +- packages/core/src/dom_edits.rs | 105 ---------------------------- packages/core/src/error.rs | 30 -------- packages/core/src/lib.rs | 29 ++++---- packages/core/src/mutations.rs | 97 +++++++++++++++++++++++++ packages/core/src/scheduler.rs | 17 +++-- packages/core/src/scope.rs | 17 ++--- packages/core/src/util.rs | 19 ----- packages/core/src/virtual_dom.rs | 52 +++++++------- 13 files changed, 178 insertions(+), 371 deletions(-) delete mode 100644 packages/core/src/debug_renderer.rs delete mode 100644 packages/core/src/dom_edits.rs delete mode 100644 packages/core/src/error.rs diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 96aa0da97..3b3c39c28 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -16,9 +16,6 @@ dioxus-core-macro = { path = "../core-macro", version = "0.1.1" } # Bumpalo is used as a micro heap backing each component bumpalo = { version = "3.6.0", features = ["collections", "boxed"] } -# custom error type -thiserror = "1" - # faster hashmaps fxhash = "0.2.1" diff --git a/packages/core/README.md b/packages/core/README.md index 01152bfc9..62a876453 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -40,42 +40,3 @@ We have big goals for Dioxus. The final implementation must: - Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs. - Be modular. Components and hooks should be work anywhere without worrying about target platform. -## Optimizations - -- Support a pluggable allocation strategy that makes VNode creation **very** fast -- Support lazy VNodes (ie VNodes that are not actually created when the html! macro is used) -- Support advanced diffing strategies (patience, Meyers, etc) - -```rust - -rsx!{ "this is a text node" } - -rsx!{ - div {} - "asd" - div {} - div {} -} -rsx!{ - div { - a {} - b {} - c {} - Container { - Container { - Container { - Container { - Container { - div {} - } - } - } - } - } - } -} - - - - -``` diff --git a/packages/core/src/debug_renderer.rs b/packages/core/src/debug_renderer.rs deleted file mode 100644 index 38604cb1c..000000000 --- a/packages/core/src/debug_renderer.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Debug virtual doms! -//! This renderer comes built in with dioxus core and shows how to implement a basic renderer. -//! -//! Renderers don't actually need to own the virtual dom (it's up to the implementer). - -use crate::innerlude::RealDom; -use crate::{events::EventTrigger, virtual_dom::VirtualDom}; -use crate::{innerlude::Result, prelude::*}; - -pub struct DebugRenderer { - internal_dom: VirtualDom, -} - -impl DebugRenderer { - /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component. - /// - /// This means that the root component must either consumes its own context, or statics are used to generate the page. - /// The root component can access things like routing in its context. - pub fn new(root: FC<()>) -> Self { - Self::new_with_props(root, ()) - } - - /// Create a new text-renderer instance from a functional component root. - /// Automatically progresses the creation of the VNode tree to completion. - /// - /// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom` - pub fn new_with_props(root: FC, root_props: T) -> Self { - Self::from_vdom(VirtualDom::new_with_props(root, root_props)) - } - - /// Create a new text renderer from an existing Virtual DOM. - pub fn from_vdom(dom: VirtualDom) -> Self { - // todo: initialize the event registry properly - Self { internal_dom: dom } - } - - pub fn handle_event(&mut self, trigger: EventTrigger) -> Result<()> { - Ok(()) - } - - // pub fn step(&mut self, machine: &mut DiffMachine) -> Result<()> { - // Ok(()) - // } - - // this does a "holy" compare - if something is missing in the rhs, it doesn't complain. - // it only complains if something shows up that's not in the lhs, *or* if a value is different. - // This lets you exclude various fields if you just want to drill in to a specific prop - // It leverages the internal diffing mechanism. - // If you have a list or "nth" child, you do need to list those children, but you don't need to - // fill in their children/attrs/etc - // Does not handle children or lifecycles and will always fail the test if they show up in the rhs - pub fn compare(&self, other: LazyNodes) -> Result<()> - where - F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>, - { - Ok(()) - } - - // Do a full compare - everything must match - // Ignores listeners and children components - pub fn compare_full(&self, other: LazyNodes) -> Result<()> - where - F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>, - { - Ok(()) - } - - pub fn trigger_listener(&mut self, id: usize) -> Result<()> { - Ok(()) - } - - pub fn render_nodes(&self, other: LazyNodes) -> Result<()> - where - F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>, - { - Ok(()) - } -} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 7c6ef2751..3c8b3ebb9 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -105,7 +105,7 @@ use DomEdit::*; /// /// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's /// stack machines all the way down! -pub struct DiffMachine<'bump> { +pub(crate) struct DiffMachine<'bump> { pub vdom: &'bump ResourcePool, pub mutations: Mutations<'bump>, pub stack: DiffStack<'bump>, @@ -115,7 +115,7 @@ pub struct DiffMachine<'bump> { /// a "saved" form of a diff machine /// in regular diff machine, the &'bump reference is a stack borrow, but the /// bump lifetimes are heap borrows. -pub struct SavedDiffWork<'bump> { +pub(crate) struct SavedDiffWork<'bump> { pub mutations: Mutations<'bump>, pub stack: DiffStack<'bump>, pub seen_scopes: FxHashSet, @@ -174,36 +174,23 @@ impl<'bump> DiffMachine<'bump> { /// /// We do depth-first to maintain high cache locality (nodes were originally generated recursively). pub async fn work(&mut self) { - // 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.stack.pop() { + // defer to individual functions so the compiler produces better code + // large functions tend to be difficult for the compiler to work with match instruction { DiffInstruction::PopScope => { self.stack.pop_scope(); } - DiffInstruction::DiffNode { old, new, .. } => { - self.diff_node(old, new); - } + DiffInstruction::DiffNode { old, new, .. } => self.diff_node(old, new), - DiffInstruction::DiffChildren { old, new } => { - self.diff_children(old, new); - } + DiffInstruction::DiffChildren { old, new } => self.diff_children(old, new), - DiffInstruction::Create { node } => { - self.create_node(node); - } + DiffInstruction::Create { node } => self.create_node(node), - DiffInstruction::Mount { and } => { - self.mount(and); - } + DiffInstruction::Mount { and } => self.mount(and), - DiffInstruction::PrepareMoveNode { node } => { - for el in RealChildIterator::new(node, self.vdom) { - self.mutations.push_root(el.direct_id()); - self.stack.add_child_count(1); - } - } + DiffInstruction::PrepareMoveNode { node } => self.prepare_move_node(node), }; // todo: call this less frequently, there is a bit of overhead involved @@ -211,6 +198,13 @@ impl<'bump> DiffMachine<'bump> { } } + fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) { + for el in RealChildIterator::new(node, self.vdom) { + self.mutations.push_root(el.direct_id()); + self.stack.add_child_count(1); + } + } + fn mount(&mut self, and: MountType<'bump>) { let nodes_created = self.stack.pop_nodes_created(); match and { @@ -363,22 +357,12 @@ impl<'bump> DiffMachine<'bump> { let new_component = self.vdom.get_scope_mut(new_idx).unwrap(); // Run the scope for one iteration to initialize it - match new_component.run_scope() { - Ok(_g) => { - // all good, new nodes exist - } - Err(err) => { - // failed to run. this is the first time the component ran, and it failed - // we manually set its head node to an empty fragment - panic!("failing components not yet implemented"); - } + if new_component.run_scope() { + // Take the node that was just generated from running the component + let nextnode = new_component.frames.fin_head(); + self.stack.create_component(new_idx, nextnode); } - // Take the node that was just generated from running the component - let nextnode = new_component.frames.fin_head(); - - self.stack.create_component(new_idx, nextnode); - // Finally, insert this scope as a seen node. self.seen_scopes.insert(new_idx); } @@ -545,8 +529,9 @@ impl<'bump> DiffMachine<'bump> { } false => { // the props are different... - scope.run_scope().unwrap(); - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); + if scope.run_scope() { + self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); + } } } diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index c09668169..5c4f39646 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -40,7 +40,7 @@ pub enum MountType<'a> { InsertBefore { other_node: &'a VNode<'a> }, } -pub struct DiffStack<'bump> { +pub(crate) struct DiffStack<'bump> { instructions: Vec>, nodes_created_stack: SmallVec<[usize; 10]>, pub scope_stack: SmallVec<[ScopeId; 5]>, diff --git a/packages/core/src/dom_edits.rs b/packages/core/src/dom_edits.rs deleted file mode 100644 index cbb70f248..000000000 --- a/packages/core/src/dom_edits.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! -//! -//! -//! -//! -//! - -use crate::innerlude::ScopeId; - -/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the -/// network or through FFI boundaries. -#[derive(Debug, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] -pub enum DomEdit<'bump> { - PushRoot { - id: u64, - }, - PopRoot, - - AppendChildren { - many: u32, - }, - - // "Root" refers to the item direclty - // it's a waste of an instruction to push the root directly - ReplaceWith { - root: u64, - m: u32, - }, - InsertAfter { - root: u64, - n: u32, - }, - InsertBefore { - root: u64, - n: u32, - }, - Remove { - root: u64, - }, - - RemoveAllChildren, - CreateTextNode { - text: &'bump str, - id: u64, - }, - CreateElement { - tag: &'bump str, - id: u64, - }, - CreateElementNs { - tag: &'bump str, - id: u64, - ns: &'static str, - }, - CreatePlaceholder { - id: u64, - }, - NewEventListener { - event_name: &'static str, - scope: ScopeId, - mounted_node_id: u64, - }, - RemoveEventListener { - event: &'static str, - }, - SetText { - text: &'bump str, - }, - SetAttribute { - field: &'static str, - value: &'bump str, - ns: Option<&'bump str>, - }, - RemoveAttribute { - name: &'static str, - }, -} -impl DomEdit<'_> { - pub fn is(&self, id: &'static str) -> bool { - match self { - DomEdit::InsertAfter { .. } => id == "InsertAfter", - DomEdit::InsertBefore { .. } => id == "InsertBefore", - DomEdit::PushRoot { .. } => id == "PushRoot", - DomEdit::PopRoot => id == "PopRoot", - DomEdit::AppendChildren { .. } => id == "AppendChildren", - DomEdit::ReplaceWith { .. } => id == "ReplaceWith", - DomEdit::Remove { .. } => id == "Remove", - DomEdit::RemoveAllChildren => id == "RemoveAllChildren", - DomEdit::CreateTextNode { .. } => id == "CreateTextNode", - DomEdit::CreateElement { .. } => id == "CreateElement", - DomEdit::CreateElementNs { .. } => id == "CreateElementNs", - DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder", - DomEdit::NewEventListener { .. } => id == "NewEventListener", - DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener", - DomEdit::SetText { .. } => id == "SetText", - DomEdit::SetAttribute { .. } => id == "SetAttribute", - DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute", - } - } -} diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs deleted file mode 100644 index 8e675b026..000000000 --- a/packages/core/src/error.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Internal error handling for Dioxus -//! -//! - -use thiserror::Error as ThisError; -pub type Result = std::result::Result; - -#[derive(ThisError, Debug)] -pub enum Error { - #[error("Fatal Internal Error: {0}")] - FatalInternal(&'static str), - - #[error("Context is missing")] - MissingSharedContext, - - #[error("No event to progress")] - NoEvent, - - #[error("Wrong Properties Type")] - WrongProps, - - #[error("The component failed to return VNodes")] - ComponentFailed, - - #[error("Base scope has not been mounted yet")] - NotMounted, - - #[error("I/O Error: {0}")] - IO(#[from] std::io::Error), -} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index d6d4fece6..6b8a2909d 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,14 +1,5 @@ #![allow(non_snake_case)] #![doc = include_str!("../README.md")] -//! Dioxus Core -//! ---------- -//! -//! -//! -//! -//! -//! -//! pub use crate::innerlude::{ format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId, @@ -26,7 +17,6 @@ pub mod prelude { pub use dioxus_core_macro::{format_args_f, html, rsx, Props}; } -// types used internally that are important pub(crate) mod innerlude { pub use crate::bumpframe::*; pub use crate::childiter::*; @@ -34,8 +24,6 @@ pub(crate) mod innerlude { pub use crate::context::*; pub use crate::diff::*; pub use crate::diff_stack::*; - pub use crate::dom_edits::*; - pub use crate::error::*; pub use crate::events::*; pub use crate::heuristics::*; pub use crate::hooklist::*; @@ -43,7 +31,6 @@ pub(crate) mod innerlude { pub use crate::mutations::*; pub use crate::nodes::*; pub use crate::scheduler::*; - // pub use crate::scheduler::*; pub use crate::scope::*; pub use crate::util::*; pub use crate::virtual_dom::*; @@ -57,19 +44,29 @@ pub(crate) mod innerlude { pub mod exports { //! Important dependencies that are used by the rest of the library - // the foundation of this library pub use bumpalo; } +/* +Navigating this crate: +- virtual_dom: the primary entrypoint for the crate +- scheduler: the core interior logic called by virtual_dom +- nodes: the definition of VNodes, listeners, etc. +- + + +Some utilities + + + +*/ pub mod bumpframe; pub mod childiter; pub mod component; pub mod context; pub mod diff; pub mod diff_stack; -pub mod dom_edits; -pub mod error; pub mod events; pub mod heuristics; pub mod hooklist; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index e16126cdc..1eed2c323 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -163,3 +163,100 @@ impl<'a> NodeRefMutation<'a> { .and_then(|f| f.downcast_mut::()) } } + +/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the +/// network or through FFI boundaries. +#[derive(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum DomEdit<'bump> { + PushRoot { + id: u64, + }, + PopRoot, + + AppendChildren { + many: u32, + }, + + // "Root" refers to the item direclty + // it's a waste of an instruction to push the root directly + ReplaceWith { + root: u64, + m: u32, + }, + InsertAfter { + root: u64, + n: u32, + }, + InsertBefore { + root: u64, + n: u32, + }, + Remove { + root: u64, + }, + + RemoveAllChildren, + CreateTextNode { + text: &'bump str, + id: u64, + }, + CreateElement { + tag: &'bump str, + id: u64, + }, + CreateElementNs { + tag: &'bump str, + id: u64, + ns: &'static str, + }, + CreatePlaceholder { + id: u64, + }, + NewEventListener { + event_name: &'static str, + scope: ScopeId, + mounted_node_id: u64, + }, + RemoveEventListener { + event: &'static str, + }, + SetText { + text: &'bump str, + }, + SetAttribute { + field: &'static str, + value: &'bump str, + ns: Option<&'bump str>, + }, + RemoveAttribute { + name: &'static str, + }, +} +impl DomEdit<'_> { + pub fn is(&self, id: &'static str) -> bool { + match self { + DomEdit::InsertAfter { .. } => id == "InsertAfter", + DomEdit::InsertBefore { .. } => id == "InsertBefore", + DomEdit::PushRoot { .. } => id == "PushRoot", + DomEdit::PopRoot => id == "PopRoot", + DomEdit::AppendChildren { .. } => id == "AppendChildren", + DomEdit::ReplaceWith { .. } => id == "ReplaceWith", + DomEdit::Remove { .. } => id == "Remove", + DomEdit::RemoveAllChildren => id == "RemoveAllChildren", + DomEdit::CreateTextNode { .. } => id == "CreateTextNode", + DomEdit::CreateElement { .. } => id == "CreateElement", + DomEdit::CreateElementNs { .. } => id == "CreateElementNs", + DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder", + DomEdit::NewEventListener { .. } => id == "NewEventListener", + DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener", + DomEdit::SetText { .. } => id == "SetText", + DomEdit::SetAttribute { .. } => id == "SetAttribute", + DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute", + } + } +} diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 910ba3ea6..9b8ec1a2f 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -128,7 +128,12 @@ pub enum SchedulerMsg { /// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task. /// /// -pub struct Scheduler { +pub(crate) struct Scheduler { + /// All mounted components are arena allocated to make additions, removals, and references easy to work with + /// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena. + /// + /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas + /// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually. pub pool: ResourcePool, pub heuristics: HeuristicsEngine, @@ -523,7 +528,7 @@ impl Scheduler { } } -pub struct PriorityLane { +pub(crate) struct PriorityLane { pub dirty_scopes: IndexSet, pub saved_state: Option>, pub in_progress: bool, @@ -682,7 +687,7 @@ impl ResourcePool { &'b self, _id: ScopeId, _f: impl FnOnce(&'b mut Scope) -> O, - ) -> Result { + ) -> Option { todo!() } @@ -692,13 +697,13 @@ impl ResourcePool { &self, _id: ScopeId, _f: impl FnOnce(&mut Scope) -> &VNode<'b>, - ) -> Result<&VNode<'b>> { + ) -> Option<&VNode<'b>> { todo!() } - pub fn try_remove(&self, id: ScopeId) -> Result { + pub fn try_remove(&self, id: ScopeId) -> Option { let inner = unsafe { &mut *self.components.get() }; - Ok(inner.remove(id.0)) + Some(inner.remove(id.0)) // .try_remove(id.0) // .ok_or_else(|| Error::FatalInternal("Scope not found")) } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 40bfd43ec..081b1bd8f 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -113,7 +113,9 @@ impl Scope { self.child_nodes = child_nodes; } - pub(crate) fn run_scope<'sel>(&'sel mut self) -> Result<()> { + /// Returns true if the scope completed successfully + /// + pub(crate) fn run_scope<'sel>(&'sel mut self) -> bool { // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners @@ -133,17 +135,12 @@ impl Scope { let render: &WrappedCaller = self.caller.as_ref(); match render(self) { - None => { - // the user's component failed. We avoid cycling to the next frame - log::error!("Running your component failed! It will no longer receive events."); - Err(Error::ComponentFailed) - } + None => false, Some(new_head) => { // the user's component succeeded. We can safely cycle to the next frame self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; self.frames.cycle_frame(); - log::debug!("Successfully rendered component"); - Ok(()) + true } } } @@ -202,7 +199,7 @@ impl Scope { &mut self, event: SyntheticEvent, element: ElementId, - ) -> Result<()> { + ) -> Option<()> { let listners = self.listeners.borrow_mut(); let raw_listener = listners.iter().find(|lis| { @@ -226,7 +223,7 @@ impl Scope { log::warn!("An event was triggered but there was no listener to handle it"); } - Ok(()) + Some(()) } pub fn root(&self) -> &VNode { diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index c7309fe68..fc406b0a2 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -11,22 +11,3 @@ pub fn empty_cell() -> Cell> { pub fn type_name_of(_: T) -> &'static str { std::any::type_name::() } - -// /// A helper type that lets scopes be ordered by their height -// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// pub struct HeightMarker { -// pub idx: ScopeId, -// pub height: u32, -// } - -// impl Ord for HeightMarker { -// fn cmp(&self, other: &Self) -> std::cmp::Ordering { -// self.height.cmp(&other.height) -// } -// } - -// impl PartialOrd for HeightMarker { -// fn partial_cmp(&self, other: &Self) -> Option { -// Some(self.cmp(other)) -// } -// } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 6c75899ba..6b152b77b 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -18,6 +18,7 @@ //! //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. + use crate::innerlude::*; use futures_util::{pin_mut, Future, FutureExt}; use std::{ @@ -35,23 +36,13 @@ use std::{ /// /// pub struct VirtualDom { - /// All mounted components are arena allocated to make additions, removals, and references easy to work with - /// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena. - /// - /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas - /// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually. - pub scheduler: Scheduler, + scheduler: Scheduler, - /// The index of the root component - /// Should always be the first (gen=0, id=0) base_scope: ScopeId, - // for managing the props that were used to create the dom - #[doc(hidden)] - _root_prop_type: std::any::TypeId, + root_prop_type: std::any::TypeId, - #[doc(hidden)] - _root_props: std::pin::Pin>, + root_props: Pin>, } impl VirtualDom { @@ -67,8 +58,8 @@ impl VirtualDom { /// /// # Example /// ``` - /// fn Example(cx: Context) -> VNode { - /// cx.render(rsx!{ div{"hello world"} }) + /// fn Example(cx: Context<()>) -> DomTree { + /// cx.render(rsx!( div { "hello world" } )) /// } /// /// let dom = VirtualDom::new(Example); @@ -91,25 +82,34 @@ impl VirtualDom { /// /// # Example /// ``` - /// fn Example(cx: Context) -> VNode { - /// cx.render(rsx!{ div{"hello world"} }) + /// #[derive(PartialEq, Props)] + /// struct SomeProps { + /// name: &'static str + /// } + /// + /// fn Example(cx: Context) -> DomTree { + /// cx.render(rsx!{ div{ "hello {cx.name}" } }) /// } /// /// let dom = VirtualDom::new(Example); /// ``` /// - /// Note: the VirtualDOM is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. + /// Note: the VirtualDOM is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it. + /// + /// ```rust + /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); + /// let mutations = dom.rebuild(); + /// ``` pub fn new_with_props(root: FC

, root_props: P) -> Self { let scheduler = Scheduler::new(); let _root_props: Pin> = Box::pin(root_props); let _root_prop_type = TypeId::of::

(); - let props_ptr = _root_props.as_ref().downcast_ref::

().unwrap() as *const P; + let props_ptr = _root_props.downcast_ref::

().unwrap() as *const P; let base_scope = scheduler.pool.insert_scope_with_key(|myidx| { let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); - let name = type_name_of(root); Scope::new( caller, myidx, @@ -122,9 +122,9 @@ impl VirtualDom { Self { base_scope, - _root_props, scheduler, - _root_prop_type, + root_props: _root_props, + root_prop_type: _root_prop_type, } } @@ -168,7 +168,7 @@ impl VirtualDom { .expect("The base scope should never be moved"); // // 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() { + if cur_component.run_scope() { diff_machine .stack .create_node(cur_component.frames.fin_head(), MountType::Append); @@ -204,9 +204,9 @@ impl VirtualDom { .get_scope_mut(self.base_scope) .expect("The base scope should never be moved"); - cur_component.run_scope().unwrap(); - - diff_machine.diff_scope(self.base_scope).await; + if cur_component.run_scope() { + diff_machine.diff_scope(self.base_scope).await; + } diff_machine.mutations } From 31702dbf878dd0207d101f7869ebefd2bb9f6860 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 27 Aug 2021 09:53:26 -0400 Subject: [PATCH 41/53] feat: wire up resource pool --- packages/core/src/childiter.rs | 3 +- packages/core/src/diff.rs | 16 +++---- packages/core/src/nodes.rs | 73 ++++++++++++++------------------ packages/core/src/scope.rs | 56 ++++++++++++------------ packages/core/src/virtual_dom.rs | 4 +- packages/ssr/src/lib.rs | 2 +- 6 files changed, 73 insertions(+), 81 deletions(-) diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs index 3b762c547..2f5ffde96 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/childiter.rs @@ -74,7 +74,8 @@ impl<'a> Iterator for RealChildIterator<'a> { // For components, we load their root and push them onto the stack VNode::Component(sc) => { let scope = - unsafe { self.scopes.get_scope(sc.ass_scope.get().unwrap()) }.unwrap(); + unsafe { self.scopes.get_scope(sc.associated_scope.get().unwrap()) } + .unwrap(); // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); // Simply swap the current node on the stack with the root of the component diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 3c8b3ebb9..59011301e 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -341,7 +341,7 @@ impl<'bump> DiffMachine<'bump> { }); // Actually initialize the caller's slot with the right address - vcomponent.ass_scope.set(Some(new_idx)); + vcomponent.associated_scope.set(Some(new_idx)); if !vcomponent.can_memoize { let cur_scope = self.vdom.get_scope_mut(parent_idx).unwrap(); @@ -357,7 +357,7 @@ impl<'bump> DiffMachine<'bump> { let new_component = self.vdom.get_scope_mut(new_idx).unwrap(); // Run the scope for one iteration to initialize it - if new_component.run_scope() { + if new_component.run_scope(self.vdom) { // Take the node that was just generated from running the component let nextnode = new_component.frames.fin_head(); self.stack.create_component(new_idx, nextnode); @@ -505,7 +505,7 @@ impl<'bump> DiffMachine<'bump> { old: &'bump VComponent<'bump>, new: &'bump VComponent<'bump>, ) { - let scope_addr = old.ass_scope.get().unwrap(); + let scope_addr = old.associated_scope.get().unwrap(); // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { @@ -513,7 +513,7 @@ impl<'bump> DiffMachine<'bump> { self.stack.scope_stack.push(scope_addr); // Make sure the new component vnode is referencing the right scope id - new.ass_scope.set(Some(scope_addr)); + new.associated_scope.set(Some(scope_addr)); // make sure the component's caller function is up to date let scope = self.vdom.get_scope_mut(scope_addr).unwrap(); @@ -529,7 +529,7 @@ impl<'bump> DiffMachine<'bump> { } false => { // the props are different... - if scope.run_scope() { + if scope.run_scope(self.vdom) { self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); } } @@ -967,7 +967,7 @@ impl<'bump> DiffMachine<'bump> { search_node = frag.children.last(); } VNode::Component(el) => { - let scope_id = el.ass_scope.get().unwrap(); + let scope_id = el.associated_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); search_node = Some(scope.root()); } @@ -985,7 +985,7 @@ impl<'bump> DiffMachine<'bump> { search_node = Some(&frag.children[0]); } VNode::Component(el) => { - let scope_id = el.ass_scope.get().unwrap(); + let scope_id = el.associated_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); search_node = Some(scope.root()); } @@ -1043,7 +1043,7 @@ impl<'bump> DiffMachine<'bump> { } VNode::Component(c) => { - let scope_id = c.ass_scope.get().unwrap(); + let scope_id = c.associated_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); let root = scope.root(); self.remove_nodes(Some(root)); diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 5e2db6ef8..a66e96727 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -13,7 +13,6 @@ use std::{ cell::{Cell, RefCell}, fmt::{Arguments, Debug, Formatter}, marker::PhantomData, - mem::ManuallyDrop, rc::Rc, }; @@ -43,8 +42,8 @@ impl<'src> VNode<'src> { VNode::Component(c) => c.key, VNode::Fragment(f) => f.key, VNode::Text(_t) => None, - VNode::Suspended(s) => None, - VNode::Anchor(f) => None, + VNode::Suspended(_s) => None, + VNode::Anchor(_f) => None, } } pub fn direct_id(&self) -> ElementId { @@ -68,16 +67,36 @@ pub struct VAnchor { pub struct VText<'src> { pub text: &'src str, + pub dom_id: Cell>, + pub is_static: bool, } pub struct VFragment<'src> { - pub children: &'src [VNode<'src>], pub key: Option<&'src str>, + + pub children: &'src [VNode<'src>], + pub is_static: bool, } +pub struct VElement<'a> { + pub tag_name: &'static str, + + pub namespace: Option<&'static str>, + + pub key: Option<&'a str>, + + pub dom_id: Cell>, + + pub listeners: &'a [Listener<'a>], + + pub attributes: &'a [Attribute<'a>], + + pub children: &'a [VNode<'a>], +} + pub trait DioxusElement { const TAG_NAME: &'static str; const NAME_SPACE: Option<&'static str>; @@ -91,19 +110,6 @@ pub trait DioxusElement { } } -pub struct VElement<'a> { - pub key: Option<&'a str>, - - // tag is always static - pub tag_name: &'static str, - pub namespace: Option<&'static str>, - pub dom_id: Cell>, - - pub listeners: &'a [Listener<'a>], - pub attributes: &'a [Attribute<'a>], - pub children: &'a [VNode<'a>], -} - /// An attribute on a DOM node, such as `id="my-thing"` or /// `href="https://example.com"`. #[derive(Clone, Debug)] @@ -131,22 +137,12 @@ pub struct Listener<'bump> { pub(crate) callback: RefCell>>, } -impl Listener<'_> { - // serialize the listener event stuff to a string - pub fn serialize(&self) { - // - } - pub fn deserialize() { - // - } -} - /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { pub key: Option<&'src str>, - pub ass_scope: Cell>, + pub associated_scope: Cell>, pub(crate) caller: Rc DomTree>, @@ -158,21 +154,19 @@ pub struct VComponent<'src> { pub is_static: bool, - pub can_memoize: bool, + pub(crate) can_memoize: bool, - pub name: &'static str, - - // a pointer into the bump arena (given by the 'src lifetime) + // Raw pointer into the bump arena for the props of the component pub(crate) raw_props: *const (), - // a pointer to the raw fn typ + // Function pointer to the FC that was used to generate this component pub(crate) user_fc: *const (), } pub struct VSuspended<'a> { - pub dom_id: Cell>, pub task_id: u64, - pub callback: RefCell) -> DomTree<'a>>>, + pub dom_id: Cell>, + pub(crate) callback: RefCell) -> DomTree<'a>>>, } /// This struct provides an ergonomic API to quickly build VNodes. @@ -350,10 +344,8 @@ impl<'a> NodeFactory<'a> { { let bump = self.bump(); - let name = crate::util::type_name_of(component); - // We don't want the fat part of the fat pointer - // This function does static dispatch so we don't need any VTable stuff + // THe "Fat" part is provided on next-render let children: &'a V = bump.alloc(children); let children = children.as_ref(); @@ -415,9 +407,8 @@ impl<'a> NodeFactory<'a> { is_static, drop_props: RefCell::new(Some(drop_props)), can_memoize: P::IS_STATIC, - ass_scope: Cell::new(None), + associated_scope: Cell::new(None), key, - name, })) } @@ -663,7 +654,7 @@ impl Debug for VNode<'_> { VNode::Component(comp) => write!( s, "VComponent {{ fc: {:?}, children: {:?} }}", - comp.name, comp.children + comp.user_fc, comp.children ), } } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 081b1bd8f..c9350b6de 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -115,12 +115,12 @@ impl Scope { /// Returns true if the scope completed successfully /// - pub(crate) fn run_scope<'sel>(&'sel mut self) -> bool { + pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool { // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners - self.ensure_drop_safety(); + self.ensure_drop_safety(pool); // Safety: // - We dropped the listeners, so no more &mut T can be used while these are held @@ -154,7 +154,7 @@ impl Scope { /// /// Refrences to hook data can only be stored in listeners and component props. During diffing, we make sure to log /// all listeners and borrowed props so we can clear them here. - fn ensure_drop_safety(&mut self) { + fn ensure_drop_safety(&mut self, pool: &ResourcePool) { // make sure all garabge is collected before trying to proceed with anything else debug_assert!( self.pending_garbage.borrow().is_empty(), @@ -163,33 +163,33 @@ impl Scope { // todo!("arch changes"); - // // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we - // // run the hooks (which hold an &mut Referrence) - // // right now, we don't drop - // // let vdom = &self.vdom; - // self.borrowed_props - // .get_mut() - // .drain(..) - // .map(|li| unsafe { &*li }) - // .for_each(|comp| { - // // First drop the component's undropped references - // let scope_id = comp.ass_scope.get().unwrap(); - // let scope = unsafe { vdom.get_scope_mut(scope_id) }.unwrap(); - // scope.ensure_drop_safety(); + // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we + // run the hooks (which hold an &mut Referrence) + // right now, we don't drop + // let vdom = &self.vdom; + self.borrowed_props + .get_mut() + .drain(..) + .map(|li| unsafe { &*li }) + .for_each(|comp| { + // First drop the component's undropped references + let scope_id = comp.associated_scope.get().unwrap(); + let scope = pool.get_scope_mut(scope_id).unwrap(); + scope.ensure_drop_safety(pool); - // // Now, drop our own reference - // let mut dropper = comp.drop_props.borrow_mut().take().unwrap(); - // dropper(); - // }); + // Now, drop our own reference + let mut dropper = comp.drop_props.borrow_mut().take().unwrap(); + dropper(); + }); - // // Now that all the references are gone, we can safely drop our own references in our listeners. - // self.listeners - // .get_mut() - // .drain(..) - // .map(|li| unsafe { &*li }) - // .for_each(|listener| { - // listener.callback.borrow_mut().take(); - // }); + // Now that all the references are gone, we can safely drop our own references in our listeners. + self.listeners + .get_mut() + .drain(..) + .map(|li| unsafe { &*li }) + .for_each(|listener| { + listener.callback.borrow_mut().take(); + }); } // A safe wrapper around calling listeners diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 6b152b77b..f8d3ebb61 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -168,7 +168,7 @@ impl VirtualDom { .expect("The base scope should never be moved"); // // We run the component. If it succeeds, then we can diff it and add the changes to the dom. - if cur_component.run_scope() { + if cur_component.run_scope(&self.scheduler.pool) { diff_machine .stack .create_node(cur_component.frames.fin_head(), MountType::Append); @@ -204,7 +204,7 @@ impl VirtualDom { .get_scope_mut(self.base_scope) .expect("The base scope should never be moved"); - if cur_component.run_scope() { + if cur_component.run_scope(&self.scheduler.pool) { diff_machine.diff_scope(self.base_scope).await; } diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 00c028cd1..e8cc94689 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -169,7 +169,7 @@ impl<'a> TextRenderer<'a> { } } VNode::Component(vcomp) => { - let idx = vcomp.ass_scope.get().unwrap(); + let idx = vcomp.associated_scope.get().unwrap(); match (self.vdom, self.cfg.skip_components) { (Some(vdom), false) => { let new_node = vdom.get_scope(idx).unwrap().root(); From 9f99f46cfd6355ce46afdf84d60dd049047c6b57 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 27 Aug 2021 09:56:49 -0400 Subject: [PATCH 42/53] wip: more cleanup in scheduler --- packages/core/src/scheduler.rs | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 9b8ec1a2f..ba5ad4f10 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -68,32 +68,23 @@ do anything too arduous from onInput. For the rest, we defer to the rIC period and work down each queue from high to low. */ -use std::cell::{Cell, RefCell, RefMut}; -use std::fmt::Display; -use std::intrinsics::transmute; -use std::{cell::UnsafeCell, rc::Rc}; - use crate::heuristics::*; use crate::innerlude::*; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::stream::FuturesUnordered; +use futures_util::{future::FusedFuture, pin_mut, Future, FutureExt, StreamExt}; use fxhash::{FxHashMap, FxHashSet}; use indexmap::IndexSet; use slab::Slab; use smallvec::SmallVec; - -use std::any::Any; - -use std::any::TypeId; -use std::cell::Ref; -use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; -use std::pin::Pin; - -use futures_util::future::FusedFuture; -use futures_util::pin_mut; -use futures_util::Future; -use futures_util::FutureExt; -use futures_util::StreamExt; +use std::{ + any::{Any, TypeId}, + cell::{Cell, RefCell, RefMut, UnsafeCell}, + collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}, + fmt::Display, + pin::Pin, + rc::Rc, +}; #[derive(Clone)] pub struct EventChannel { From 5cbdc571e3abe54391aec8f296d7389410b373b6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 31 Aug 2021 12:28:44 -0400 Subject: [PATCH 43/53] more cleanup --- packages/core/src/bumpframe.rs | 4 +- packages/core/src/childiter.rs | 15 +---- packages/core/src/context.rs | 1 - packages/core/src/events.rs | 16 +++--- packages/core/src/hooks.rs | 2 +- packages/core/src/lib.rs | 102 ++++++++++++++++----------------- packages/core/src/scheduler.rs | 83 +++++++++++---------------- packages/core/src/scope.rs | 8 +-- packages/core/src/util.rs | 53 +++++++++++++++++ packages/core/src/yield_now.rs | 56 ------------------ 10 files changed, 148 insertions(+), 192 deletions(-) delete mode 100644 packages/core/src/yield_now.rs diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index 59a6252a5..8696d4e8e 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -2,7 +2,7 @@ use crate::innerlude::*; use bumpalo::Bump; use std::cell::Cell; -pub struct ActiveFrame { +pub(crate) struct ActiveFrame { // We use a "generation" for users of contents in the bump frames to ensure their data isn't broken pub generation: Cell, @@ -10,7 +10,7 @@ pub struct ActiveFrame { pub frames: [BumpFrame; 2], } -pub struct BumpFrame { +pub(crate) struct BumpFrame { pub bump: Bump, pub(crate) head_node: VNode<'static>, diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs index 2f5ffde96..5436f9355 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/childiter.rs @@ -4,7 +4,7 @@ use crate::innerlude::*; /// /// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like /// "InsertBefore". -pub struct RealChildIterator<'a> { +pub(crate) struct RealChildIterator<'a> { scopes: &'a ResourcePool, // Heuristcally we should never bleed into 4 completely nested fragments/components @@ -20,19 +20,6 @@ impl<'a> RealChildIterator<'a> { stack: smallvec::smallvec![(0, starter)], } } - - pub fn new_from_slice(nodes: &'a [VNode<'a>], scopes: &'a ResourcePool) -> Self { - let mut stack = smallvec::smallvec![]; - for node in nodes { - stack.push((0, node)); - } - Self { scopes, stack } - } - // keep the memory around - pub fn reset_with(&mut self, node: &'a VNode<'a>) { - self.stack.clear(); - self.stack.push((0, node)); - } } impl<'a> Iterator for RealChildIterator<'a> { diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 2c53384ab..903e53249 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -112,7 +112,6 @@ impl<'src, P> Context<'src, P> { } /// Get's this component's unique identifier. - /// pub fn get_scope_id(&self) -> ScopeId { self.scope.our_arena_idx.clone() } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index a52c24f53..7360d87e4 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -6,13 +6,15 @@ use crate::innerlude::{ElementId, ScopeId}; #[derive(Debug)] -pub struct EventTrigger { +pub struct UiEvent { /// The originator of the event trigger pub scope: ScopeId, /// The optional real node associated with the trigger pub mounted_dom_id: Option, + pub listener_type: &'static str, + /// The type of event pub event: SyntheticEvent, } @@ -26,7 +28,6 @@ pub enum SyntheticEvent { FocusEvent(on::FocusEvent), FormEvent(on::FormEvent), SelectionEvent(on::SelectionEvent), - UIEvent(on::UIEvent), WheelEvent(on::WheelEvent), MediaEvent(on::MediaEvent), AnimationEvent(on::AnimationEvent), @@ -46,7 +47,6 @@ impl std::fmt::Debug for SyntheticEvent { SyntheticEvent::FormEvent(_) => "FormEvent", SyntheticEvent::SelectionEvent(_) => "SelectionEvent", SyntheticEvent::TouchEvent(_) => "TouchEvent", - SyntheticEvent::UIEvent(_) => "UIEvent", SyntheticEvent::WheelEvent(_) => "WheelEvent", SyntheticEvent::MediaEvent(_) => "MediaEvent", SyntheticEvent::AnimationEvent(_) => "AnimationEvent", @@ -284,6 +284,9 @@ pub mod on { /// onmouseout onmouseout + /// + onscroll + /// onmouseover /// /// Triggered when the users's mouse hovers over an element. @@ -331,14 +334,9 @@ pub mod on { ontouchstart ]; - UIEventInner(UIEvent): [ - /// - scroll - ]; - WheelEventInner(WheelEvent): [ /// - wheel + onwheel ]; MediaEventInner(MediaEvent): [ diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index b8808993e..86f7b4b4f 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -13,7 +13,7 @@ use crate::innerlude::*; use futures_util::FutureExt; use std::{ any::{Any, TypeId}, - cell::{Cell, RefCell}, + cell::RefCell, future::Future, rc::Rc, }; diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 6b8a2909d..aa604f0ab 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,65 +1,16 @@ #![allow(non_snake_case)] #![doc = include_str!("../README.md")] -pub use crate::innerlude::{ - format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId, - EventPriority, EventTrigger, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, - SuspendedContext, SyntheticEvent, VNode, VirtualDom, FC, -}; - -pub mod prelude { - pub use crate::component::{fc_to_builder, Fragment, Properties}; - pub use crate::context::Context; - pub use crate::hooks::*; - pub use crate::innerlude::{DioxusElement, DomTree, LazyNodes, Mutations, NodeFactory, FC}; - pub use crate::nodes::VNode; - pub use crate::VirtualDom; - pub use dioxus_core_macro::{format_args_f, html, rsx, Props}; -} - -pub(crate) mod innerlude { - pub use crate::bumpframe::*; - pub use crate::childiter::*; - pub use crate::component::*; - pub use crate::context::*; - pub use crate::diff::*; - pub use crate::diff_stack::*; - pub use crate::events::*; - pub use crate::heuristics::*; - pub use crate::hooklist::*; - pub use crate::hooks::*; - pub use crate::mutations::*; - pub use crate::nodes::*; - pub use crate::scheduler::*; - 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; - - pub use dioxus_core_macro::{format_args_f, html, rsx}; -} - -pub mod exports { - //! Important dependencies that are used by the rest of the library - // the foundation of this library - pub use bumpalo; -} - /* Navigating this crate: - virtual_dom: the primary entrypoint for the crate - scheduler: the core interior logic called by virtual_dom - nodes: the definition of VNodes, listeners, etc. -- - +- diff: the stackmachine-based diffing algorithm +- hooks: foundational hooks that require crate-private APIs +- mutations: DomEdits/NodeRefs and internal API to create them Some utilities - - - */ pub mod bumpframe; pub mod childiter; @@ -78,4 +29,49 @@ pub mod scope; pub mod signals; pub mod util; pub mod virtual_dom; -pub mod yield_now; + +pub(crate) mod innerlude { + pub(crate) use crate::bumpframe::*; + pub(crate) use crate::childiter::*; + pub use crate::component::*; + pub use crate::context::*; + pub(crate) use crate::diff::*; + pub use crate::diff_stack::*; + pub use crate::events::*; + pub use crate::heuristics::*; + pub use crate::hooklist::*; + pub use crate::hooks::*; + pub use crate::mutations::*; + pub use crate::nodes::*; + pub use crate::scheduler::*; + pub use crate::scope::*; + pub use crate::util::*; + pub use crate::virtual_dom::*; + + pub type DomTree<'a> = Option>; + pub type FC

= fn(Context

) -> DomTree; + + pub use dioxus_core_macro::{format_args_f, html, rsx}; +} + +pub use crate::innerlude::{ + format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId, + EventPriority, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, + SuspendedContext, SyntheticEvent, UiEvent, VNode, VirtualDom, FC, +}; + +pub mod prelude { + pub use crate::component::{fc_to_builder, Fragment, Properties}; + pub use crate::context::Context; + pub use crate::hooks::*; + pub use crate::innerlude::{DioxusElement, DomTree, LazyNodes, Mutations, NodeFactory, FC}; + pub use crate::nodes::VNode; + pub use crate::VirtualDom; + pub use dioxus_core_macro::{format_args_f, html, rsx, Props}; +} + +pub mod exports { + //! Important dependencies that are used by the rest of the library + // the foundation of this library + pub use bumpalo; +} diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index ba5ad4f10..a1a778a84 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -97,7 +97,7 @@ pub struct EventChannel { pub enum SchedulerMsg { Immediate(ScopeId), - UiEvent(EventTrigger), + UiEvent(UiEvent), SubmitTask(FiberTask, u64), ToggleTask(u64), PauseTask(u64), @@ -140,11 +140,11 @@ pub(crate) struct Scheduler { // scheduler stuff pub current_priority: EventPriority, - pub ui_events: VecDeque, + pub ui_events: VecDeque, pub pending_immediates: VecDeque, - pub pending_tasks: VecDeque, + pub pending_tasks: VecDeque, pub garbage_scopes: HashSet, @@ -248,39 +248,37 @@ impl Scheduler { // Converts UI events into dirty scopes with various priorities pub fn consume_pending_events(&mut self) { - // while let Some(trigger) = self.ui_events.pop_back() { - // match &trigger.event { - // SyntheticEvent::ClipboardEvent(_) - // | SyntheticEvent::CompositionEvent(_) - // | SyntheticEvent::KeyboardEvent(_) - // | SyntheticEvent::FocusEvent(_) - // | SyntheticEvent::FormEvent(_) - // | SyntheticEvent::SelectionEvent(_) - // | SyntheticEvent::TouchEvent(_) - // | SyntheticEvent::UIEvent(_) - // | SyntheticEvent::WheelEvent(_) - // | SyntheticEvent::MediaEvent(_) - // | SyntheticEvent::AnimationEvent(_) - // | SyntheticEvent::TransitionEvent(_) - // | SyntheticEvent::ToggleEvent(_) - // | SyntheticEvent::MouseEvent(_) - // | SyntheticEvent::PointerEvent(_) => { - // if let Some(scope) = self.get_scope_mut(trigger.scope) { - // if let Some(element) = trigger.mounted_dom_id { - // scope.call_listener(trigger.event, element)?; + while let Some(trigger) = self.ui_events.pop_back() { + if let Some(scope) = self.pool.get_scope_mut(trigger.scope) { + if let Some(element) = trigger.mounted_dom_id { + let priority = match &trigger.event { + SyntheticEvent::ClipboardEvent(_) => {} + SyntheticEvent::CompositionEvent(_) => {} + SyntheticEvent::KeyboardEvent(_) => {} + SyntheticEvent::FocusEvent(_) => {} + SyntheticEvent::FormEvent(_) => {} + SyntheticEvent::SelectionEvent(_) => {} + SyntheticEvent::TouchEvent(_) => {} + SyntheticEvent::WheelEvent(_) => {} + SyntheticEvent::MediaEvent(_) => {} + SyntheticEvent::AnimationEvent(_) => {} + SyntheticEvent::TransitionEvent(_) => {} + SyntheticEvent::ToggleEvent(_) => {} + SyntheticEvent::MouseEvent(_) => {} + SyntheticEvent::PointerEvent(_) => {} + }; - // // let receiver = self.immediate_receiver.clone(); - // // let mut receiver = receiver.borrow_mut(); + scope.call_listener(trigger.event, element); + // let receiver = self.immediate_receiver.clone(); + // let mut receiver = receiver.borrow_mut(); - // // // Drain the immediates into the dirty scopes, setting the appropiate priorities - // // while let Ok(Some(dirty_scope)) = receiver.try_next() { - // // self.add_dirty_scope(dirty_scope, trigger.priority) - // // } - // } - // } - // } - // } - // } + // // Drain the immediates into the dirty scopes, setting the appropiate priorities + // while let Ok(Some(dirty_scope)) = receiver.try_next() { + // self.add_dirty_scope(dirty_scope, trigger.priority) + // } + } + } + } } // nothing to do, no events on channels, no work @@ -293,26 +291,13 @@ impl Scheduler { } pub fn has_work(&self) -> bool { - todo!() - // self.high_priorty.has_work() - // || self.medium_priority.has_work() - // || self.low_priority.has_work() + self.lanes.iter().find(|f| f.has_work()).is_some() } pub fn has_pending_garbage(&self) -> bool { !self.garbage_scopes.is_empty() } - fn get_current_fiber<'a>(&'a mut self) -> &mut DiffMachine<'a> { - todo!() - // 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) } - } - fn shift_priorities(&mut self) { self.current_priority = match ( self.lanes[0].has_work(), @@ -634,7 +619,7 @@ pub enum EventPriority { } #[derive(Clone)] -pub struct ResourcePool { +pub(crate) struct ResourcePool { /* This *has* to be an UnsafeCell. diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index c9350b6de..5dc02e790 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -195,11 +195,7 @@ impl Scope { // A safe wrapper around calling listeners // // - pub(crate) fn call_listener( - &mut self, - event: SyntheticEvent, - element: ElementId, - ) -> Option<()> { + pub(crate) fn call_listener(&mut self, event: SyntheticEvent, element: ElementId) { let listners = self.listeners.borrow_mut(); let raw_listener = listners.iter().find(|lis| { @@ -222,8 +218,6 @@ impl Scope { } else { log::warn!("An event was triggered but there was no listener to handle it"); } - - Some(()) } pub fn root(&self) -> &VNode { diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index fc406b0a2..d2df10eab 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -11,3 +11,56 @@ pub fn empty_cell() -> Cell> { pub fn type_name_of(_: T) -> &'static str { std::any::type_name::() } + +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/core/src/yield_now.rs b/packages/core/src/yield_now.rs deleted file mode 100644 index 8fef0848e..000000000 --- a/packages/core/src/yield_now.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Utility Code taken from async_std to immediately yield the current task to the executor. -//! -//! - -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(()) - } - } -} From acad9ca622748f96599dd02ad22aaeaae3621b76 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 31 Aug 2021 12:28:57 -0400 Subject: [PATCH 44/53] wip: web stuff --- packages/desktop/src/events.rs | 6 +++--- packages/web/src/dom.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index ee26d2fbc..2a44a7d78 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -8,7 +8,7 @@ use dioxus_core::{ on::{MouseEvent, MouseEventInner}, SyntheticEvent, }, - ElementId, EventPriority, EventTrigger, ScopeId, + ElementId, EventPriority, ScopeId, UiEvent, }; #[derive(serde::Serialize, serde::Deserialize)] @@ -17,14 +17,14 @@ struct ImEvent { mounted_dom_id: u64, scope: u64, } -pub fn trigger_from_serialized(val: serde_json::Value) -> EventTrigger { +pub fn trigger_from_serialized(val: serde_json::Value) -> UiEvent { let mut data: Vec = serde_json::from_value(val).unwrap(); let data = data.drain(..).next().unwrap(); let event = SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent))); let scope = ScopeId(data.scope as usize); let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize)); - EventTrigger { + UiEvent { event, scope, mounted_dom_id, diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 5a62bfe76..d4c282980 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc}; use dioxus_core::{ - events::{on::GenericEventInner, EventTrigger, SyntheticEvent}, + events::{on::GenericEventInner, SyntheticEvent, UiEvent}, mutations::NodeRefMutation, scheduler::SchedulerMsg, DomEdit, ElementId, ScopeId, @@ -519,7 +519,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent { /// This function decodes a websys event and produces an EventTrigger /// With the websys implementation, we attach a unique key to the nodes -fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { +fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { log::debug!("Handling event!"); let target = event @@ -561,7 +561,7 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { let triggered_scope = gi_id; // let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into(); log::debug!("Triggered scope is {:#?}", triggered_scope); - Ok(EventTrigger { + Ok(UiEvent { event: virtual_event_from_websys_event(event.clone()), mounted_dom_id: Some(ElementId(real_id as usize)), scope: ScopeId(triggered_scope as usize), From c321532a6cef40b2d2e4adc8c7a55931b6755b08 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 1 Sep 2021 00:57:04 -0400 Subject: [PATCH 45/53] feat: some docs, cleaning --- packages/core/.vscode/settings.json | 2 +- packages/core/examples/alternative.rs | 2 +- packages/core/examples/async.rs | 2 +- packages/core/examples/vdom_usage.rs | 4 +- packages/core/src/childiter.rs | 7 +- packages/core/src/context.rs | 19 +- packages/core/src/diff.rs | 46 ++++- packages/core/src/diffsupport.rs | 72 ------- packages/core/src/hooks.rs | 6 +- packages/core/src/lib.rs | 1 - packages/core/src/nodes.rs | 267 ++++++++++++++++++------- packages/core/src/scheduler.rs | 2 + packages/core/src/scope.rs | 14 +- packages/core/src/signals.rs | 5 - packages/core/src/virtual_dom.rs | 3 +- packages/core/tests/diff_iterative.rs | 2 +- packages/core/tests/set_state_batch.rs | 6 +- packages/core/tests/test_logging.rs | 2 +- packages/ssr/src/lib.rs | 2 +- 19 files changed, 269 insertions(+), 195 deletions(-) delete mode 100644 packages/core/src/diffsupport.rs delete mode 100644 packages/core/src/signals.rs diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/examples/alternative.rs b/packages/core/examples/alternative.rs index 177eca78f..95a8469ff 100644 --- a/packages/core/examples/alternative.rs +++ b/packages/core/examples/alternative.rs @@ -3,7 +3,7 @@ use dioxus_core::prelude::*; fn main() {} pub static Example: FC<()> = |cx| { - let list = (0..10).map(|f| LazyNodes::new(move |f| todo!())); + let list = (0..10).map(|_f| LazyNodes::new(move |_f| todo!())); cx.render(LazyNodes::new(move |cx| { cx.raw_element( diff --git a/packages/core/examples/async.rs b/packages/core/examples/async.rs index 9c6463e8a..73a83b456 100644 --- a/packages/core/examples/async.rs +++ b/packages/core/examples/async.rs @@ -21,7 +21,7 @@ const App: FC<()> = |cx| { }; const Task: FC<()> = |cx| { - let (task, res) = use_task(cx, || async { true }); + let (_task, _res) = use_task(cx, || async { true }); // task.pause(); // task.restart(); // task.stop(); diff --git a/packages/core/examples/vdom_usage.rs b/packages/core/examples/vdom_usage.rs index 4dde570d8..c5d643df5 100644 --- a/packages/core/examples/vdom_usage.rs +++ b/packages/core/examples/vdom_usage.rs @@ -10,6 +10,6 @@ async fn main() { dom.rebuild(); - let mut deadline = async_std::task::sleep(Duration::from_millis(50)); - let fut = dom.run_with_deadline(deadline); + let deadline = async_std::task::sleep(Duration::from_millis(50)); + let _fut = dom.run_with_deadline(deadline); } diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs index 5436f9355..daf5281c4 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/childiter.rs @@ -60,9 +60,10 @@ impl<'a> Iterator for RealChildIterator<'a> { // For components, we load their root and push them onto the stack VNode::Component(sc) => { - let scope = - unsafe { self.scopes.get_scope(sc.associated_scope.get().unwrap()) } - .unwrap(); + let scope = self + .scopes + .get_scope(sc.associated_scope.get().unwrap()) + .unwrap(); // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); // Simply swap the current node on the stack with the root of the component diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 903e53249..21d89436e 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -87,6 +87,10 @@ impl<'src, P> Context<'src, P> { /// }) /// } /// ``` + /// + /// ## Notes: + /// + /// This method returns a "ScopeChildren" object. This object is copy-able and preserve the correct lifetime. pub fn children(&self) -> ScopeChildren<'src> { self.scope.child_nodes() } @@ -103,19 +107,6 @@ impl<'src, P> Context<'src, P> { self.scope.shared.schedule_any_immediate.clone() } - pub fn schedule_effect(&self) -> Rc { - todo!() - } - - pub fn schedule_layout_effect(&self) { - todo!() - } - - /// Get's this component's unique identifier. - pub fn get_scope_id(&self) -> ScopeId { - self.scope.our_arena_idx.clone() - } - /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. /// /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned. @@ -175,7 +166,7 @@ impl<'src, P> Context<'src, P> { let getter = &self.scope.shared.get_shared_context; let ty = TypeId::of::(); let idx = self.scope.our_arena_idx; - getter(idx, ty).map(|f| f.downcast().unwrap()) + getter(idx, ty).map(|f| f.downcast().expect("TypeID already validated")) } /// Store a value between renders diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 59011301e..a6b7d6ace 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -200,7 +200,7 @@ impl<'bump> DiffMachine<'bump> { fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) { for el in RealChildIterator::new(node, self.vdom) { - self.mutations.push_root(el.direct_id()); + self.mutations.push_root(el.mounted_id()); self.stack.add_child_count(1); } } @@ -223,7 +223,7 @@ impl<'bump> DiffMachine<'bump> { let mut iter = RealChildIterator::new(old, self.vdom); let first = iter.next().unwrap(); self.mutations - .replace_with(first.direct_id(), nodes_created as u32); + .replace_with(first.mounted_id(), nodes_created as u32); self.remove_nodes(iter); } @@ -1091,4 +1091,46 @@ impl<'bump> DiffMachine<'bump> { let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; queue.push(long_listener as *const _) } + + /// Destroy a scope and all of its descendents. + /// + /// Calling this will run the destuctors on all hooks in the tree. + /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. + fn destroy_scopes(&mut self, old_scope: ScopeId) { + let mut nodes_to_delete = vec![old_scope]; + let mut scopes_to_explore = vec![old_scope]; + + // explore the scope tree breadth first + while let Some(scope_id) = scopes_to_explore.pop() { + // If we're planning on deleting this node, then we don't need to both rendering it + self.seen_scopes.insert(scope_id); + let scope = self.vdom.get_scope(scope_id).unwrap(); + for child in scope.descendents.borrow().iter() { + // Add this node to be explored + scopes_to_explore.push(child.clone()); + + // Also add it for deletion + nodes_to_delete.push(child.clone()); + } + } + + // Delete all scopes that we found as part of this subtree + for node in nodes_to_delete { + log::debug!("Removing scope {:#?}", node); + let _scope = self.vdom.try_remove(node).unwrap(); + // do anything we need to do to delete the scope + // I think we need to run the destructors on the hooks + // TODO + } + } +} + +fn compare_strs(a: &str, b: &str) -> bool { + // Check by pointer, optimizing for static strs + if !std::ptr::eq(a, b) { + // If the pointers are different then check by value + a == b + } else { + true + } } diff --git a/packages/core/src/diffsupport.rs b/packages/core/src/diffsupport.rs deleted file mode 100644 index 8b01182e2..000000000 --- a/packages/core/src/diffsupport.rs +++ /dev/null @@ -1,72 +0,0 @@ -/// Destroy a scope and all of its descendents. -/// -/// Calling this will run the destuctors on all hooks in the tree. -/// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. -fn destroy_scopes(&mut self, old_scope: ScopeId) { - let mut nodes_to_delete = vec![old_scope]; - let mut scopes_to_explore = vec![old_scope]; - - // explore the scope tree breadth first - while let Some(scope_id) = scopes_to_explore.pop() { - // If we're planning on deleting this node, then we don't need to both rendering it - self.seen_scopes.insert(scope_id); - let scope = self.get_scope(&scope_id).unwrap(); - for child in scope.descendents.borrow().iter() { - // Add this node to be explored - scopes_to_explore.push(child.clone()); - - // Also add it for deletion - nodes_to_delete.push(child.clone()); - } - } - - // Delete all scopes that we found as part of this subtree - for node in nodes_to_delete { - log::debug!("Removing scope {:#?}", node); - let _scope = self.vdom.try_remove(node).unwrap(); - // do anything we need to do to delete the scope - // I think we need to run the destructors on the hooks - // TODO - } -} - -pub(crate) fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'bump mut Scope> { - // ensure we haven't seen this scope before - // if we have, then we're trying to alias it, which is not allowed - debug_assert!(!self.seen_scopes.contains(id)); - - unsafe { self.vdom.get_scope_mut(*id) } -} -pub(crate) fn get_scope(&mut self, id: &ScopeId) -> Option<&'bump Scope> { - // ensure we haven't seen this scope before - // if we have, then we're trying to alias it, which is not allowed - unsafe { self.vdom.get_scope(*id) } -} - -fn compare_strs(a: &str, b: &str) -> bool { - // Check by pointer, optimizing for static strs - if !std::ptr::eq(a, b) { - // If the pointers are different then check by value - a == b - } else { - true - } -} - -fn find_first_real_node<'a>( - nodes: impl IntoIterator>, - scopes: &'a SharedResources, -) -> Option<&'a VNode<'a>> { - for node in nodes { - let mut iter = RealChildIterator::new(node, scopes); - if let Some(node) = iter.next() { - return Some(node); - } - } - - None -} - -fn remove_children(&mut self, old: &'bump [VNode<'bump>]) { - self.replace_and_create_many_with_many(old, None) -} diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 86f7b4b4f..5bddaca12 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -119,13 +119,11 @@ where let slot = task_dump.clone(); let updater = cx.prepare_update(); - let update_id = cx.get_scope_id(); - - let originator = cx.scope.our_arena_idx.clone(); + let originator = cx.scope.our_arena_idx; let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move { *slot.as_ref().borrow_mut() = Some(output); - updater(update_id); + updater(originator); originator }))); diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index aa604f0ab..077a282e3 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -26,7 +26,6 @@ pub mod mutations; pub mod nodes; pub mod scheduler; pub mod scope; -pub mod signals; pub mod util; pub mod virtual_dom; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a66e96727..77d4112a1 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,12 +1,11 @@ //! Virtual Node Support -//! -------------------- -//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers. //! -//! These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick. -use crate::{ - events::SyntheticEvent, - innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC}, - SuspendedContext, +//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers. These VNodes should be *very* +//! cheap and *very* fast to construct - building a full tree should be quick. + +use crate::innerlude::{ + empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, SuspendedContext, + SyntheticEvent, FC, }; use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ @@ -16,26 +15,113 @@ use std::{ rc::Rc, }; -/// Tools for the base unit of the virtual dom - the VNode -/// VNodes are intended to be quickly-allocated, lightweight enum values. +/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM. /// -/// Components will be generating a lot of these very quickly, so we want to -/// limit the amount of heap allocations / overly large enum sizes. +/// VNodes are designed to be lightweight and used with with a bump alloactor. To create a VNode, you can use either of: +/// - the [`rsx`] macro +/// - the [`html`] macro +/// - the [`NodeFactory`] API pub enum VNode<'src> { + /// Text VNodes simply bump-allocated (or static) string slices + /// + /// # Example + /// + /// let node = cx.render(rsx!{ "hello" }).unwrap(); + /// + /// if let VNode::Text(vtext) = node { + /// assert_eq!(vtext.text, "hello"); + /// assert_eq!(vtext.dom_id.get(), None); + /// assert_eq!(vtext.is_static, true); + /// } + /// ``` Text(VText<'src>), + /// Element VNodes are VNodes that may contain attributes, listeners, a key, a tag, and children. + /// + /// # Example + /// + /// ```rust + /// let node = cx.render(rsx!{ + /// div { + /// key: "a", + /// onclick: |e| log::info!("clicked"), + /// hidden: "true", + /// style: { background_color: "red" } + /// "hello" + /// } + /// }).unwrap(); + /// if let VNode::Element(velement) = node { + /// assert_eq!(velement.tag_name, "div"); + /// assert_eq!(velement.namespace, None); + /// assert_eq!(velement.key, Some("a)); + /// } + /// ``` Element(&'src VElement<'src>), + /// Fragment nodes may contain many VNodes without a single root. + /// + /// # Example + /// + /// ```rust + /// rsx!{ + /// a {} + /// link {} + /// style {} + /// "asd" + /// Example {} + /// } + /// ``` Fragment(VFragment<'src>), + /// Component nodes represent a mounted component with props, children, and a key. + /// + /// # Example + /// + /// ```rust + /// fn Example(cx: Context<()>) -> DomTree { + /// todo!() + /// } + /// + /// let node = cx.render(rsx!{ + /// Example {} + /// }).unwrap(); + /// + /// if let VNode::Component(vcomp) = node { + /// assert_eq!(vcomp.user_fc, Example as *const ()); + /// } + /// ``` Component(&'src VComponent<'src>), + /// Suspended VNodes represent chunks of the UI tree that are not yet ready to be displayed. + /// + /// These nodes currently can only be constructed via the [`use_suspense`] hook. + /// + /// # Example + /// + /// ```rust + /// rsx!{ + /// } + /// ``` Suspended(&'src VSuspended<'src>), + /// Anchors are a type of placeholder VNode used when fragments don't contain any children. + /// + /// Anchors cannot be directly constructed via public APIs. + /// + /// # Example + /// + /// ```rust + /// let node = cx.render(rsx! ( Fragment {} )).unwrap(); + /// if let VNode::Fragment(frag) = node { + /// let root = &frag.children[0]; + /// assert_eq!(root, VNode::Anchor); + /// } + /// ``` Anchor(VAnchor), } impl<'src> VNode<'src> { + /// Get the VNode's "key" used in the keyed diffing algorithm. pub fn key(&self) -> Option<&'src str> { match &self { VNode::Element(el) => el.key, @@ -46,25 +132,35 @@ impl<'src> VNode<'src> { VNode::Anchor(_f) => None, } } - pub fn direct_id(&self) -> ElementId { - self.try_direct_id().unwrap() + + /// Get the ElementID of the mounted VNode. + /// + /// Panics if the mounted ID is None or if the VNode is not represented by a single Element. + pub fn mounted_id(&self) -> ElementId { + self.try_mounted_id().unwrap() } - pub fn try_direct_id(&self) -> Option { + + /// Try to get the ElementID of the mounted VNode. + /// + /// Returns None if the VNode is not mounted, or if the VNode cannot be presented by a mounted ID (Fragment/Component) + pub fn try_mounted_id(&self) -> Option { match &self { VNode::Text(el) => el.dom_id.get(), VNode::Element(el) => el.dom_id.get(), VNode::Anchor(el) => el.dom_id.get(), + VNode::Suspended(el) => el.dom_id.get(), VNode::Fragment(_) => None, VNode::Component(_) => None, - VNode::Suspended(_) => None, } } } +/// A placeholder node only generated when Fragments don't have any children. pub struct VAnchor { pub dom_id: Cell>, } +/// A bump-alloacted string slice and metadata. pub struct VText<'src> { pub text: &'src str, @@ -73,6 +169,7 @@ pub struct VText<'src> { pub is_static: bool, } +/// A list of VNodes with no single root. pub struct VFragment<'src> { pub key: Option<&'src str>, @@ -81,6 +178,7 @@ pub struct VFragment<'src> { pub is_static: bool, } +/// An element like a "div" with children, listeners, and attributes. pub struct VElement<'a> { pub tag_name: &'static str, @@ -97,6 +195,22 @@ pub struct VElement<'a> { pub children: &'a [VNode<'a>], } +/// A trait for any generic Dioxus Element. +/// +/// This trait provides the ability to use custom elements in the `rsx!` macro. +/// +/// ```rust +/// struct my_element; +/// +/// impl DioxusElement for my_element { +/// const TAG_NAME: "my_element"; +/// const NAME_SPACE: None; +/// } +/// +/// let _ = rsx!{ +/// my_element {} +/// }; +/// ``` pub trait DioxusElement { const TAG_NAME: &'static str; const NAME_SPACE: Option<&'static str>; @@ -122,18 +236,21 @@ pub struct Attribute<'a> { pub is_volatile: bool, - // Doesn't exist in the html spec, mostly used to denote "style" tags - could be for any type of group + // Doesn't exist in the html spec. + // Used in Dioxus to denote "style" tags. pub namespace: Option<&'static str>, } /// An event listener. /// IE onclick, onkeydown, etc pub struct Listener<'bump> { - /// The type of event to listen for. - pub(crate) event: &'static str, - pub mounted_node: Cell>, + /// The type of event to listen for. + /// + /// IE "onclick" - whatever the renderer needs to attach the listener by name. + pub event: &'static str, + pub(crate) callback: RefCell>>, } @@ -144,6 +261,11 @@ pub struct VComponent<'src> { pub associated_scope: Cell>, + pub is_static: bool, + + // Function pointer to the FC that was used to generate this component + pub user_fc: *const (), + pub(crate) caller: Rc DomTree>, pub(crate) children: &'src [VNode<'src>], @@ -152,15 +274,10 @@ pub struct VComponent<'src> { pub(crate) drop_props: RefCell>>, - pub is_static: bool, - pub(crate) can_memoize: bool, // Raw pointer into the bump arena for the props of the component pub(crate) raw_props: *const (), - - // Function pointer to the FC that was used to generate this component - pub(crate) user_fc: *const (), } pub struct VSuspended<'a> { @@ -203,7 +320,7 @@ impl<'a> NodeFactory<'a> { }) } - /// Used in a place or two to make it easier to build vnodes from dummy text + /// 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(VText { dom_id: empty_cell(), @@ -220,9 +337,9 @@ impl<'a> NodeFactory<'a> { Some(static_str) => (static_str, true), None => { use bumpalo::core_alloc::fmt::Write; - let mut s = bumpalo::collections::String::new_in(self.bump()); - s.write_fmt(args).unwrap(); - (s.into_bump_str(), false) + let mut str_buf = bumpalo::collections::String::new_in(self.bump()); + str_buf.write_fmt(args).unwrap(); + (str_buf.into_bump_str(), false) } } } @@ -264,7 +381,7 @@ impl<'a> NodeFactory<'a> { pub fn raw_element( &self, - tag: &'static str, + tag_name: &'static str, namespace: Option<&'static str>, listeners: L, attributes: A, @@ -288,7 +405,7 @@ impl<'a> NodeFactory<'a> { let key = key.map(|f| self.raw_text(f).0); VNode::Element(self.bump().alloc(VElement { - tag_name: tag, + tag_name, key, namespace, listeners, @@ -315,22 +432,6 @@ impl<'a> NodeFactory<'a> { } } - pub fn attr_with_alloc_val( - &self, - name: &'static str, - val: &'a str, - namespace: Option<&'static str>, - is_volatile: bool, - ) -> Attribute<'a> { - Attribute { - name, - value: val, - is_static: false, - namespace, - is_volatile, - } - } - pub fn component( &self, component: FC

, @@ -343,14 +444,9 @@ impl<'a> NodeFactory<'a> { V: 'a + AsRef<[VNode<'a>]>, { let bump = self.bump(); - - // We don't want the fat part of the fat pointer - // THe "Fat" part is provided on next-render let children: &'a V = bump.alloc(children); let children = children.as_ref(); - let props = bump.alloc(props); - let raw_props = props as *mut P as *mut (); let user_fc = component as *const (); @@ -379,36 +475,46 @@ impl<'a> NodeFactory<'a> { } })); - // create a closure to drop the props - let mut has_dropped = false; - let drop_props: &mut dyn FnMut() = bump.alloc_with(|| { - move || unsafe { - if !has_dropped { - let real_other = raw_props as *mut _ as *mut P; - let b = BumpBox::from_raw(real_other); - std::mem::drop(b); + let drop_props = { + // create a closure to drop the props + let mut has_dropped = false; - has_dropped = true; + let drop_props: &mut dyn FnMut() = bump.alloc_with(|| { + move || unsafe { + if !has_dropped { + let real_other = raw_props as *mut _ as *mut P; + let b = BumpBox::from_raw(real_other); + std::mem::drop(b); + + has_dropped = true; + } } - } - }); - let drop_props = unsafe { BumpBox::from_raw(drop_props) }; + }); + + let drop_props = unsafe { BumpBox::from_raw(drop_props) }; + + RefCell::new(Some(drop_props)) + }; let is_static = children.len() == 0 && P::IS_STATIC && key.is_none(); let key = key.map(|f| self.raw_text(f).0); + let caller = NodeFactory::create_component_caller(component, raw_props); + + let can_memoize = children.len() == 0 && P::IS_STATIC; + VNode::Component(bump.alloc_with(|| VComponent { user_fc, comparator, raw_props, children, - caller: NodeFactory::create_component_caller(component, raw_props), + caller, is_static, - drop_props: RefCell::new(Some(drop_props)), - can_memoize: P::IS_STATIC, - associated_scope: Cell::new(None), key, + can_memoize, + drop_props, + associated_scope: Cell::new(None), })) } @@ -509,21 +615,41 @@ where } } +/// Child nodes of the parent component. +/// +/// # Example +/// +/// ```rust +/// let children = cx.children(); +/// let first_node = &children[0]; +/// rsx!{ +/// h1 { {first_node} } +/// p { {&children[1..]} } +/// } +/// ``` +/// pub struct ScopeChildren<'a>(pub &'a [VNode<'a>]); + impl Copy for ScopeChildren<'_> {} + impl<'a> Clone for ScopeChildren<'a> { fn clone(&self) -> Self { ScopeChildren(self.0) } } + impl ScopeChildren<'_> { - pub unsafe fn extend_lifetime(self) -> ScopeChildren<'static> { + // dangerous method - used to fix the associated lifetime + pub(crate) unsafe fn extend_lifetime(self) -> ScopeChildren<'static> { std::mem::transmute(self) } - pub unsafe fn unextend_lfetime<'a>(self) -> ScopeChildren<'a> { + + // dangerous method - used to fix the associated lifetime + pub(crate) unsafe fn shorten_lifetime<'a>(self) -> ScopeChildren<'a> { std::mem::transmute(self) } } + impl<'a> IntoVNodeList<'a> for ScopeChildren<'a> { fn into_vnode_list(self, _: NodeFactory<'a>) -> &'a [VNode<'a>] { self.0 @@ -610,6 +736,7 @@ impl IntoVNode<'_> for Option<()> { cx.fragment_from_iter(None as Option) } } + impl<'a> IntoVNode<'a> for Option> { fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { match self { @@ -647,7 +774,7 @@ impl Debug for VNode<'_> { .finish() } VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text), - VNode::Anchor(a) => write!(s, "VAnchor"), + VNode::Anchor(_) => write!(s, "VAnchor"), VNode::Fragment(frag) => write!(s, "VFragment {{ children: {:?} }}", frag.children), VNode::Suspended { .. } => write!(s, "VSuspended"), diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index a1a778a84..8e260ee8e 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -703,6 +703,8 @@ impl ResourcePool { id } + pub fn borrow_bumpframe(&self) {} + pub fn clean_up_garbage(&mut self) { // let mut scopes_to_kill = Vec::new(); // let mut garbage_list = Vec::new(); diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 5dc02e790..d854690a2 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,12 +1,9 @@ use crate::innerlude::*; -use bumpalo::boxed::Box as BumpBox; -use futures_channel::mpsc::UnboundedSender; use fxhash::FxHashSet; use std::{ any::{Any, TypeId}, - borrow::BorrowMut, - cell::{Cell, RefCell}, - collections::{HashMap, HashSet}, + cell::RefCell, + collections::HashMap, future::Future, pin::Pin, rc::Rc, @@ -68,15 +65,10 @@ impl Scope { // Therefore, their lifetimes are connected exclusively to the virtual dom pub fn new<'creator_node>( caller: Rc, - arena_idx: ScopeId, - parent: Option, - height: u32, - child_nodes: ScopeChildren, - shared: EventChannel, ) -> Self { let child_nodes = unsafe { child_nodes.extend_lifetime() }; @@ -225,7 +217,7 @@ impl Scope { } pub fn child_nodes<'a>(&'a self) -> ScopeChildren { - unsafe { self.child_nodes.unextend_lfetime() } + unsafe { self.child_nodes.shorten_lifetime() } } pub fn consume_garbage(&self) -> Vec<&VNode> { diff --git a/packages/core/src/signals.rs b/packages/core/src/signals.rs deleted file mode 100644 index bbce9a861..000000000 --- a/packages/core/src/signals.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Signas: Avoid the Diff Engine -//! ----------------------------- -//! -//! -//! TODO! diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index f8d3ebb61..3b30f37e4 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -14,13 +14,12 @@ //! - The [`ActiveFrame`] object for managing the Scope`s microheap //! - The [`Context`] object for exposing VirtualDOM API to components //! - The [`NodeFactory`] object for lazyily exposing the `Context` API to the nodebuilder API -//! - The [`Hook`] object for exposing state management in components. //! //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. use crate::innerlude::*; -use futures_util::{pin_mut, Future, FutureExt}; +use futures_util::{Future, FutureExt}; use std::{ any::{Any, TypeId}, pin::Pin, diff --git a/packages/core/tests/diff_iterative.rs b/packages/core/tests/diff_iterative.rs index e4d247031..530c9af59 100644 --- a/packages/core/tests/diff_iterative.rs +++ b/packages/core/tests/diff_iterative.rs @@ -31,7 +31,7 @@ async fn test_iterative_create_components() { } } } - {(0..0).map(|f| rsx!{ div { "walalla"}})} + {(0..0).map(|_f| rsx!{ div { "walalla"}})} p {} }) } diff --git a/packages/core/tests/set_state_batch.rs b/packages/core/tests/set_state_batch.rs index 98d808a96..fc897102e 100644 --- a/packages/core/tests/set_state_batch.rs +++ b/packages/core/tests/set_state_batch.rs @@ -16,12 +16,12 @@ about 10ms, which is way more than enough for diffing/creating to happen. async fn batch() { let (sender, mut recver) = futures_channel::mpsc::unbounded::(); - let handle = async_std::task::spawn(async move { - let msg = recver.next().await; + let _handle = async_std::task::spawn(async move { + let _msg = recver.next().await; while let Ok(msg) = recver.try_next() { println!("{:#?}", msg); } - let msg = recver.next().await; + let _msg = recver.next().await; while let Ok(msg) = recver.try_next() { println!("{:#?}", msg); } diff --git a/packages/core/tests/test_logging.rs b/packages/core/tests/test_logging.rs index aafb831e2..8f696be89 100644 --- a/packages/core/tests/test_logging.rs +++ b/packages/core/tests/test_logging.rs @@ -1,6 +1,6 @@ pub fn set_up_logging(enabled: bool) { use fern::colors::{Color, ColoredLevelConfig}; - use log::debug; + if !enabled { return; diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index e8cc94689..a03520b27 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -129,7 +129,7 @@ impl<'a> TextRenderer<'a> { // // when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add // them interpreter's stack - match (self.cfg.pre_render, node.try_direct_id()) { + match (self.cfg.pre_render, node.try_mounted_id()) { (true, Some(id)) => { write!(f, " dio_el=\"{}\"", id)?; // From fac2e56ed63fc708a89e14df15f1fc32f58391a9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 1 Sep 2021 15:45:53 -0400 Subject: [PATCH 46/53] feat: update root props --- packages/core/.vscode/settings.json | 2 +- packages/core/src/bumpframe.rs | 6 ----- packages/core/src/events.rs | 40 ++++++++++++++--------------- packages/core/src/hooklist.rs | 4 ++- packages/core/src/hooks.rs | 31 +++++++++++----------- packages/core/src/lib.rs | 2 +- packages/core/src/nodes.rs | 11 +++----- packages/core/src/scope.rs | 25 +++++++++++++++--- packages/core/src/virtual_dom.rs | 30 ++++++++++++++++++++++ 9 files changed, 96 insertions(+), 55 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 79505b018..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index 8696d4e8e..b2b54107e 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -67,12 +67,6 @@ impl ActiveFrame { } } - pub fn finished_frame_mut(&mut self) -> &mut BumpFrame { - match self.generation.get() & 1 == 1 { - true => &mut self.frames[0], - false => &mut self.frames[1], - } - } /// Give out our self-referential item with our own borrowed lifetime pub fn fin_head<'b>(&'b self) -> &'b VNode<'b> { let cur_head = &self.finished_frame().head_node; diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 7360d87e4..952c3b2e3 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -633,16 +633,16 @@ pub mod on { DownArrow = 40, Insert = 45, Delete = 46, - _0 = 48, - _1 = 49, - _2 = 50, - _3 = 51, - _4 = 52, - _5 = 53, - _6 = 54, - _7 = 55, - _8 = 56, - _9 = 57, + Num0 = 48, + Num1 = 49, + Num2 = 50, + Num3 = 51, + Num4 = 52, + Num5 = 53, + Num6 = 54, + Num7 = 55, + Num8 = 56, + Num9 = 57, A = 65, B = 66, C = 67, @@ -738,16 +738,16 @@ pub mod on { 40 => DownArrow, 45 => Insert, 46 => Delete, - 48 => _0, - 49 => _1, - 50 => _2, - 51 => _3, - 52 => _4, - 53 => _5, - 54 => _6, - 55 => _7, - 56 => _8, - 57 => _9, + 48 => Num0, + 49 => Num1, + 50 => Num2, + 51 => Num3, + 52 => Num4, + 53 => Num5, + 54 => Num6, + 55 => Num7, + 56 => Num8, + 57 => Num9, 65 => A, 66 => B, 67 => C, diff --git a/packages/core/src/hooklist.rs b/packages/core/src/hooklist.rs index 3781fc2ed..ebfe5723a 100644 --- a/packages/core/src/hooklist.rs +++ b/packages/core/src/hooklist.rs @@ -3,7 +3,7 @@ use std::{ cell::{Cell, RefCell, UnsafeCell}, }; -pub struct HookList { +pub(crate) struct HookList { vals: RefCell>>>, idx: Cell, } @@ -20,6 +20,7 @@ impl Default for HookList { struct InnerHook { cell: UnsafeCell, } + impl InnerHook { fn new(new: T) -> Self { Self { @@ -27,6 +28,7 @@ impl InnerHook { } } } + impl HookList { pub(crate) fn next(&self) -> Option<&mut T> { self.vals.borrow().get(self.idx.get()).and_then(|inn| { diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 5bddaca12..47e7fcda9 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -3,11 +3,11 @@ //! This module contains all the low-level built-in hooks that require 1st party support to work. //! //! Hooks: -//! - use_hook -//! - use_state_provider -//! - use_state_consumer -//! - use_task -//! - use_suspense +//! - [`use_hook`] +//! - [`use_state_provider`] +//! - [`use_state_consumer`] +//! - [`use_task`] +//! - [`use_suspense`] use crate::innerlude::*; use futures_util::FutureExt; @@ -53,15 +53,7 @@ where (true, true) => {} // Needs to be initialized - (false, false) => { - log::debug!("Initializing context..."); - cx.add_shared_state(init()); - log::info!( - "There are now {} shared contexts for scope {:?}", - cx.scope.shared_contexts.borrow().len(), - cx.scope.our_arena_idx, - ); - } + (false, false) => cx.add_shared_state(init()), _ => debug_assert!(false, "Cannot initialize two contexts of the same type"), }; @@ -205,7 +197,10 @@ where cx.render(LazyNodes::new(|f| { let bump = f.bump(); - let g: &dyn FnOnce(SuspendedContext<'src>) -> DomTree<'src> = + + use bumpalo::boxed::Box as BumpBox; + + let f: &mut dyn FnMut(SuspendedContext<'src>) -> DomTree<'src> = bump.alloc(move |sus| { let val = value.borrow(); @@ -218,11 +213,12 @@ where user_callback(sus, out) }); + let callback = unsafe { BumpBox::from_raw(f) }; VNode::Suspended(bump.alloc(VSuspended { dom_id: empty_cell(), task_id: hook.handle.our_id, - callback: RefCell::new(Some(g)), + callback: RefCell::new(Some(callback)), })) })) } @@ -235,10 +231,13 @@ pub(crate) struct SuspenseHook { pub handle: TaskHandle, pub value: Rc>>>, } + type SuspendedCallback = Box Fn(SuspendedContext<'a>) -> DomTree<'a>>; + pub struct SuspendedContext<'a> { pub(crate) inner: Context<'a, ()>, } + impl<'src> SuspendedContext<'src> { pub fn render) -> VNode<'src>>( self, diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 077a282e3..f01838e23 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -38,7 +38,7 @@ pub(crate) mod innerlude { pub use crate::diff_stack::*; pub use crate::events::*; pub use crate::heuristics::*; - pub use crate::hooklist::*; + pub(crate) use crate::hooklist::*; pub use crate::hooks::*; pub use crate::mutations::*; pub use crate::nodes::*; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 77d4112a1..58f259c72 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -283,7 +283,8 @@ pub struct VComponent<'src> { pub struct VSuspended<'a> { pub task_id: u64, pub dom_id: Cell>, - pub(crate) callback: RefCell) -> DomTree<'a>>>, + pub(crate) callback: + RefCell) -> DomTree<'a>>>>, } /// This struct provides an ergonomic API to quickly build VNodes. @@ -518,7 +519,7 @@ impl<'a> NodeFactory<'a> { })) } - pub fn create_component_caller<'g, P: 'g>( + pub(crate) fn create_component_caller<'g, P: 'g>( component: FC

, raw_props: *const (), ) -> Rc Fn(&'r Scope) -> DomTree<'r>> { @@ -530,12 +531,8 @@ impl<'a> NodeFactory<'a> { props: safe_props, scope: scp, }; - let res = component(cx); - - let g2 = unsafe { std::mem::transmute(res) }; - - g2 + unsafe { std::mem::transmute(res) } }); unsafe { std::mem::transmute::<_, Captured<'static>>(caller) } } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index d854690a2..9b154f097 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -36,8 +36,7 @@ pub struct Scope { // Listeners pub(crate) listeners: RefCell>>, pub(crate) borrowed_props: RefCell>>, - - pub(crate) suspended_nodes: RefCell>>, + pub(crate) suspended_nodes: RefCell>>, // State pub(crate) hooks: HookList, @@ -146,7 +145,7 @@ impl Scope { /// /// Refrences to hook data can only be stored in listeners and component props. During diffing, we make sure to log /// all listeners and borrowed props so we can clear them here. - fn ensure_drop_safety(&mut self, pool: &ResourcePool) { + pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) { // make sure all garabge is collected before trying to proceed with anything else debug_assert!( self.pending_garbage.borrow().is_empty(), @@ -220,6 +219,26 @@ impl Scope { unsafe { self.child_nodes.shorten_lifetime() } } + pub fn call_suspended_node<'a>(&'a self, task: u64) { + let g = self.suspended_nodes.borrow_mut(); + + if let Some(suspended) = g.get(&task) { + let sus: &'a VSuspended<'static> = unsafe { &**suspended }; + let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) }; + + let bump = self.frames.wip_frame(); + let mut cb = sus.callback.borrow_mut(); + let mut _cb = cb.take().unwrap(); + let cx: SuspendedContext<'a> = SuspendedContext { + inner: Context { + props: &(), + scope: &self, + }, + }; + let n: DomTree<'a> = (_cb)(cx); + } + } + pub fn consume_garbage(&self) -> Vec<&VNode> { self.pending_garbage .borrow_mut() diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 3b30f37e4..7b50f2fe5 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -23,6 +23,7 @@ use futures_util::{Future, FutureExt}; use std::{ any::{Any, TypeId}, pin::Pin, + rc::Rc, }; /// An integrated virtual node system that progresses events and diffs UI trees. @@ -39,6 +40,8 @@ pub struct VirtualDom { base_scope: ScopeId, + root_fc: Box, + root_prop_type: std::any::TypeId, root_props: Pin>, @@ -120,6 +123,7 @@ impl VirtualDom { }); Self { + root_fc: Box::new(root), base_scope, scheduler, root_props: _root_props, @@ -135,6 +139,32 @@ impl VirtualDom { self.scheduler.pool.get_scope(id) } + /// Update the root props of this VirtualDOM. + /// + /// This method retuns None if the old props could not be removed. The entire VirtualDOM will be rebuilt immediately, + /// so calling this method will block the main thread until computation is done. + pub fn update_root_props<'s, P: 'static>(&'s mut self, root_props: P) -> Option> { + let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap(); + root_scope.ensure_drop_safety(&self.scheduler.pool); + + let mut _root_props: Pin> = Box::pin(root_props); + let _root_prop_type = TypeId::of::

(); + + if let Some(props_ptr) = _root_props.downcast_ref::

().map(|p| p as *const P) { + std::mem::swap(&mut self.root_props, &mut _root_props); + + let root = *self.root_fc.downcast_ref::>().unwrap(); + + let new_caller = NodeFactory::create_component_caller(root, props_ptr as *const _); + + root_scope.update_scope_dependencies(new_caller, ScopeChildren(&[])); + + Some(self.rebuild()) + } else { + None + } + } + /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch /// /// The diff machine expects the RealDom's stack to be the root of the application From 5c3dc0a8741df251dd3cb7f954fae2243df155f7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 1 Sep 2021 15:52:38 -0400 Subject: [PATCH 47/53] wip: more work on rebuild --- packages/core/.vscode/settings.json | 2 +- packages/core/src/virtual_dom.rs | 37 +++++++++++++++++++---------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 7b50f2fe5..6ce6d5d05 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -42,8 +42,6 @@ pub struct VirtualDom { root_fc: Box, - root_prop_type: std::any::TypeId, - root_props: Pin>, } @@ -105,10 +103,8 @@ impl VirtualDom { pub fn new_with_props(root: FC

, root_props: P) -> Self { let scheduler = Scheduler::new(); - let _root_props: Pin> = Box::pin(root_props); - let _root_prop_type = TypeId::of::

(); - - let props_ptr = _root_props.downcast_ref::

().unwrap() as *const P; + let root_props: Pin> = Box::pin(root_props); + let props_ptr = root_props.downcast_ref::

().unwrap() as *const P; let base_scope = scheduler.pool.insert_scope_with_key(|myidx| { let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); @@ -126,15 +122,19 @@ impl VirtualDom { root_fc: Box::new(root), base_scope, scheduler, - root_props: _root_props, - root_prop_type: _root_prop_type, + root_props, } } + /// Get the [`Scope`] for the root component. + /// + /// This is useful for traversing the tree from the root for heuristics or altnerative renderers that use Dioxus + /// directly. pub fn base_scope(&self) -> &Scope { self.scheduler.pool.get_scope(self.base_scope).unwrap() } + /// Get the [`Scope`] for a component given its [`ScopeId`] pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> { self.scheduler.pool.get_scope(id) } @@ -143,15 +143,28 @@ impl VirtualDom { /// /// This method retuns None if the old props could not be removed. The entire VirtualDOM will be rebuilt immediately, /// so calling this method will block the main thread until computation is done. + /// + /// ## Example + /// + /// ```rust + /// #[derive(Props, PartialEq)] + /// struct AppProps { + /// route: &'static str + /// } + /// static App: FC = |cx| cx.render(rsx!{ "route is {cx.route}" }); + /// + /// let mut dom = VirtualDom::new_with_props(App, AppProps { route: "start" }); + /// + /// let mutations = dom.update_root_props(AppProps { route: "end" }).unwrap(); + /// ``` pub fn update_root_props<'s, P: 'static>(&'s mut self, root_props: P) -> Option> { let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap(); root_scope.ensure_drop_safety(&self.scheduler.pool); - let mut _root_props: Pin> = Box::pin(root_props); - let _root_prop_type = TypeId::of::

(); + let mut root_props: Pin> = Box::pin(root_props); - if let Some(props_ptr) = _root_props.downcast_ref::

().map(|p| p as *const P) { - std::mem::swap(&mut self.root_props, &mut _root_props); + if let Some(props_ptr) = root_props.downcast_ref::

().map(|p| p as *const P) { + std::mem::swap(&mut self.root_props, &mut root_props); let root = *self.root_fc.downcast_ref::>().unwrap(); From 1cd5e697128877e911ece2c9b34ec38d74eeec80 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 1 Sep 2021 23:22:34 -0400 Subject: [PATCH 48/53] wip: more work on scheduler --- examples/async.rs | 2 +- examples/reference/task.rs | 2 +- packages/core/.vscode/settings.json | 2 +- packages/core/src/diff.rs | 23 ++-- packages/core/src/scheduler.rs | 177 +++++++++++++++++----------- packages/core/src/virtual_dom.rs | 43 +++---- 6 files changed, 140 insertions(+), 109 deletions(-) diff --git a/examples/async.rs b/examples/async.rs index 8e6766bd1..111102921 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -28,7 +28,7 @@ pub static App: FC<()> = |cx| { } button { "Start counting" - onclick: move |_| task.start() + onclick: move |_| task.resume() } button { "Switch counting direcion" diff --git a/examples/reference/task.rs b/examples/reference/task.rs index 7e28786ec..8f1e61e0a 100644 --- a/examples/reference/task.rs +++ b/examples/reference/task.rs @@ -47,7 +47,7 @@ pub static Example: FC<()> = |cx| { } button { "Start counting" - onclick: move |_| task.start() + onclick: move |_| task.resume() } button { "Switch counting direcion" diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 79505b018..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index a6b7d6ace..e9c4e46cb 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -88,9 +88,8 @@ //! More info on how to improve this diffing algorithm: //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ -use crate::{innerlude::*, scheduler::Scheduler}; +use crate::innerlude::*; use fxhash::{FxHashMap, FxHashSet}; -use slab::Slab; use DomEdit::*; /// Our DiffMachine is an iterative tree differ. @@ -154,14 +153,7 @@ impl<'bump> DiffMachine<'bump> { } } - // pub fn new_headless(shared: &'bump SharedResources) -> Self { - // let edits = Mutations::new(); - // let cur_scope = ScopeId(0); - // Self::new(edits, cur_scope, shared) - // } - - // - pub async fn diff_scope(&'bump mut self, id: ScopeId) { + pub fn diff_scope(&'bump mut self, id: ScopeId) { if let Some(component) = self.vdom.get_scope_mut(id) { let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); self.stack.push(DiffInstruction::DiffNode { new, old }); @@ -173,7 +165,9 @@ 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). - pub async fn work(&mut self) { + /// + /// Returns a `bool` indicating that the work completed properly. + pub fn work(&mut self, deadline_expired: &mut impl FnMut() -> bool) -> bool { while let Some(instruction) = self.stack.pop() { // defer to individual functions so the compiler produces better code // large functions tend to be difficult for the compiler to work with @@ -193,9 +187,12 @@ impl<'bump> DiffMachine<'bump> { DiffInstruction::PrepareMoveNode { node } => self.prepare_move_node(node), }; - // todo: call this less frequently, there is a bit of overhead involved - yield_now().await; + if deadline_expired() { + return false; + } } + + true } fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) { diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 8e260ee8e..dfa90bb56 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -18,7 +18,7 @@ point if being "fast" if you can't also be "responsive." As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms). -The controller responsible for this priority management is called the "scheduler" and is responsible for juggle many +The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many different types of work simultaneously. # How does it work? @@ -155,7 +155,7 @@ impl Scheduler { pub fn new() -> Self { /* Preallocate 2000 elements and 100 scopes to avoid dynamic allocation. - Perhaps this should be configurable? + Perhaps this should be configurable from some external config? */ let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100))); let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(2000))); @@ -283,21 +283,14 @@ impl Scheduler { // nothing to do, no events on channels, no work pub fn has_any_work(&self) -> bool { - self.has_work() || self.has_pending_events() || self.has_pending_garbage() + let pending_lanes = self.lanes.iter().find(|f| f.has_work()).is_some(); + pending_lanes || self.has_pending_events() } pub fn has_pending_events(&self) -> bool { self.ui_events.len() > 0 } - pub fn has_work(&self) -> bool { - self.lanes.iter().find(|f| f.has_work()).is_some() - } - - pub fn has_pending_garbage(&self) -> bool { - !self.garbage_scopes.is_empty() - } - fn shift_priorities(&mut self) { self.current_priority = match ( self.lanes[0].has_work(), @@ -312,6 +305,9 @@ impl Scheduler { }; } + /// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work + fn balance_lanes(&mut self) {} + fn load_current_lane(&mut self) -> &mut PriorityLane { match self.current_priority { EventPriority::Immediate => todo!(), @@ -335,6 +331,29 @@ impl Scheduler { } } + /// Work the scheduler down, not polling any ongoing tasks. + /// + /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor. + pub fn work_sync<'a>(&'a mut self) -> Vec> { + let mut committed_mutations = Vec::>::new(); + + // Internalize any pending work since the last time we ran + self.manually_poll_events(); + + if !self.has_any_work() { + self.pool.clean_up_garbage(); + } + + while self.has_any_work() { + // do work + + // Create work from the pending event queue + self.consume_pending_events(); + } + + committed_mutations + } + /// The primary workhorse of the VirtualDOM. /// /// Uses some fairly complex logic to schedule what work should be produced. @@ -342,7 +361,7 @@ impl Scheduler { /// Returns a list of successful mutations. pub async fn work_with_deadline<'a>( &'a mut self, - mut deadline: Pin>>, + mut deadline_reached: Pin>>, ) -> Vec> { /* Strategy: @@ -374,7 +393,7 @@ impl Scheduler { // Wait for any new events if we have nothing to do if !self.has_any_work() { self.pool.clean_up_garbage(); - let deadline_expired = self.wait_for_any_trigger(&mut deadline).await; + let deadline_expired = self.wait_for_any_trigger(&mut deadline_reached).await; if deadline_expired { return committed_mutations; @@ -384,58 +403,15 @@ impl Scheduler { // Create work from the pending event queue self.consume_pending_events(); - // Work through the current subtree, and commit the results when it finishes - // When the deadline expires, give back the work + // shift to the correct lane self.shift_priorities(); - let saved_state = self.load_work(); + let mut deadline_reached = || (&mut deadline_reached).now_or_never().is_some(); - // We have to split away some parts of ourself - current lane is borrowed mutably - let mut shared = self.pool.clone(); - let mut machine = unsafe { saved_state.promote(&mut shared) }; + let finished_before_deadline = + self.work_on_current_lane(&mut deadline_reached, &mut committed_mutations); - if machine.stack.is_empty() { - let shared = self.pool.clone(); - self.current_lane().dirty_scopes.sort_by(|a, b| { - let h1 = shared.get_scope(*a).unwrap().height; - let h2 = shared.get_scope(*b).unwrap().height; - h1.cmp(&h2) - }); - - if let Some(scope) = self.current_lane().dirty_scopes.pop() { - let component = self.pool.get_scope(scope).unwrap(); - let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); - machine.stack.push(DiffInstruction::DiffNode { new, old }); - } - } - - let completed = { - let fut = machine.work(); - pin_mut!(fut); - use futures_util::future::{select, Either}; - match select(fut, &mut deadline).await { - Either::Left((_, _other)) => true, - Either::Right((_, _other)) => false, - } - }; - - let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) }; - let mut saved = machine.save(); - - if completed { - for node in saved.seen_scopes.drain() { - self.current_lane().dirty_scopes.remove(&node); - } - - let mut new_mutations = Mutations::new(); - std::mem::swap(&mut new_mutations, &mut saved.mutations); - - committed_mutations.push(new_mutations); - } - - self.save_work(saved); - - if !completed { + if !finished_before_deadline { break; } } @@ -443,6 +419,57 @@ impl Scheduler { committed_mutations } + // returns true if the lane is finished + pub fn work_on_current_lane( + &mut self, + deadline_reached: &mut impl FnMut() -> bool, + mutations: &mut Vec, + ) -> bool { + // Work through the current subtree, and commit the results when it finishes + // When the deadline expires, give back the work + let saved_state = self.load_work(); + + // We have to split away some parts of ourself - current lane is borrowed mutably + let mut shared = self.pool.clone(); + let mut machine = unsafe { saved_state.promote(&mut shared) }; + + if machine.stack.is_empty() { + let shared = self.pool.clone(); + self.current_lane().dirty_scopes.sort_by(|a, b| { + let h1 = shared.get_scope(*a).unwrap().height; + let h2 = shared.get_scope(*b).unwrap().height; + h1.cmp(&h2) + }); + + if let Some(scope) = self.current_lane().dirty_scopes.pop() { + let component = self.pool.get_scope(scope).unwrap(); + let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + machine.stack.push(DiffInstruction::DiffNode { new, old }); + } + } + + let deadline_expired = machine.work(deadline_reached); + + let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) }; + let mut saved = machine.save(); + + if deadline_expired { + self.save_work(saved); + false + } else { + for node in saved.seen_scopes.drain() { + self.current_lane().dirty_scopes.remove(&node); + } + + let mut new_mutations = Mutations::new(); + std::mem::swap(&mut new_mutations, &mut saved.mutations); + + mutations.push(new_mutations); + self.save_work(saved); + true + } + } + // waits for a trigger, canceling early if the deadline is reached // returns true if the deadline was reached // does not return the trigger, but caches it in the scheduler @@ -537,16 +564,32 @@ impl TaskHandle { /// Toggles this coroutine off/on. /// /// This method is not synchronous - your task will not stop immediately. - pub fn toggle(&self) {} + pub fn toggle(&self) { + self.sender + .unbounded_send(SchedulerMsg::ToggleTask(self.our_id)) + .unwrap() + } /// This method is not synchronous - your task will not stop immediately. - pub fn start(&self) {} + pub fn resume(&self) { + self.sender + .unbounded_send(SchedulerMsg::ResumeTask(self.our_id)) + .unwrap() + } /// This method is not synchronous - your task will not stop immediately. - pub fn stop(&self) {} + pub fn stop(&self) { + self.sender + .unbounded_send(SchedulerMsg::ToggleTask(self.our_id)) + .unwrap() + } /// This method is not synchronous - your task will not stop immediately. - pub fn restart(&self) {} + pub fn restart(&self) { + self.sender + .unbounded_send(SchedulerMsg::ToggleTask(self.our_id)) + .unwrap() + } } #[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] @@ -589,9 +632,9 @@ impl ElementId { /// flushed before proceeding. Multiple discrete events is highly unlikely, though. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] pub enum EventPriority { - /// Work that must be completed during the EventHandler phase - /// + /// Work that must be completed during the EventHandler phase. /// + /// Currently this is reserved for controlled inputs. Immediate = 3, /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 6ce6d5d05..af1c9536f 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -209,13 +209,14 @@ impl VirtualDom { .get_scope_mut(self.base_scope) .expect("The base scope should never be moved"); - // // We run the component. If it succeeds, then we can diff it and add the changes to the dom. + // We run the component. If it succeeds, then we can diff it and add the changes to the dom. if cur_component.run_scope(&self.scheduler.pool) { diff_machine .stack .create_node(cur_component.frames.fin_head(), MountType::Append); diff_machine.stack.scope_stack.push(self.base_scope); - diff_machine.work().await; + + // let completed = diff_machine.work(); } else { // todo: should this be a hard error? log::warn!( @@ -227,19 +228,7 @@ impl VirtualDom { unsafe { std::mem::transmute(diff_machine.mutations) } } - pub fn diff_sync<'s>(&'s mut self) -> Mutations<'s> { - let mut fut = self.diff_async().boxed_local(); - - loop { - if let Some(edits) = (&mut fut).now_or_never() { - break edits; - } - } - } - - pub async fn diff_async<'s>(&'s mut self) -> Mutations<'s> { - let mut diff_machine = DiffMachine::new(Mutations::new(), todo!()); - + pub fn diff<'s>(&'s mut self) -> Mutations<'s> { let cur_component = self .scheduler .pool @@ -247,22 +236,24 @@ impl VirtualDom { .expect("The base scope should never be moved"); if cur_component.run_scope(&self.scheduler.pool) { - diff_machine.diff_scope(self.base_scope).await; + let mut diff_machine = DiffMachine::new(Mutations::new(), todo!()); + diff_machine.diff_scope(self.base_scope); + diff_machine.mutations + } else { + Mutations::new() } - - diff_machine.mutations } /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. /// - /// This method will not wait for any suspended nodes to complete. - pub fn run_immediate<'s>(&'s mut self) -> Mutations<'s> { - 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") + /// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will + /// return "None" + pub fn run_immediate<'s>(&'s mut self) -> Option>> { + if self.scheduler.has_any_work() { + Some(self.scheduler.work_sync()) + } else { + None + } } /// Run the virtualdom with a deadline. From 44b43849613a52cee8dc98ea3068095416b201c2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 1 Sep 2021 23:33:25 -0400 Subject: [PATCH 49/53] wip: more scheduler work --- packages/core/.vscode/settings.json | 2 +- packages/core/src/diff.rs | 2 +- packages/core/src/scheduler.rs | 16 +++++++++------- packages/core/src/virtual_dom.rs | 6 +++++- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index e9c4e46cb..089c4fa5d 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -153,7 +153,7 @@ impl<'bump> DiffMachine<'bump> { } } - pub fn diff_scope(&'bump mut self, id: ScopeId) { + pub fn diff_scope(&mut self, id: ScopeId) { if let Some(component) = self.vdom.get_scope_mut(id) { let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); self.stack.push(DiffInstruction::DiffNode { new, old }); diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index dfa90bb56..14465d656 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -335,20 +335,20 @@ impl Scheduler { /// /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor. pub fn work_sync<'a>(&'a mut self) -> Vec> { - let mut committed_mutations = Vec::>::new(); + let mut committed_mutations = Vec::new(); - // Internalize any pending work since the last time we ran self.manually_poll_events(); if !self.has_any_work() { self.pool.clean_up_garbage(); + return committed_mutations; } - while self.has_any_work() { - // do work + self.consume_pending_events(); - // Create work from the pending event queue - self.consume_pending_events(); + while self.has_any_work() { + self.shift_priorities(); + self.work_on_current_lane(&mut || false, &mut committed_mutations); } committed_mutations @@ -419,7 +419,9 @@ impl Scheduler { committed_mutations } - // returns true if the lane is finished + /// Load the current lane, and work on it, periodically checking in if the deadline has been reached. + /// + /// Returns true if the lane is finished before the deadline could be met. pub fn work_on_current_lane( &mut self, deadline_reached: &mut impl FnMut() -> bool, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index af1c9536f..b43907ea2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -228,6 +228,9 @@ impl VirtualDom { unsafe { std::mem::transmute(diff_machine.mutations) } } + /// Compute a manual diff of the VirtualDOM between states. + /// + /// This can be useful when state inside the DOM is remotely changed from the outside, but not propogated as an event. pub fn diff<'s>(&'s mut self) -> Mutations<'s> { let cur_component = self .scheduler @@ -236,7 +239,8 @@ impl VirtualDom { .expect("The base scope should never be moved"); if cur_component.run_scope(&self.scheduler.pool) { - let mut diff_machine = DiffMachine::new(Mutations::new(), todo!()); + let mut diff_machine: DiffMachine<'s> = + DiffMachine::new(Mutations::new(), &mut self.scheduler.pool); diff_machine.diff_scope(self.base_scope); diff_machine.mutations } else { From a652090dc5708db334fa7430fededb1bac207880 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 1 Sep 2021 23:57:34 -0400 Subject: [PATCH 50/53] wip: add test_dom --- packages/core/src/diff.rs | 2 +- packages/core/src/events.rs | 9 ++- packages/core/src/lib.rs | 1 + packages/core/src/test_dom.rs | 82 +++++++++++++++++++ packages/core/tests/create_iterative.rs | 2 +- packages/core/tests/diff_iterative.rs | 2 +- packages/core/tests/diffing.rs | 84 -------------------- packages/core/tests/eventsystem.rs | 2 +- packages/core/tests/hooks.rs | 2 +- packages/core/tests/work_sync.rs | 2 +- packages/desktop/src/events.rs | 1 + packages/web/src/dom.rs | 100 ++++++++++++++++++++++-- 12 files changed, 192 insertions(+), 97 deletions(-) create mode 100644 packages/core/src/test_dom.rs diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 089c4fa5d..75d1d9a9a 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -136,7 +136,7 @@ impl<'a> SavedDiffWork<'a> { } impl<'bump> DiffMachine<'bump> { - pub(crate) fn new(edits: Mutations<'bump>, shared: &'bump mut ResourcePool) -> Self { + pub(crate) fn new(edits: Mutations<'bump>, shared: &'bump ResourcePool) -> Self { Self { stack: DiffStack::new(), mutations: edits, diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 952c3b2e3..b7643ca7d 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -13,7 +13,10 @@ pub struct UiEvent { /// The optional real node associated with the trigger pub mounted_dom_id: Option, - pub listener_type: &'static str, + /// The event type IE "onclick" or "onmouseover" + /// + /// The name that the renderer will use to mount the listener. + pub name: &'static str, /// The type of event pub event: SyntheticEvent, @@ -406,6 +409,10 @@ pub mod on { /// ontoggle ]; + + GenericEventInner(GenericEvent): [ + + ]; } pub trait GenericEventInner { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index f01838e23..8193673e4 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -26,6 +26,7 @@ pub mod mutations; pub mod nodes; pub mod scheduler; pub mod scope; +pub mod test_dom; pub mod util; pub mod virtual_dom; diff --git a/packages/core/src/test_dom.rs b/packages/core/src/test_dom.rs new file mode 100644 index 000000000..4ab761fbb --- /dev/null +++ b/packages/core/src/test_dom.rs @@ -0,0 +1,82 @@ +//! A DOM for testing - both internal and external code. +use bumpalo::Bump; + +use crate::innerlude::*; +use crate::nodes::IntoVNode; + +pub struct TestDom { + bump: Bump, + scheduler: Scheduler, +} + +impl TestDom { + pub fn new() -> TestDom { + let bump = Bump::new(); + let scheduler = Scheduler::new(); + TestDom { bump, scheduler } + } + pub fn new_factory<'a>(&'a self) -> NodeFactory<'a> { + NodeFactory::new(&self.bump) + } + + pub fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a> + where + F: FnOnce(NodeFactory<'a>) -> VNode<'a>, + { + lazy_nodes.into_vnode(NodeFactory::new(&self.bump)) + } + + pub fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { + let mutations = Mutations::new(); + let mut machine = DiffMachine::new(mutations, &self.scheduler.pool); + machine.stack.push(DiffInstruction::DiffNode { new, old }); + + machine.mutations + } + + pub fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> Mutations<'a> + where + F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, + { + let old = self.bump.alloc(self.render(left)); + + let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); + + machine.stack.create_node(old, MountType::Append); + + machine.work(&mut || false); + + machine.mutations + } + + pub fn lazy_diff<'a, F1, F2>( + &'a self, + left: LazyNodes<'a, F1>, + right: LazyNodes<'a, F2>, + ) -> (Mutations<'a>, Mutations<'a>) + where + F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, + F2: FnOnce(NodeFactory<'a>) -> VNode<'a>, + { + let old = self.bump.alloc(self.render(left)); + + let new = self.bump.alloc(self.render(right)); + + let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); + + machine.stack.create_node(old, MountType::Append); + + machine.work(&mut || false); + let create_edits = machine.mutations; + + let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); + + machine.stack.push(DiffInstruction::DiffNode { old, new }); + + machine.work(&mut || false); + + let edits = machine.mutations; + + (create_edits, edits) + } +} diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_iterative.rs index 445e990ed..d95b3b0dd 100644 --- a/packages/core/tests/create_iterative.rs +++ b/packages/core/tests/create_iterative.rs @@ -1,7 +1,7 @@ //! tests to prove that the iterative implementation works use anyhow::{Context, Result}; -use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, DomEdit, Mutations}; +use dioxus::{prelude::*, DomEdit, Mutations}; mod test_logging; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/diff_iterative.rs b/packages/core/tests/diff_iterative.rs index 530c9af59..0e1a9b1b5 100644 --- a/packages/core/tests/diff_iterative.rs +++ b/packages/core/tests/diff_iterative.rs @@ -43,6 +43,6 @@ async fn test_iterative_create_components() { let mutations = dom.rebuild_async().await; dbg!(mutations); - let mutations = dom.diff_async().await; + let mutations = dom.diff(); dbg!(mutations); } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index e8a018e42..c6cfdd5f1 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -15,90 +15,6 @@ use DomEdit::*; // feel free to enable while debugging const IS_LOGGING_ENABLED: bool = false; -struct TestDom { - bump: Bump, - resources: Scheduler, -} -impl TestDom { - fn new() -> TestDom { - test_logging::set_up_logging(IS_LOGGING_ENABLED); - let bump = Bump::new(); - let resources = Scheduler::new(); - TestDom { bump, resources } - } - fn new_factory<'a>(&'a self) -> NodeFactory<'a> { - NodeFactory::new(&self.bump) - } - - fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a> - where - F: FnOnce(NodeFactory<'a>) -> VNode<'a>, - { - use dioxus_core::nodes::{IntoVNode, IntoVNodeList}; - lazy_nodes.into_vnode(NodeFactory::new(&self.bump)) - } - - fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { - // let mut edits = Vec::new(); - let mut machine = DiffMachine::new_headless(&self.resources); - - machine.stack.push(DiffInstruction::DiffNode { new, old }); - - machine.mutations - } - - fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> Mutations<'a> - where - F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, - { - let old = self.bump.alloc(self.render(left)); - - let mut machine = DiffMachine::new_headless(&self.resources); - - machine.stack.create_node(old, MountType::Append); - - work_sync(&mut machine); - - machine.mutations - } - - fn lazy_diff<'a, F1, F2>( - &'a self, - left: LazyNodes<'a, F1>, - right: LazyNodes<'a, F2>, - ) -> (Mutations<'a>, Mutations<'a>) - where - F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, - F2: FnOnce(NodeFactory<'a>) -> VNode<'a>, - { - let old = self.bump.alloc(self.render(left)); - - let new = self.bump.alloc(self.render(right)); - - let mut machine = DiffMachine::new_headless(&self.resources); - - machine.stack.create_node(old, MountType::Append); - - work_sync(&mut machine); - let create_edits = machine.mutations; - - let mut machine = DiffMachine::new_headless(&self.resources); - machine.stack.push(DiffInstruction::DiffNode { old, new }); - work_sync(&mut machine); - let edits = machine.mutations; - - (create_edits, edits) - } -} - -fn work_sync(machine: &mut DiffMachine) { - let mut fut = machine.work().boxed_local(); - - while let None = (&mut fut).now_or_never() { - // - } -} - #[test] fn diffing_works() {} diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs index db57118b4..9c7748e44 100644 --- a/packages/core/tests/eventsystem.rs +++ b/packages/core/tests/eventsystem.rs @@ -1,7 +1,7 @@ use bumpalo::Bump; use anyhow::{Context, Result}; -use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, DomEdit}; +use dioxus::{prelude::*, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/hooks.rs b/packages/core/tests/hooks.rs index 3fcc0bccd..66b2f5906 100644 --- a/packages/core/tests/hooks.rs +++ b/packages/core/tests/hooks.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, DomEdit}; +use dioxus::prelude::*; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/work_sync.rs b/packages/core/tests/work_sync.rs index 075060c20..b36c65b1a 100644 --- a/packages/core/tests/work_sync.rs +++ b/packages/core/tests/work_sync.rs @@ -3,7 +3,7 @@ //! This means you can actually call it synchronously if you want. use anyhow::{Context, Result}; -use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, scope::Scope}; +use dioxus::{prelude::*, scope::Scope}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; use futures_util::FutureExt; diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 2a44a7d78..2e4b57255 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -25,6 +25,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UiEvent { let scope = ScopeId(data.scope as usize); let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize)); UiEvent { + name: todo!(), event, scope, mounted_dom_id, diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index d4c282980..1ef419b99 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -530,13 +530,13 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { let typ = event.type_(); - use anyhow::Context; + // let attrs = target.attributes(); + // for x in 0..attrs.length() { + // let attr = attrs.item(x).unwrap(); + // log::debug!("attrs include: {:#?}", attr); + // } - let attrs = target.attributes(); - for x in 0..attrs.length() { - let attr = attrs.item(x).unwrap(); - log::debug!("attrs include: {:#?}", attr); - } + use anyhow::Context; // The error handling here is not very descriptive and needs to be replaced with a zero-cost error system let val: String = target @@ -562,6 +562,7 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { // let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into(); log::debug!("Triggered scope is {:#?}", triggered_scope); Ok(UiEvent { + name: event_name_from_typ(&typ), event: virtual_event_from_websys_event(event.clone()), mounted_dom_id: Some(ElementId(real_id as usize)), scope: ScopeId(triggered_scope as usize), @@ -578,3 +579,90 @@ pub fn load_document() -> Document { .document() .expect("should have access to the Document") } + +pub fn event_name_from_typ(typ: &str) -> &'static str { + match typ { + "copy" => "oncopy", + "cut" => "oncut", + "paste" => "onpaste", + "compositionend" => "oncompositionend", + "compositionstart" => "oncompositionstart", + "compositionupdate" => "oncompositionupdate", + "keydown" => "onkeydown", + "keypress" => "onkeypress", + "keyup" => "onkeyup", + "focus" => "onfocus", + "blur" => "onblur", + "change" => "onchange", + "input" => "oninput", + "invalid" => "oninvalid", + "reset" => "onreset", + "submit" => "onsubmit", + "click" => "onclick", + "contextmenu" => "oncontextmenu", + "doubleclick" => "ondoubleclick", + "drag" => "ondrag", + "dragend" => "ondragend", + "dragenter" => "ondragenter", + "dragexit" => "ondragexit", + "dragleave" => "ondragleave", + "dragover" => "ondragover", + "dragstart" => "ondragstart", + "drop" => "ondrop", + "mousedown" => "onmousedown", + "mouseenter" => "onmouseenter", + "mouseleave" => "onmouseleave", + "mousemove" => "onmousemove", + "mouseout" => "onmouseout", + "mouseover" => "onmouseover", + "mouseup" => "onmouseup", + "pointerdown" => "onpointerdown", + "pointermove" => "onpointermove", + "pointerup" => "onpointerup", + "pointercancel" => "onpointercancel", + "gotpointercapture" => "ongotpointercapture", + "lostpointercapture" => "onlostpointercapture", + "pointerenter" => "onpointerenter", + "pointerleave" => "onpointerleave", + "pointerover" => "onpointerover", + "pointerout" => "onpointerout", + "select" => "onselect", + "touchcancel" => "ontouchcancel", + "touchend" => "ontouchend", + "touchmove" => "ontouchmove", + "touchstart" => "ontouchstart", + "scroll" => "onscroll", + "wheel" => "onwheel", + "animationstart" => "onanimationstart", + "animationend" => "onanimationend", + "animationiteration" => "onanimationiteration", + "transitionend" => "ontransitionend", + "abort" => "onabort", + "canplay" => "oncanplay", + "canplaythrough" => "oncanplaythrough", + "durationchange" => "ondurationchange", + "emptied" => "onemptied", + "encrypted" => "onencrypted", + "ended" => "onended", + "error" => "onerror", + "loadeddata" => "onloadeddata", + "loadedmetadata" => "onloadedmetadata", + "loadstart" => "onloadstart", + "pause" => "onpause", + "play" => "onplay", + "playing" => "onplaying", + "progress" => "onprogress", + "ratechange" => "onratechange", + "seeked" => "onseeked", + "seeking" => "onseeking", + "stalled" => "onstalled", + "suspend" => "onsuspend", + "timeupdate" => "ontimeupdate", + "volumechange" => "onvolumechange", + "waiting" => "onwaiting", + "toggle" => "ontoggle", + _ => { + panic!("unsupported event type") + } + } +} From 958c02a26e86b07431d3e5f51f00afe116656981 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 2 Sep 2021 00:10:09 -0400 Subject: [PATCH 51/53] wip: working down warnings --- packages/core/src/bumpframe.rs | 4 --- packages/core/src/diff.rs | 60 +++++++++++++++++---------------- packages/core/src/lib.rs | 3 +- packages/core/src/scope.rs | 16 ++++----- packages/core/src/test_dom.rs | 4 +-- packages/core/tests/diffing.rs | 61 ++++++++++++++++++---------------- 6 files changed, 77 insertions(+), 71 deletions(-) diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index b2b54107e..8cb6d6734 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -40,10 +40,6 @@ impl ActiveFrame { self.wip_frame_mut().bump.reset() } - pub fn update_head_node<'a>(&mut self, node: VNode<'a>) { - self.wip_frame_mut().head_node = unsafe { std::mem::transmute(node) }; - } - /// The "work in progress frame" represents the frame that is currently being worked on. pub fn wip_frame(&self) -> &BumpFrame { match self.generation.get() & 1 == 0 { diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 75d1d9a9a..bafdfea35 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -294,15 +294,18 @@ impl<'bump> DiffMachine<'bump> { self.stack.add_child_count(1); - let cur_scope_id = self.stack.current_scope().unwrap(); - let scope = self.vdom.get_scope(cur_scope_id).unwrap(); + if let Some(cur_scope_id) = self.stack.current_scope() { + let scope = self.vdom.get_scope(cur_scope_id).unwrap(); - listeners.iter().for_each(|listener| { - self.attach_listener_to_scope(listener, scope); - listener.mounted_node.set(Some(real_id)); - self.mutations - .new_event_listener(listener, cur_scope_id.clone()); - }); + listeners.iter().for_each(|listener| { + self.attach_listener_to_scope(listener, scope); + listener.mounted_node.set(Some(real_id)); + self.mutations + .new_event_listener(listener, cur_scope_id.clone()); + }); + } 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); @@ -465,28 +468,29 @@ impl<'bump> DiffMachine<'bump> { // // TODO: take a more efficient path than this - let cur_scope_id = self.stack.current_scope().unwrap(); - let scope = self.vdom.get_scope(cur_scope_id).unwrap(); + if let Some(cur_scope_id) = self.stack.current_scope() { + let scope = self.vdom.get_scope(cur_scope_id).unwrap(); - 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 { - please_commit(&mut self.mutations.edits); - self.mutations.remove_event_listener(old_l.event); - self.mutations.new_event_listener(new_l, cur_scope_id); + 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 { + please_commit(&mut self.mutations.edits); + self.mutations.remove_event_listener(old_l.event); + self.mutations.new_event_listener(new_l, cur_scope_id); + } + new_l.mounted_node.set(old_l.mounted_node.get()); + self.attach_listener_to_scope(new_l, scope); + } + } else { + please_commit(&mut self.mutations.edits); + for listener in old.listeners { + self.mutations.remove_event_listener(listener.event); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.mutations.new_event_listener(listener, cur_scope_id); + self.attach_listener_to_scope(listener, scope); } - new_l.mounted_node.set(old_l.mounted_node.get()); - self.attach_listener_to_scope(new_l, scope); - } - } else { - please_commit(&mut self.mutations.edits); - for listener in old.listeners { - self.mutations.remove_event_listener(listener.event); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.mutations.new_event_listener(listener, cur_scope_id); - self.attach_listener_to_scope(listener, scope); } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 8193673e4..22fa4f09a 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -45,6 +45,7 @@ pub(crate) mod innerlude { pub use crate::nodes::*; pub use crate::scheduler::*; pub use crate::scope::*; + pub use crate::test_dom::*; pub use crate::util::*; pub use crate::virtual_dom::*; @@ -57,7 +58,7 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, - SuspendedContext, SyntheticEvent, UiEvent, VNode, VirtualDom, FC, + SuspendedContext, SyntheticEvent, TestDom, UiEvent, VNode, VirtualDom, FC, }; pub mod prelude { diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 9b154f097..0f5cae2c7 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -62,7 +62,7 @@ impl Scope { // // Scopes cannot be made anywhere else except for this file // Therefore, their lifetimes are connected exclusively to the virtual dom - pub fn new<'creator_node>( + pub(crate) fn new<'creator_node>( caller: Rc, arena_idx: ScopeId, parent: Option, @@ -211,15 +211,11 @@ impl Scope { } } - pub fn root(&self) -> &VNode { - self.frames.fin_head() - } - - pub fn child_nodes<'a>(&'a self) -> ScopeChildren { + pub(crate) fn child_nodes<'a>(&'a self) -> ScopeChildren { unsafe { self.child_nodes.shorten_lifetime() } } - pub fn call_suspended_node<'a>(&'a self, task: u64) { + pub(crate) fn call_suspended_node<'a>(&'a self, task: u64) { let g = self.suspended_nodes.borrow_mut(); if let Some(suspended) = g.get(&task) { @@ -239,7 +235,7 @@ impl Scope { } } - pub fn consume_garbage(&self) -> Vec<&VNode> { + pub(crate) fn consume_garbage(&self) -> Vec<&VNode> { self.pending_garbage .borrow_mut() .drain(..) @@ -251,4 +247,8 @@ impl Scope { }) .collect::>() } + + pub fn root(&self) -> &VNode { + self.frames.fin_head() + } } diff --git a/packages/core/src/test_dom.rs b/packages/core/src/test_dom.rs index 4ab761fbb..39b9e5e74 100644 --- a/packages/core/src/test_dom.rs +++ b/packages/core/src/test_dom.rs @@ -12,9 +12,10 @@ pub struct TestDom { impl TestDom { pub fn new() -> TestDom { let bump = Bump::new(); - let scheduler = Scheduler::new(); + let mut scheduler = Scheduler::new(); TestDom { bump, scheduler } } + pub fn new_factory<'a>(&'a self) -> NodeFactory<'a> { NodeFactory::new(&self.bump) } @@ -30,7 +31,6 @@ impl TestDom { let mutations = Mutations::new(); let mut machine = DiffMachine::new(mutations, &self.scheduler.pool); machine.stack.push(DiffInstruction::DiffNode { new, old }); - machine.mutations } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index c6cfdd5f1..2f58eb0ae 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -1,13 +1,13 @@ //! Diffing Tests +//! +//! These tests only verify that the diffing algorithm works properly for single components. +//! +//! It does not validated that component lifecycles work properly. This is done in another test file. -use bumpalo::Bump; - -use dioxus::{ - diff::DiffMachine, prelude::*, scheduler::Scheduler, DiffInstruction, DomEdit, MountType, -}; +use dioxus::{prelude::*, DomEdit, TestDom}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; -use futures_util::FutureExt; + mod test_logging; use DomEdit::*; @@ -15,13 +15,18 @@ use DomEdit::*; // feel free to enable while debugging const IS_LOGGING_ENABLED: bool = false; +fn new_dom() -> TestDom { + test_logging::set_up_logging(IS_LOGGING_ENABLED); + TestDom::new() +} + #[test] fn diffing_works() {} /// Should push the text node onto the stack and modify it #[test] fn html_and_rsx_generate_the_same_output() { - let dom = TestDom::new(); + let dom = new_dom(); let (create, change) = dom.lazy_diff( rsx! ( div { "Hello world" } ), rsx! ( div { "Goodbye world" } ), @@ -54,7 +59,7 @@ fn html_and_rsx_generate_the_same_output() { /// Should result in 3 elements on the stack #[test] fn fragments_create_properly() { - let dom = TestDom::new(); + let dom = new_dom(); let create = dom.create(rsx! { div { "Hello a" } @@ -91,7 +96,7 @@ fn fragments_create_properly() { /// Should result in the creation of an anchor (placeholder) and then a replacewith #[test] fn empty_fragments_create_anchors() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ (0..0).map(|f| rsx! { div {}}) }); let right = rsx!({ (0..1).map(|f| rsx! { div {}}) }); @@ -114,7 +119,7 @@ fn empty_fragments_create_anchors() { /// Should result in the creation of an anchor (placeholder) and then a replacewith m=5 #[test] fn empty_fragments_create_many_anchors() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ (0..0).map(|f| rsx! { div {}}) }); let right = rsx!({ (0..5).map(|f| rsx! { div {}}) }); @@ -141,7 +146,7 @@ fn empty_fragments_create_many_anchors() { /// Includes child nodes inside the fragment #[test] fn empty_fragments_create_anchors_with_many_children() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ (0..0).map(|f| rsx! { div {} }) }); let right = rsx!({ @@ -184,7 +189,7 @@ fn empty_fragments_create_anchors_with_many_children() { /// Should result in every node being pushed and then replaced with an anchor #[test] fn many_items_become_fragment() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ (0..2).map(|f| { @@ -227,7 +232,7 @@ fn many_items_become_fragment() { /// Should result in no edits #[test] fn two_equal_fragments_are_equal() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ (0..2).map(|f| { @@ -247,7 +252,7 @@ fn two_equal_fragments_are_equal() { /// Should result the creation of more nodes appended after the old last node #[test] fn two_fragments_with_differrent_elements_are_differet() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!( { (0..2).map(|_| rsx! { div { }} ) } @@ -280,7 +285,7 @@ fn two_fragments_with_differrent_elements_are_differet() { /// Should result in multiple nodes destroyed - with changes to the first nodes #[test] fn two_fragments_with_differrent_elements_are_differet_shorter() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!( {(0..5).map(|f| {rsx! { div { }}})} @@ -321,7 +326,7 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() { /// Should result in multiple nodes destroyed - with no changes #[test] fn two_fragments_with_same_elements_are_differet() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!( {(0..2).map(|f| {rsx! { div { }}})} @@ -356,7 +361,7 @@ fn two_fragments_with_same_elements_are_differet() { /// should result in the removal of elements #[test] fn keyed_diffing_order() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!( {(0..5).map(|f| {rsx! { div { key: "{f}" }}})} @@ -377,7 +382,7 @@ fn keyed_diffing_order() { /// Should result in moves, but not removals or additions #[test] fn keyed_diffing_out_of_order() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| { @@ -402,7 +407,7 @@ fn keyed_diffing_out_of_order() { /// Should result in moves only #[test] fn keyed_diffing_out_of_order_adds() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { @@ -429,7 +434,7 @@ fn keyed_diffing_out_of_order_adds() { /// Should result in moves onl #[test] fn keyed_diffing_out_of_order_adds_2() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { @@ -457,7 +462,7 @@ fn keyed_diffing_out_of_order_adds_2() { /// Should result in moves onl #[test] fn keyed_diffing_out_of_order_adds_3() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { @@ -485,7 +490,7 @@ fn keyed_diffing_out_of_order_adds_3() { /// Should result in moves onl #[test] fn keyed_diffing_out_of_order_adds_4() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { @@ -513,7 +518,7 @@ fn keyed_diffing_out_of_order_adds_4() { /// Should result in moves onl #[test] fn keyed_diffing_out_of_order_adds_5() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { @@ -536,7 +541,7 @@ fn keyed_diffing_out_of_order_adds_5() { #[test] fn keyed_diffing_additions() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { @@ -563,7 +568,7 @@ fn keyed_diffing_additions() { #[test] fn keyed_diffing_additions_and_moves_on_ends() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { @@ -595,7 +600,7 @@ fn keyed_diffing_additions_and_moves_on_ends() { #[test] fn keyed_diffing_additions_and_moves_in_middle() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { @@ -632,7 +637,7 @@ fn keyed_diffing_additions_and_moves_in_middle() { #[test] fn controlled_keyed_diffing_out_of_order() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [4, 5, 6, 7].iter().map(|f| { @@ -669,7 +674,7 @@ fn controlled_keyed_diffing_out_of_order() { #[test] fn controlled_keyed_diffing_out_of_order_max_test() { - let dom = TestDom::new(); + let dom = new_dom(); let left = rsx!({ [0, 1, 2, 3, 4].iter().map(|f| { From dfe9f86bfff48ae3166fd41d673245ed142c26f0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 2 Sep 2021 00:16:49 -0400 Subject: [PATCH 52/53] wip: garbage collection --- packages/core/src/scheduler.rs | 119 +++++++++++++++++---------------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 14465d656..c9ca86856 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -340,7 +340,7 @@ impl Scheduler { self.manually_poll_events(); if !self.has_any_work() { - self.pool.clean_up_garbage(); + self.clean_up_garbage(); return committed_mutations; } @@ -392,7 +392,7 @@ impl Scheduler { // Wait for any new events if we have nothing to do if !self.has_any_work() { - self.pool.clean_up_garbage(); + self.clean_up_garbage(); let deadline_expired = self.wait_for_any_trigger(&mut deadline_reached).await; if deadline_expired { @@ -437,6 +437,7 @@ impl Scheduler { if machine.stack.is_empty() { let shared = self.pool.clone(); + self.current_lane().dirty_scopes.sort_by(|a, b| { let h1 = shared.get_scope(*a).unwrap().height; let h2 = shared.get_scope(*b).unwrap().height; @@ -531,6 +532,65 @@ impl Scheduler { // EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), // }; } + + fn collect_garbage(&mut self, id: ElementId) { + // + } + + pub fn clean_up_garbage(&mut self) { + let mut scopes_to_kill = Vec::new(); + let mut garbage_list = Vec::new(); + + for scope in self.garbage_scopes.drain() { + let scope = self.pool.get_scope_mut(scope).unwrap(); + for node in scope.consume_garbage() { + garbage_list.push(node); + } + + while let Some(node) = garbage_list.pop() { + match &node { + VNode::Text(_) => { + self.pool.collect_garbage(node.mounted_id()); + } + VNode::Anchor(_) => { + self.pool.collect_garbage(node.mounted_id()); + } + VNode::Suspended(_) => { + self.pool.collect_garbage(node.mounted_id()); + } + + VNode::Element(el) => { + self.pool.collect_garbage(node.mounted_id()); + for child in el.children { + garbage_list.push(child); + } + } + + VNode::Fragment(frag) => { + for child in frag.children { + garbage_list.push(child); + } + } + + VNode::Component(comp) => { + // TODO: run the hook destructors and then even delete the scope + + let scope_id = comp.associated_scope.get().unwrap(); + let scope = self.pool.get_scope(scope_id).unwrap(); + let root = scope.root(); + + garbage_list.push(root); + scopes_to_kill.push(scope_id); + } + } + } + } + + for scope in scopes_to_kill.drain(..) { + // + // kill em + } + } } pub(crate) struct PriorityLane { @@ -749,59 +809,4 @@ impl ResourcePool { } pub fn borrow_bumpframe(&self) {} - - pub fn clean_up_garbage(&mut self) { - // let mut scopes_to_kill = Vec::new(); - // let mut garbage_list = Vec::new(); - - todo!("garbage collection is currently immediate") - // for scope in self.garbage_scopes.drain() { - // let scope = self.get_scope_mut(scope).unwrap(); - // for node in scope.consume_garbage() { - // garbage_list.push(node); - // } - - // while let Some(node) = garbage_list.pop() { - // match &node { - // VNode::Text(_) => { - // self.collect_garbage(node.direct_id()); - // } - // VNode::Anchor(_) => { - // self.collect_garbage(node.direct_id()); - // } - // VNode::Suspended(_) => { - // self.collect_garbage(node.direct_id()); - // } - - // VNode::Element(el) => { - // self.collect_garbage(node.direct_id()); - // for child in el.children { - // garbage_list.push(child); - // } - // } - - // VNode::Fragment(frag) => { - // for child in frag.children { - // garbage_list.push(child); - // } - // } - - // VNode::Component(comp) => { - // // TODO: run the hook destructors and then even delete the scope - - // let scope_id = comp.ass_scope.get().unwrap(); - // let scope = self.get_scope(scope_id).unwrap(); - // let root = scope.root(); - // garbage_list.push(root); - // scopes_to_kill.push(scope_id); - // } - // } - // } - // } - - // for scope in scopes_to_kill.drain(..) { - // // - // // kill em - // } - } } From 725b4a1d7f5d629b1b0a163b65bfd93b2f8a151b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 2 Sep 2021 00:37:57 -0400 Subject: [PATCH 53/53] wip: fix some event stuff for web and core --- Cargo.toml | 2 +- packages/core/src/events.rs | 50 ++++++++++++++-------------- packages/core/src/lib.rs | 2 +- packages/core/src/mutations.rs | 19 ++--------- packages/core/src/scheduler.rs | 7 ++-- packages/desktop/src/events.rs | 6 ++-- packages/web/src/dom.rs | 12 +++---- packages/web/src/events.rs | 60 ++++++++++++++++++++++++++++++++++ 8 files changed, 101 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1e1a592bb..043c7c802 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ dioxus-mobile = { path = "./packages/mobile", optional = true } [features] # core -default = ["core", "ssr", "web"] +default = ["core", "ssr"] core = ["macro", "hooks", "html"] macro = ["dioxus-core-macro"] hooks = ["dioxus-hooks"] diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index b7643ca7d..119afbf35 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -5,8 +5,17 @@ //! be heavy or need to interact through FFI, so the events themselves are designed to be lazy. use crate::innerlude::{ElementId, ScopeId}; +use bumpalo::boxed::Box as BumpBox; +use std::{any::Any, cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; + +use crate::{ + innerlude::NodeFactory, + innerlude::{Attribute, Listener, VNode}, +}; +use std::cell::Cell; + #[derive(Debug)] -pub struct UiEvent { +pub struct UserEvent { /// The originator of the event trigger pub scope: ScopeId, @@ -23,22 +32,23 @@ pub struct UiEvent { } pub enum SyntheticEvent { - KeyboardEvent(on::KeyboardEvent), - TouchEvent(on::TouchEvent), - MouseEvent(on::MouseEvent), + AnimationEvent(on::AnimationEvent), ClipboardEvent(on::ClipboardEvent), CompositionEvent(on::CompositionEvent), FocusEvent(on::FocusEvent), FormEvent(on::FormEvent), - SelectionEvent(on::SelectionEvent), - WheelEvent(on::WheelEvent), - MediaEvent(on::MediaEvent), - AnimationEvent(on::AnimationEvent), - TransitionEvent(on::TransitionEvent), + KeyboardEvent(on::KeyboardEvent), + GenericEvent(on::GenericEvent), + TouchEvent(on::TouchEvent), ToggleEvent(on::ToggleEvent), + MediaEvent(on::MediaEvent), + MouseEvent(on::MouseEvent), + WheelEvent(on::WheelEvent), + SelectionEvent(on::SelectionEvent), + TransitionEvent(on::TransitionEvent), PointerEvent(on::PointerEvent), - // ImageEvent(event_data::ImageEvent), } +// ImageEvent(event_data::ImageEvent), impl std::fmt::Debug for SyntheticEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -57,6 +67,7 @@ impl std::fmt::Debug for SyntheticEvent { SyntheticEvent::ToggleEvent(_) => "ToggleEvent", SyntheticEvent::MouseEvent(_) => "MouseEvent", SyntheticEvent::PointerEvent(_) => "PointerEvent", + SyntheticEvent::GenericEvent(_) => "GenericEvent", }; f.debug_struct("VirtualEvent").field("type", &name).finish() @@ -71,18 +82,7 @@ pub mod on { //! Arc allocation through "get_mut" //! //! React recently dropped support for re-using event allocation and just passes the real event along. - - #![allow(unused)] - use bumpalo::boxed::Box as BumpBox; - use std::{any::Any, cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; - - use crate::{ - innerlude::NodeFactory, - innerlude::{Attribute, ElementId, Listener, VNode}, - }; - use std::cell::Cell; - - use super::SyntheticEvent; + use super::*; macro_rules! event_directory { ( $( @@ -409,12 +409,10 @@ pub mod on { /// ontoggle ]; - - GenericEventInner(GenericEvent): [ - - ]; } + pub struct GenericEvent(pub Rc); + pub trait GenericEventInner { /// Return a reference to the raw event. User will need to downcast the event to the right platform-specific type. fn raw_event(&self) -> &dyn Any; diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 22fa4f09a..b588215b3 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -58,7 +58,7 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, - SuspendedContext, SyntheticEvent, TestDom, UiEvent, VNode, VirtualDom, FC, + SuspendedContext, SyntheticEvent, TestDom, UserEvent, VNode, VirtualDom, FC, }; pub mod prelude { diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 1eed2c323..19c590e70 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,5 +1,6 @@ //! Instructions returned by the VirtualDOM on how to modify the Real DOM. //! +//! This module contains an internal API to generate these instructions. use crate::innerlude::*; use std::any::Any; @@ -102,30 +103,14 @@ impl<'a> Mutations<'a> { let Attribute { name, value, - is_static, - is_volatile, namespace, - } = attribute; - - self.edits.push(SetAttribute { - field: name, - value, - ns: *namespace, - }); - } - pub(crate) fn set_attribute_ns(&mut self, attribute: &'a Attribute, namespace: &'a str) { - let Attribute { - name, - value, - is_static, - is_volatile, .. } = attribute; self.edits.push(SetAttribute { field: name, value, - ns: Some(namespace), + ns: *namespace, }); } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index c9ca86856..d173a81f5 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -97,7 +97,7 @@ pub struct EventChannel { pub enum SchedulerMsg { Immediate(ScopeId), - UiEvent(UiEvent), + UiEvent(UserEvent), SubmitTask(FiberTask, u64), ToggleTask(u64), PauseTask(u64), @@ -140,11 +140,11 @@ pub(crate) struct Scheduler { // scheduler stuff pub current_priority: EventPriority, - pub ui_events: VecDeque, + pub ui_events: VecDeque, pub pending_immediates: VecDeque, - pub pending_tasks: VecDeque, + pub pending_tasks: VecDeque, pub garbage_scopes: HashSet, @@ -266,6 +266,7 @@ impl Scheduler { SyntheticEvent::ToggleEvent(_) => {} SyntheticEvent::MouseEvent(_) => {} SyntheticEvent::PointerEvent(_) => {} + SyntheticEvent::GenericEvent(_) => {} }; scope.call_listener(trigger.event, element); diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 2e4b57255..83f2a8bba 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -8,7 +8,7 @@ use dioxus_core::{ on::{MouseEvent, MouseEventInner}, SyntheticEvent, }, - ElementId, EventPriority, ScopeId, UiEvent, + ElementId, EventPriority, ScopeId, UserEvent, }; #[derive(serde::Serialize, serde::Deserialize)] @@ -17,14 +17,14 @@ struct ImEvent { mounted_dom_id: u64, scope: u64, } -pub fn trigger_from_serialized(val: serde_json::Value) -> UiEvent { +pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { let mut data: Vec = serde_json::from_value(val).unwrap(); let data = data.drain(..).next().unwrap(); let event = SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent))); let scope = ScopeId(data.scope as usize); let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize)); - UiEvent { + UserEvent { name: todo!(), event, scope, diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 1ef419b99..621ad89c3 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc}; use dioxus_core::{ - events::{on::GenericEventInner, SyntheticEvent, UiEvent}, + events::{on::GenericEventInner, SyntheticEvent, UserEvent}, mutations::NodeRefMutation, scheduler::SchedulerMsg, DomEdit, ElementId, ScopeId, @@ -458,7 +458,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent { } "change" => { let evt = event.dyn_into().unwrap(); - SyntheticEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt)))) } "input" | "invalid" | "reset" | "submit" => { let evt: web_sys::InputEvent = event.clone().dyn_into().unwrap(); @@ -485,7 +485,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent { } "scroll" => { let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); - SyntheticEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt)))) } "wheel" => { let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap(); @@ -512,14 +512,14 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent { } _ => { let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap(); - SyntheticEvent::UIEvent(UIEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt)))) } } } /// This function decodes a websys event and produces an EventTrigger /// With the websys implementation, we attach a unique key to the nodes -fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { +fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { log::debug!("Handling event!"); let target = event @@ -561,7 +561,7 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { let triggered_scope = gi_id; // let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into(); log::debug!("Triggered scope is {:#?}", triggered_scope); - Ok(UiEvent { + Ok(UserEvent { name: event_name_from_typ(&typ), event: virtual_event_from_websys_event(event.clone()), mounted_dom_id: Some(ElementId(real_id as usize)), diff --git a/packages/web/src/events.rs b/packages/web/src/events.rs index 3a84f080c..ff4ced474 100644 --- a/packages/web/src/events.rs +++ b/packages/web/src/events.rs @@ -171,11 +171,71 @@ impl KeyboardEventInner for WebsysKeyboardEvent { } pub struct WebsysGenericUiEvent(pub UiEvent); +impl GenericEventInner for WebsysGenericUiEvent { + fn raw_event(&self) -> &dyn std::any::Any { + // self.0.raw_event() + todo!() + } + + fn bubbles(&self) -> bool { + self.0.bubbles() + } + + fn cancel_bubble(&self) { + self.0.cancel_bubble(); + } + + fn cancelable(&self) -> bool { + self.0.cancelable() + } + + fn composed(&self) -> bool { + self.0.composed() + } + + fn current_target(&self) { + // self.0.current_target() + } + + fn default_prevented(&self) -> bool { + self.0.default_prevented() + } + + fn event_phase(&self) -> u16 { + self.0.event_phase() + } + + fn is_trusted(&self) -> bool { + self.0.is_trusted() + } + + fn prevent_default(&self) { + self.0.prevent_default() + } + + fn stop_immediate_propagation(&self) { + self.0.stop_immediate_propagation() + } + + fn stop_propagation(&self) { + self.0.stop_propagation() + } + + fn target(&self) { + // self.0.target() + } + + fn time_stamp(&self) -> f64 { + self.0.time_stamp() + } +} + impl UIEventInner for WebsysGenericUiEvent { fn detail(&self) -> i32 { todo!() } } + impl SelectionEventInner for WebsysGenericUiEvent {} pub struct WebsysFocusEvent(pub web_sys::FocusEvent);