From fac42339c272b0e430ebf4f31b6061a0635d3e19 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 8 Aug 2021 15:15:16 -0400 Subject: [PATCH] feat: mutations --- packages/core-macro/examples/prop_test.rs | 1 + packages/core-macro/src/props/mod.rs | 6 + packages/core/examples/borrowed.rs | 1 + packages/core/src/arena.rs | 19 +- packages/core/src/component.rs | 2 +- packages/core/src/diff.rs | 103 +++--- packages/core/src/events.rs | 22 +- packages/core/src/hooks.rs | 4 +- packages/core/src/lib.rs | 4 +- packages/core/src/nodes.rs | 35 +- packages/core/src/scope.rs | 68 ++-- packages/core/src/util.rs | 13 - packages/core/src/virtual_dom.rs | 392 +++++++++++++++------- packages/core/tests/diffing.rs | 1 - packages/core/tests/eventsystem.rs | 32 ++ packages/desktop/src/lib.rs | 34 +- packages/web/src/lib.rs | 2 +- 17 files changed, 464 insertions(+), 275 deletions(-) create mode 100644 packages/core/tests/eventsystem.rs diff --git a/packages/core-macro/examples/prop_test.rs b/packages/core-macro/examples/prop_test.rs index 9a38fd604..d80c0cc35 100644 --- a/packages/core-macro/examples/prop_test.rs +++ b/packages/core-macro/examples/prop_test.rs @@ -4,6 +4,7 @@ pub mod dioxus { pub mod prelude { pub trait Properties { type Builder; + const IS_STATIC: bool; fn builder() -> Self::Builder; unsafe fn memoize(&self, other: &Self) -> bool; } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index f91eebbda..7088f1da2 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -662,6 +662,11 @@ Finally, call `.build()` to create the instance of `{name}`. false => quote! { self == other }, }; + let is_static = match are_there_generics { + true => quote! { false }, + false => quote! { true }, + }; + Ok(quote! { impl #impl_generics #name #ty_generics #where_clause { #[doc = #builder_method_doc] @@ -693,6 +698,7 @@ Finally, call `.build()` to create the instance of `{name}`. impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{ type Builder = #builder_name #generics_with_empty; + const IS_STATIC: bool = #is_static; fn builder() -> Self::Builder { #name::builder() } diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs index 7959e0f44..e30447548 100644 --- a/packages/core/examples/borrowed.rs +++ b/packages/core/examples/borrowed.rs @@ -67,6 +67,7 @@ impl PartialEq for ChildProps { } impl Properties for ChildProps { type Builder = (); + const IS_STATIC: bool = true; fn builder() -> Self::Builder { () } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 1759aeebc..44c4b34a8 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -66,8 +66,6 @@ impl SharedResources { // elements are super cheap - the value takes no space let raw_elements = Slab::with_capacity(2000); - // let pending_events = Rc::new(RefCell::new(Vec::new())); - let (sender, receiver) = futures_channel::mpsc::unbounded(); let heuristics = HeuristicsEngine::new(); @@ -106,8 +104,8 @@ impl SharedResources { } /// this is unsafe because the caller needs to track which other scopes it's already using - pub unsafe fn get_scope(&self, idx: ScopeId) -> Option<&Scope> { - let inner = &*self.components.get(); + pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> { + let inner = unsafe { &*self.components.get() }; inner.get(idx.0) } @@ -167,6 +165,19 @@ impl SharedResources { 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(); + + EventKey { + height, + originator: trigger.originator, + priority: trigger.priority, + } + } } pub struct TaskHandle {} diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index 754fa5498..ee25bd5bd 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -41,7 +41,7 @@ use crate::innerlude::{Context, DomTree, LazyNodes, FC}; /// ``` pub trait Properties: Sized { type Builder; - const IS_STATIC: bool = false; + const IS_STATIC: bool; fn builder() -> Self::Builder; /// Memoization can only happen if the props are 'static diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 475bc530e..e3a8003bf 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -69,43 +69,13 @@ use futures_util::Future; use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use std::{any::Any, cell::Cell, cmp::Ordering, pin::Pin}; +use std::{any::Any, cell::Cell, cmp::Ordering, marker::PhantomData, pin::Pin}; use DomEdit::*; -/// Instead of having handles directly over nodes, Dioxus uses simple u32 as node IDs. -/// The expectation is that the underlying renderer will mainain their Nodes in vec where the ids are the index. This allows -/// for a form of passive garbage collection where nodes aren't immedately cleaned up. -/// -/// The "RealDom" abstracts over the... real dom. The RealDom trait assumes that the renderer maintains a stack of real -/// nodes as the diffing algorithm descenes through the tree. This means that whatever is on top of the stack will receive -/// any modifications that follow. This technique enables the diffing algorithm to avoid directly handling or storing any -/// target-specific Node type as well as easily serializing the edits to be sent over a network or IPC connection. -pub trait RealDom { - fn raw_node_as_any(&self) -> &mut dyn Any; - - /// Essentially "are we out of time to do more work?" - /// Right now, defaults to "no" giving us unlimited time to process work. - /// This will lead to blocking behavior in the UI, so implementors will want to override this. - fn must_commit(&self) -> bool { - false - } - - fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} - - // pause the virtualdom until the main loop is ready to process more work - fn wait_until_ready<'s>(&'s mut self) -> Pin + 's>> { - // - Box::pin(async { - // - }) - } -} - pub struct DiffMachine<'r, 'bump> { - // pub real_dom: &'real dyn RealDom, pub vdom: &'bump SharedResources, - pub edits: &'r mut Vec>, + pub edits: Mutations<'bump>, pub scope_stack: SmallVec<[ScopeId; 5]>, @@ -114,11 +84,13 @@ pub struct DiffMachine<'r, 'bump> { // 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> { pub(crate) fn new( - edits: &'r mut Vec>, + edits: Mutations<'bump>, cur_scope: ScopeId, shared: &'bump SharedResources, ) -> Self { @@ -128,6 +100,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { vdom: shared, diffed: FxHashSet::default(), seen_scopes: FxHashSet::default(), + _r: PhantomData, } } @@ -136,15 +109,16 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { /// /// This will PANIC if any component elements are passed in. pub fn new_headless( - edits: &'r mut Vec>, + // edits: &'r mut Vec>, shared: &'bump SharedResources, ) -> Self { Self { - edits, + edits: Mutations { edits: Vec::new() }, scope_stack: smallvec![ScopeId(0)], vdom: shared, diffed: FxHashSet::default(), seen_scopes: FxHashSet::default(), + _r: PhantomData, } } @@ -211,13 +185,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); + please_commit(&mut self.edits.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); + please_commit(&mut self.edits.edits); for attribute in old.attributes { self.edit_remove_attribute(attribute); } @@ -238,7 +212,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); + please_commit(&mut self.edits.edits); self.edit_remove_event_listener(old_l.event); self.edit_new_event_listener(new_l, cur_scope); } @@ -246,7 +220,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { self.fix_listener(new_l); } } else { - please_commit(&mut self.edits); + please_commit(&mut self.edits.edits); for listener in old.listeners { self.edit_remove_event_listener(listener.event); } @@ -421,6 +395,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { 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)); @@ -464,7 +439,6 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { } VNodeKind::Component(vcomponent) => { - log::debug!("Mounting a new component"); let caller = vcomponent.caller.clone(); let parent_idx = self.scope_stack.last().unwrap().clone(); @@ -486,9 +460,17 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // 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: - // Noderefs - // Effects + // add noderefs to current noderef list Noderefs + // add effects to current effect list Effects let new_component = self.get_scope_mut(&new_idx).unwrap(); @@ -501,9 +483,6 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // 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"); - - // new_component.frames.head - // self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; } } @@ -1367,42 +1346,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.push(PushRoot { id }); + self.edits.edits.push(PushRoot { id }); } pub(crate) fn edit_pop(&mut self) { - self.edits.push(PopRoot {}); + self.edits.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.push(AppendChildren { many }); + self.edits.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.push(ReplaceWith { n, m }); + self.edits.edits.push(ReplaceWith { n, m }); } pub(crate) fn edit_insert_after(&mut self, n: u32) { - self.edits.push(InsertAfter { n }); + self.edits.edits.push(InsertAfter { n }); } pub(crate) fn edit_insert_before(&mut self, n: u32) { - self.edits.push(InsertBefore { n }); + self.edits.edits.push(InsertBefore { n }); } // Remove Nodesfrom the dom pub(crate) fn edit_remove(&mut self) { - self.edits.push(Remove); + self.edits.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.push(CreateTextNode { text, id }); + self.edits.edits.push(CreateTextNode { text, id }); } pub(crate) fn edit_create_element( @@ -1413,15 +1392,15 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { ) { let id = id.as_u64(); match ns { - Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }), - None => self.edits.push(CreateElement { id, tag }), + Some(ns) => self.edits.edits.push(CreateElementNs { id, ns, tag }), + None => self.edits.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.push(CreatePlaceholder { id }); + self.edits.edits.push(CreatePlaceholder { id }); } // events @@ -1434,7 +1413,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { let element_id = mounted_node.get().unwrap().as_u64(); - self.edits.push(NewEventListener { + self.edits.edits.push(NewEventListener { scope, event_name: event, mounted_node_id: element_id, @@ -1442,12 +1421,12 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { } pub(crate) fn edit_remove_event_listener(&mut self, event: &'static str) { - self.edits.push(RemoveEventListener { event }); + self.edits.edits.push(RemoveEventListener { event }); } // modify pub(crate) fn edit_set_text(&mut self, text: &'bump str) { - self.edits.push(SetText { text }); + self.edits.edits.push(SetText { text }); } pub(crate) fn edit_set_attribute(&mut self, attribute: &'bump Attribute) { @@ -1461,7 +1440,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // field: &'static str, // value: &'bump str, // ns: Option<&'static str>, - self.edits.push(SetAttribute { + self.edits.edits.push(SetAttribute { field: name, value, ns: *namespace, @@ -1484,7 +1463,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // field: &'static str, // value: &'bump str, // ns: Option<&'static str>, - self.edits.push(SetAttribute { + self.edits.edits.push(SetAttribute { field: name, value, ns: Some(namespace), @@ -1493,7 +1472,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { let name = attribute.name; - self.edits.push(RemoveAttribute { name }); + self.edits.edits.push(RemoveAttribute { name }); } } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 17ec98d81..6804a9f95 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -28,21 +28,15 @@ pub struct EventTrigger { /// The priority of the event pub priority: EventPriority, } -impl EventTrigger { - pub fn make_key(&self) -> EventKey { - EventKey { - originator: self.originator, - priority: self.priority, - } - } -} -#[derive(PartialEq, Eq)] +#[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 } @@ -117,9 +111,6 @@ pub enum VirtualEvent { /// be processed. GarbageCollection, - /// - DiffComponent, - /// A type of "immediate" event scheduled by components ScheduledUpdate { height: u32, @@ -135,9 +126,11 @@ pub enum VirtualEvent { // 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 {}, + AsyncEvent { + should_rerender: bool, + }, - // Suspense events are a type of async event + // Suspense events are a type of async event generated when suspended nodes are ready to be processed. // // they have the lowest priority SuspenseEvent { @@ -188,7 +181,6 @@ impl std::fmt::Debug for VirtualEvent { VirtualEvent::ScheduledUpdate { .. } => "SetStateEvent", VirtualEvent::AsyncEvent { .. } => "AsyncEvent", VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent", - VirtualEvent::DiffComponent { .. } => "DiffComponent", }; f.debug_struct("VirtualEvent").field("type", &name).finish() diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 4fef6aa1c..538e2ae18 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -127,7 +127,9 @@ where *slot.as_ref().borrow_mut() = Some(output); updater(update_id); EventTrigger { - event: VirtualEvent::AsyncEvent {}, + event: VirtualEvent::AsyncEvent { + should_rerender: false, + }, originator, priority: EventPriority::Low, real_node_id: None, diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 34506c4e0..c627496b2 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, RealDom, ScopeId, SuspendedContext, VNode, - VNodeKind, VirtualDom, VirtualEvent, FC, + EventTrigger, LazyNodes, NodeFactory, Properties, ScopeId, SuspendedContext, VNode, VNodeKind, + VirtualDom, VirtualEvent, FC, }; pub mod prelude { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 49958d905..fae6c31d4 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -152,15 +152,16 @@ pub struct VComponent<'src> { pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>, - pub(crate) drop_props: Option<&'src dyn FnOnce()>, + pub(crate) drop_props: RefCell>>, pub is_static: bool, + pub can_memoize: bool, + // a pointer into the bump arena (given by the 'src lifetime) pub(crate) raw_props: *const (), // a pointer to the raw fn typ - // pub(crate) user_fc: BumpB\, pub(crate) user_fc: *const (), } @@ -365,20 +366,19 @@ impl<'a> NodeFactory<'a> { P: Properties + 'a, V: 'a + AsRef<[VNode<'a>]>, { - // TODO - // It's somewhat wrong to go about props like this + let bump = self.bump(); // 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 = self.bump().alloc(children); + let children: &'a V = bump.alloc(children); let children = children.as_ref(); - let props = self.bump().alloc(props); + let props = bump.alloc(props); let raw_props = props as *mut P as *mut (); let user_fc = component as *const (); - let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(self.bump().alloc_with(|| { + let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(bump.alloc_with(|| { move |other: &VComponent| { if user_fc == other.user_fc { // Safety @@ -404,13 +404,19 @@ impl<'a> NodeFactory<'a> { })); // create a closure to drop the props - let drop_props: Option<&dyn FnOnce()> = Some(self.bump().alloc_with(|| { + let mut has_dropped = false; + let drop_props: &mut dyn FnMut() = bump.alloc_with(|| { move || unsafe { - let real_other = raw_props as *mut _ as *mut P; - let b = BumpBox::from_raw(real_other); - std::mem::drop(b); + 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 is_static = children.len() == 0 && P::IS_STATIC && key.is_none(); @@ -418,14 +424,15 @@ impl<'a> NodeFactory<'a> { VNode { key, - kind: VNodeKind::Component(self.bump().alloc_with(|| VComponent { + 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, + drop_props: RefCell::new(Some(drop_props)), + can_memoize: P::IS_STATIC, ass_scope: Cell::new(None), })), } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index b303170f3..951b7280b 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -37,6 +37,7 @@ pub struct Scope { // Listeners pub(crate) listeners: RefCell>>, + pub(crate) borrowed_props: RefCell>>, // State pub(crate) hooks: HookList, @@ -94,6 +95,7 @@ impl Scope { hooks: Default::default(), shared_contexts: Default::default(), listeners: Default::default(), + borrowed_props: Default::default(), descendents: Default::default(), pending_garbage: Default::default(), } @@ -114,27 +116,8 @@ impl Scope { // 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 - if !self.pending_garbage.borrow().is_empty() { - // We have some garbage to clean up - log::error!("Cleaning up garabge"); - panic!("cannot run scope while garbage is pending! Please clean up your mess first"); - } - log::debug!("reset okay"); - - // make sure we call the drop implementation on all the listeners - // this is important to not leak memory - self.listeners - .borrow_mut() - .drain(..) - .map(|li| unsafe { &*li }) - .for_each(|listener| { - listener.callback.borrow_mut().take(); - }); - - // 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 + self.ensure_drop_safety(); // Safety: // - We dropped the listeners, so no more &mut T can be used while these are held @@ -164,6 +147,51 @@ impl Scope { } } + /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from + /// causuing UB in our tree. + /// + /// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components + /// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove + /// any possible references to the data in the hook list. + /// + /// 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) { + // make sure all garabge is collected before trying to proceed with anything else + debug_assert!( + self.pending_garbage.borrow().is_empty(), + "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(); + + // 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 // calling listeners will invalidate the list of listeners // The listener list will be completely drained because the next frame will write over previous listeners diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 33a6d9fb2..0add0f456 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -26,16 +26,3 @@ pub fn empty_cell() -> Cell> { // Some(self.cmp(other)) // } // } - -pub struct DebugDom {} -impl DebugDom { - pub fn new() -> Self { - Self {} - } -} - -impl RealDom for DebugDom { - fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - todo!() - } -} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 335b5f8c1..090282728 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -29,7 +29,7 @@ use std::any::Any; use std::any::TypeId; use std::cell::{Ref, RefCell, RefMut}; -use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet}; use std::pin::Pin; /// An integrated virtual node system that progresses events and diffs UI trees. @@ -53,6 +53,8 @@ pub struct VirtualDom { /// Should always be the first (gen=0, id=0) pub base_scope: ScopeId, + active_fibers: Vec>, + pending_events: BTreeMap, // for managing the props that were used to create the dom @@ -144,6 +146,7 @@ impl VirtualDom { base_scope, _root_props: root_props, shared: components, + active_fibers: Vec::new(), pending_events: BTreeMap::new(), _root_prop_type: TypeId::of::

(), } @@ -195,8 +198,10 @@ 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, edits: &'_ mut Vec>) -> Result<()> { - let mut diff_machine = DiffMachine::new(edits, self.base_scope, &self.shared); + 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 cur_component = diff_machine .get_scope_mut(&self.base_scope) @@ -214,7 +219,7 @@ impl VirtualDom { ); } - Ok(()) + Ok(edits) } async fn select_next_event(&mut self) -> Option { @@ -222,8 +227,9 @@ impl VirtualDom { // 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!("scooping event from receiver"); - self.pending_events.insert(trigger.make_key(), trigger); + 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() { @@ -242,83 +248,172 @@ impl VirtualDom { futures_util::future::Either::Right((trigger, _)) => trigger, } .unwrap(); - self.pending_events.insert(trigger.make_key(), trigger); + let key = self.shared.make_trigger_key(&trigger); + self.pending_events.insert(key, trigger); } - todo!() + // pop the most important event off + let key = self.pending_events.keys().next().unwrap().clone(); + let trigger = self.pending_events.remove(&key).unwrap(); - // let trigger = self.select_next_event().unwrap(); - // trigger + Some(trigger) } - // the cooperartive, fiber-based scheduler - // is "awaited" and will always return some edits for the real dom to apply - // - // Right now, we'll happily partially render component trees - // - // Normally, you would descend through the tree depth-first, but we actually descend breadth-first. - pub async fn run(&mut self, realdom: &mut dyn RealDom) -> Result<()> { - let cur_component = self.base_scope; - let mut edits = Vec::new(); - let resources = self.shared.clone(); + /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. + /// + /// This method will not wait for any suspended tasks, completely skipping over + pub fn run_immediate<'s>(&'s mut self) -> Result> { + // - let mut diff_machine = DiffMachine::new(&mut edits, cur_component, &resources); + todo!() + } + + /// 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) -> Result> { + self.run_with_deadline(|| false).await + } + + /// Run the virtualdom with a time limit. + /// + /// 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 + /// exhaust the deadline working on them. + /// + /// 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. + /// + /// 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. + /// + /// # Example + /// + /// ```no_run + /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!( div {"hello"} ))); + /// loop { + /// let started = std::time::Instant::now(); + /// let deadline = move || std::time::Instant::now() - started > std::time::Duration::from_millis(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, + ) -> Result> { + let cur_component = self.base_scope; + + let mut mutations = Mutations { edits: Vec::new() }; + + let mut diff_machine = DiffMachine::new(mutations, cur_component, &self.shared); + + let must_be_re_rendered = HashSet::::new(); + + let mut receiver = self.shared.task_receiver.borrow_mut(); + + // loop { - let trigger = self.select_next_event().await.unwrap(); + if deadline_exceeded() { + break; + } + + /* + 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. + 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 + 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) + + + + + The user of this method will loop over "run", waiting for edits and then committing them IE + + + task::spawn_local(async { + let vdom = VirtualDom::new(App); + loop { + let deadline = wait_for_idle().await; + let mutations = vdom.run_with_deadline(deadline); + realdom.apply_edits(mutations.edits); + realdom.apply_refs(mutations.refs); + } + }); + + + let vdom = VirtualDom::new(App); + let realdom = WebsysDom::new(App); + loop { + let deadline = wait_for_idle().await; + let mutations = vdom.run_with_deadline(deadline); + + realdom.apply_edits(mutations.edits); + realdom.apply_refs(mutations.refs); + } + + + + ``` + task::spawn_local(async move { + let vdom = VirtualDom::new(App); + loop { + let mutations = vdom.run_with_deadline(16); + realdom.apply_edits(mutations.edits)?; + realdom.apply_refs(mutations.refs)?; + } + }); + + event_loop.run(move |event, _, flow| { + + }); + ``` + */ + let mut receiver = self.shared.task_receiver.borrow_mut(); + + // match receiver.try_next() {} + + let trigger = receiver.next().await.unwrap(); match &trigger.event { - // Collecting garabge is not currently interruptible. + // If any user event is received, then we run the listener and let it dump "needs updates" into the queue // - // 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 - // TODO: allow interruption here - if !realdom.must_commit() { - 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); - } - } + 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(_) => { + let scope_id = &trigger.originator; + let scope = unsafe { self.shared.get_scope_mut(*scope_id) }; + match scope { + Some(scope) => { + scope.call_listener(trigger)?; + } + None => { + log::warn!("No scope found for event: {:#?}", scope_id); } - } - - for scope in scopes_to_kill { - // oy kill em - log::debug!("should be removing scope {:#?}", scope); } } @@ -363,74 +458,93 @@ impl VirtualDom { } } + // 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); + } + } + // Run the component VirtualEvent::ScheduledUpdate { height: u32 } => { let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); match scope.run_scope() { Ok(_) => { - let event = VirtualEvent::DiffComponent; - let trigger = EventTrigger { - event, - originator: trigger.originator, - priority: EventPriority::High, - real_node_id: None, - }; - self.shared.task_sender.unbounded_send(trigger); + todo!(); + // let event = VirtualEvent::DiffComponent; + // let trigger = EventTrigger { + // event, + // originator: trigger.originator, + // priority: EventPriority::High, + // real_node_id: None, + // }; + // self.shared.task_sender.unbounded_send(trigger); } Err(_) => { log::error!("failed to run this component!"); } } } - - VirtualEvent::DiffComponent => { - diff_machine.diff_scope(trigger.originator)?; - } - - // Process any user-facing events just by calling their listeners - // This performs no actual work - but the listeners themselves will cause insert new work - 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(_) => { - let scope_id = &trigger.originator; - let scope = unsafe { self.shared.get_scope_mut(*scope_id) }; - match scope { - Some(scope) => scope.call_listener(trigger)?, - None => { - log::warn!("No scope found for event: {:#?}", scope_id); - } - } - } } - if realdom.must_commit() || self.pending_events.is_empty() { - realdom.commit_edits(&mut diff_machine.edits); - realdom.wait_until_ready().await; - } + // // + // if realdom.must_commit() { + // // commit these edits and then wait for the next idle period + // realdom.commit_edits(&mut diff_machine.edits).await; + // } } - Ok(()) + Ok(diff_machine.edits) } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - todo!() - // std::rc::Rc::new(move |_| { - // // - // }) - // todo() + self.shared.task_sender.clone() } } @@ -439,8 +553,38 @@ impl VirtualDom { unsafe impl Sync for VirtualDom {} unsafe impl Send for VirtualDom {} -fn select_next_event( - pending_events: &mut BTreeMap, -) -> Option { - None +struct Fiber<'a> { + trigger: EventTrigger, + + // 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(trigger: EventTrigger) -> Self { + Self { + trigger, + 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>, } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 8f7049431..97c794b6e 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -11,7 +11,6 @@ use dioxus::{ arena::SharedResources, diff::{CreateMeta, DiffMachine}, prelude::*, - util::DebugDom, DomEdit, }; use dioxus_core as dioxus; diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs new file mode 100644 index 000000000..cb593dee7 --- /dev/null +++ b/packages/core/tests/eventsystem.rs @@ -0,0 +1,32 @@ +use bumpalo::Bump; + +use anyhow::{Context, Result}; +use dioxus::{ + arena::SharedResources, + diff::{CreateMeta, DiffMachine}, + prelude::*, + DomEdit, +}; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; + +#[async_std::test] +async fn event_queue_works() { + static App: FC<()> = |cx| { + cx.render(rsx! { + div { "hello world" } + }) + }; + + let mut dom = VirtualDom::new(App); + let edits = dom.rebuild().unwrap(); + + async_std::task::spawn_local(async move { + match dom.run_unbounded().await { + Err(_) => todo!(), + Ok(mutations) => { + // + } + } + }); +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 108883991..c9c7153b4 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -88,26 +88,26 @@ impl WebviewRenderer { let channel = vir.get_event_sender(); struct WebviewBridge {} - impl RealDom for WebviewBridge { - fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - todo!() - } + // impl RealDom for WebviewBridge { + // fn raw_node_as_any(&self) -> &mut dyn std::any::Any { + // todo!() + // } - fn must_commit(&self) -> bool { - false - } + // fn must_commit(&self) -> bool { + // false + // } - fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} + // fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} - fn wait_until_ready<'s>( - &'s mut self, - ) -> std::pin::Pin + 's>> { - // - Box::pin(async { - // - }) - } - } + // fn wait_until_ready<'s>( + // &'s mut self, + // ) -> std::pin::Pin + 's>> { + // // + // Box::pin(async { + // // + // }) + // } + // } let mut real_dom = WebviewBridge {}; // async_std::task::spawn_local(vir.run(&mut real_dom)); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index d7cf7b653..aa941cd75 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -96,7 +96,7 @@ pub async fn run_with_props( Rc::new(move |event| tasks.unbounded_send(event).unwrap()), ); - dom.run(&mut websys_dom).await?; + dom.run_with_deadline(&mut websys_dom).await?; Ok(()) }