diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index ac1c66189..b4facf39f 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -28,7 +28,6 @@ impl ActiveFrame { head_node: VNode::Fragment(VFragment { key: None, children: &[], - is_static: false, }), bump: b1, @@ -39,7 +38,6 @@ impl ActiveFrame { head_node: VNode::Fragment(VFragment { key: None, children: &[], - is_static: false, }), bump: b2, diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs index 0dc047caf..b7b6a70ea 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/childiter.rs @@ -62,7 +62,7 @@ impl<'a> Iterator for RealChildIterator<'a> { VNode::Component(sc) => { let scope = self .scopes - .get_scope(sc.associated_scope.get().unwrap()) + .get_scope(&sc.associated_scope.get().unwrap()) .unwrap(); // Simply swap the current node on the stack with the root of the component diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index e249bfae1..8bcc8bd35 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -5,10 +5,7 @@ //! if the type supports PartialEq. The Properties trait is used by the rsx! and html! macros to generate the type-safe builder //! that ensures compile-time required and optional fields on cx. -use crate::{ - innerlude::{Context, Element, VAnchor, VFragment, VNode}, - LazyNodes, ScopeChildren, -}; +use crate::innerlude::{Context, Element, LazyNodes, ScopeChildren}; /// A component is a wrapper around a Context and some Props that share a lifetime /// /// @@ -51,7 +48,7 @@ pub struct FragmentBuilder<'a, const BUILT: bool> { children: Option>, } impl<'a> FragmentBuilder<'a, false> { - pub fn children(mut self, children: ScopeChildren<'a>) -> FragmentBuilder<'a, true> { + pub fn children(self, children: ScopeChildren<'a>) -> FragmentBuilder<'a, true> { FragmentBuilder { children: Some(children), } @@ -75,7 +72,7 @@ impl<'a> Properties for FragmentProps<'a> { FragmentBuilder { children: None } } - unsafe fn memoize(&self, other: &Self) -> bool { + unsafe fn memoize(&self, _other: &Self) -> bool { false } } diff --git a/packages/core/src/coroutines.rs b/packages/core/src/coroutines.rs deleted file mode 100644 index 490a0acab..000000000 --- a/packages/core/src/coroutines.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Coroutines are just a "futures unordered" buffer for tasks that can be submitted through the use_coroutine hook. -//! -//! The idea here is to move *coroutine* support as a layer on top of *tasks* - -use futures_util::{stream::FuturesUnordered, Future}; - -pub struct CoroutineScheduler { - futures: FuturesUnordered>>, -} - -impl CoroutineScheduler { - pub fn new() -> Self { - CoroutineScheduler { - futures: FuturesUnordered::new(), - } - } -} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 654048267..f59d89490 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -169,7 +169,7 @@ impl<'bump> DiffMachine<'bump> { } pub fn diff_scope(&mut self, id: ScopeId) { - if let Some(component) = self.vdom.get_scope_mut(id) { + 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::Diff { new, old }); self.work(|| false); @@ -189,7 +189,10 @@ impl<'bump> DiffMachine<'bump> { DiffInstruction::Diff { old, new } => self.diff_node(old, new), DiffInstruction::Create { node } => self.create_node(node), DiffInstruction::Mount { and } => self.mount(and), - DiffInstruction::PrepareMove { node } => self.prepare_move_node(node), + DiffInstruction::PrepareMove { node } => { + let num_on_stack = self.push_all_nodes(node); + self.stack.add_child_count(num_on_stack); + } DiffInstruction::PopScope => self.stack.pop_off_scope(), }; @@ -202,10 +205,29 @@ impl<'bump> DiffMachine<'bump> { true } - fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) { - for el in RealChildIterator::new(node, self.vdom) { - self.mutations.push_root(el.mounted_id()); - self.stack.add_child_count(1); + // recursively push all the nodes of a tree onto the stack and return how many are there + fn push_all_nodes(&mut self, node: &'bump VNode<'bump>) -> usize { + match node { + VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => { + self.mutations.push_root(node.mounted_id()); + 1 + } + + VNode::Fragment(_) | VNode::Component(_) => node + .children() + .iter() + .map(|child| self.push_all_nodes(child)) + .sum(), + + VNode::Element(el) => { + let mut num_on_stack = 0; + for child in el.children.iter() { + num_on_stack += self.push_all_nodes(child); + } + self.mutations.push_root(el.dom_id.get().unwrap()); + + num_on_stack + 1 + } } } @@ -307,7 +329,7 @@ impl<'bump> DiffMachine<'bump> { self.stack.add_child_count(1); if let Some(cur_scope_id) = self.stack.current_scope() { - let scope = self.vdom.get_scope(cur_scope_id).unwrap(); + let scope = self.vdom.get_scope_mut(&cur_scope_id).unwrap(); listeners.iter().for_each(|listener| { self.attach_listener_to_scope(listener, scope); @@ -332,18 +354,18 @@ impl<'bump> DiffMachine<'bump> { } fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { - let caller = vcomponent.caller; + // let caller = vcomponent.caller; let parent_idx = self.stack.current_scope().unwrap(); 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 parent_scope = self.vdom.get_scope(&parent_idx).unwrap(); let new_idx = self.vdom.insert_scope_with_key(|new_idx| { ScopeInner::new( - caller, + vcomponent, new_idx, Some(parent_idx), parent_scope.height + 1, @@ -356,17 +378,18 @@ impl<'bump> DiffMachine<'bump> { vcomponent.associated_scope.set(Some(new_idx)); if !vcomponent.can_memoize { - let cur_scope = self.vdom.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); + + cur_scope.items.get_mut().borrowed_props.push(extended); } // TODO: // add noderefs to current noderef list Noderefs // add effects to current effect list Effects - let new_component = self.vdom.get_scope_mut(new_idx).unwrap(); + let new_component = self.vdom.get_scope_mut(&new_idx).unwrap(); log::debug!( "initializing component {:?} with height {:?}", @@ -494,7 +517,7 @@ impl<'bump> DiffMachine<'bump> { // // TODO: take a more efficient path than this if let Some(cur_scope_id) = self.stack.current_scope() { - let scope = self.vdom.get_scope(cur_scope_id).unwrap(); + let scope = self.vdom.get_scope_mut(&cur_scope_id).unwrap(); if old.listeners.len() == new.listeners.len() { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { @@ -549,19 +572,21 @@ impl<'bump> DiffMachine<'bump> { 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(); - scope.update_scope_dependencies(new.caller); + let scope = self.vdom.get_scope_mut(&scope_addr).unwrap(); + scope.update_vcomp(new); // React doesn't automatically memoize, but we do. - let props_are_the_same = old.comparator.unwrap(); + let props_are_the_same = todo!("reworking component memoization"); + // let props_are_the_same = todo!("reworking component memoization"); + // let props_are_the_same = old.comparator.unwrap(); - if self.cfg.force_diff || !props_are_the_same(new) { - let succeeded = scope.run_scope(self.vdom); + // if self.cfg.force_diff || !props_are_the_same(new) { + // let succeeded = scope.run_scope(self.vdom); - if succeeded { - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); - } - } + // if succeeded { + // self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); + // } + // } self.stack.scope_stack.pop(); } else { @@ -1038,7 +1063,7 @@ impl<'bump> DiffMachine<'bump> { } VNode::Component(el) => { let scope_id = el.associated_scope.get().unwrap(); - let scope = self.vdom.get_scope(scope_id).unwrap(); + let scope = self.vdom.get_scope(&scope_id).unwrap(); search_node = Some(scope.root_node()); } } @@ -1056,7 +1081,7 @@ impl<'bump> DiffMachine<'bump> { } VNode::Component(el) => { let scope_id = el.associated_scope.get().unwrap(); - let scope = self.vdom.get_scope(scope_id).unwrap(); + let scope = self.vdom.get_scope(&scope_id).unwrap(); search_node = Some(scope.root_node()); } VNode::Text(t) => break t.dom_id.get(), @@ -1131,12 +1156,12 @@ impl<'bump> DiffMachine<'bump> { VNode::Component(c) => { let scope_id = c.associated_scope.get().unwrap(); - let scope = self.vdom.get_scope_mut(scope_id).unwrap(); + let scope = self.vdom.get_scope_mut(&scope_id).unwrap(); let root = scope.root_node(); self.remove_nodes(Some(root), gen_muts); log::debug!("Destroying scope {:?}", scope_id); - let mut s = self.vdom.try_remove(scope_id).unwrap(); + let mut s = self.vdom.try_remove(&scope_id).unwrap(); s.hooks.clear_hooks(); } } @@ -1161,23 +1186,27 @@ impl<'bump> DiffMachine<'bump> { } /// Adds a listener closure to a scope during diff. - fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &ScopeInner) { - let mut queue = scope.listeners.borrow_mut(); + fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &mut ScopeInner) { let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; - queue.push(long_listener as *const _) + scope + .items + .get_mut() + .listeners + .push(long_listener as *const _) } fn attach_suspended_node_to_scope(&mut self, suspended: &'bump VSuspended) { if let Some(scope) = self .stack .current_scope() - .and_then(|id| self.vdom.get_scope_mut(id)) + .and_then(|id| self.vdom.get_scope_mut(&id)) { // safety: this lifetime is managed by the logic on scope let extended: &VSuspended<'static> = unsafe { std::mem::transmute(suspended) }; scope + .items + .get_mut() .suspended_nodes - .borrow_mut() .insert(suspended.task_id, extended as *const _); } } diff --git a/packages/core/src/heuristics.rs b/packages/core/src/heuristics.rs deleted file mode 100644 index a8069c95e..000000000 --- a/packages/core/src/heuristics.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::collections::HashMap; - -use fxhash::FxHashMap; - -use crate::FC; - -/// Provides heuristics to the "SharedResources" object for improving allocation performance. -/// -/// This heuristics engine records the memory footprint of bump arenas and hook lists for each component. These records are -/// then used later on to optimize the initial allocation for future components. This helps save large allocations later on -/// that would slow down the diffing and initialization process. -/// -/// -pub struct HeuristicsEngine { - heuristics: FxHashMap, -} - -pub type FcSlot = *const (); - -pub struct Heuristic { - hooks: u32, - bump_size: u64, -} - -impl HeuristicsEngine { - pub(crate) fn new() -> Self { - Self { - heuristics: FxHashMap::default(), - } - } - - fn recommend(&mut self, fc: FC, heuristic: Heuristic) { - let g = fc as FcSlot; - let e = self.heuristics.entry(g); - } - - fn get_recommendation(&mut self, fc: FC) -> &Heuristic { - let id = fc as FcSlot; - - self.heuristics.entry(id).or_insert(Heuristic { - bump_size: 100, - hooks: 10, - }) - } -} - -#[test] -fn types_work() { - let engine = HeuristicsEngine::new(); -} diff --git a/packages/core/src/hooklist.rs b/packages/core/src/hooklist.rs index ed4eb49dd..03bce78b4 100644 --- a/packages/core/src/hooklist.rs +++ b/packages/core/src/hooklist.rs @@ -1,10 +1,9 @@ use std::{ any::Any, - cell::{Cell, RefCell, UnsafeCell}, + cell::{Cell, RefCell}, }; -type UnsafeInnerHookState = UnsafeCell>; -type HookCleanup = Box)>; +use bumpalo::Bump; /// An abstraction over internally stored data using a hook-based memory layout. /// @@ -15,7 +14,8 @@ type HookCleanup = Box)>; /// Todo: this could use its very own bump arena, but that might be a tad overkill #[derive(Default)] pub(crate) struct HookList { - vals: RefCell>, + arena: Bump, + vals: RefCell>, idx: Cell, } @@ -23,7 +23,7 @@ impl HookList { pub(crate) fn next(&self) -> Option<&mut T> { self.vals.borrow().get(self.idx.get()).and_then(|inn| { self.idx.set(self.idx.get() + 1); - let raw_box = unsafe { &mut *inn.0.get() }; + let raw_box = unsafe { &mut **inn }; raw_box.downcast_mut::() }) } @@ -38,10 +38,9 @@ impl HookList { self.idx.set(0); } - pub(crate) fn push_hook(&self, new: T, cleanup: HookCleanup) { - self.vals - .borrow_mut() - .push((UnsafeCell::new(Box::new(new)), cleanup)) + pub(crate) fn push_hook(&self, new: T) { + let val = self.arena.alloc(new); + self.vals.borrow_mut().push(val) } pub(crate) fn len(&self) -> usize { @@ -57,10 +56,16 @@ impl HookList { } pub fn clear_hooks(&mut self) { - log::debug!("clearing hooks..."); - self.vals - .borrow_mut() - .drain(..) - .for_each(|(state, cleanup)| cleanup(state.into_inner())); + self.vals.borrow_mut().drain(..).for_each(|state| { + let as_mut = unsafe { &mut *state }; + let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) }; + drop(boxed); + }); + } + + /// Get the ammount of memory a hooklist uses + /// Used in heuristics + pub fn get_hook_arena_size(&self) -> usize { + self.arena.allocated_bytes() } } diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index b18a445c1..54a5ca5a5 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -13,73 +13,73 @@ use crate::innerlude::*; use futures_util::FutureExt; use std::{any::Any, cell::RefCell, future::Future, ops::Deref, rc::Rc}; -/// Awaits the given task, forcing the component to re-render when the value is ready. -/// -/// Returns the handle to the task and the value (if it is ready, else None). -/// -/// ``` -/// static Example: FC<()> = |(cx, props)| { -/// let (task, value) = use_task(|| async { -/// timer::sleep(Duration::from_secs(1)).await; -/// "Hello World" -/// }); -/// -/// match contents { -/// Some(contents) => rsx!(cx, div { "{title}" }), -/// None => rsx!(cx, div { "Loading..." }), -/// } -/// }; -/// ``` -pub fn use_coroutine<'src, Out, Fut, Init>( - cx: Context<'src>, - task_initializer: Init, -) -> (&'src TaskHandle, &'src Option) -where - Out: 'static, - Fut: Future + 'static, - Init: FnOnce() -> Fut + 'src, -{ - struct TaskHook { - handle: TaskHandle, - task_dump: Rc>>, - value: Option, - } +// /// Awaits the given task, forcing the component to re-render when the value is ready. +// /// +// /// Returns the handle to the task and the value (if it is ready, else None). +// /// +// /// ``` +// /// static Example: FC<()> = |(cx, props)| { +// /// let (task, value) = use_task(|| async { +// /// timer::sleep(Duration::from_secs(1)).await; +// /// "Hello World" +// /// }); +// /// +// /// match contents { +// /// Some(contents) => rsx!(cx, div { "{title}" }), +// /// None => rsx!(cx, div { "Loading..." }), +// /// } +// /// }; +// /// ``` +// pub fn use_coroutine<'src, Out, Fut, Init>( +// cx: Context<'src>, +// task_initializer: Init, +// ) -> (&'src TaskHandle, &'src Option) +// where +// Out: 'static, +// Fut: Future + 'static, +// Init: FnOnce() -> Fut + 'src, +// { +// struct TaskHook { +// handle: TaskHandle, +// task_dump: Rc>>, +// value: Option, +// } - todo!() +// todo!() - // // whenever the task is complete, save it into th - // cx.use_hook( - // move |_| { - // let task_fut = task_initializer(); +// // // whenever the task is complete, save it into th +// // cx.use_hook( +// // move |_| { +// // let task_fut = task_initializer(); - // let task_dump = Rc::new(RefCell::new(None)); +// // let task_dump = Rc::new(RefCell::new(None)); - // let slot = task_dump.clone(); +// // let slot = task_dump.clone(); - // let updater = cx.schedule_update_any(); - // let originator = cx.scope.our_arena_idx; +// // let updater = cx.schedule_update_any(); +// // 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(originator); - // originator - // }))); +// // let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move { +// // *slot.as_ref().borrow_mut() = Some(output); +// // updater(originator); +// // originator +// // }))); - // TaskHook { - // task_dump, - // value: None, - // handle, - // } - // }, - // |hook| { - // if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() { - // hook.value = Some(val); - // } - // (&hook.handle, &hook.value) - // }, - // |_| {}, - // ) -} +// // TaskHook { +// // task_dump, +// // value: None, +// // handle, +// // } +// // }, +// // |hook| { +// // if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() { +// // hook.value = Some(val); +// // } +// // (&hook.handle, &hook.value) +// // }, +// // |_| {}, +// // ) +// } /// Asynchronously render new nodes once the given future has completed. /// @@ -167,10 +167,10 @@ where // ) } -pub(crate) struct SuspenseHook { - pub handle: TaskHandle, - pub value: Rc>>>, -} +// pub(crate) struct SuspenseHook { +// pub handle: TaskHandle, +// pub value: Rc>>>, +// } #[derive(Clone, Copy)] pub struct NodeRef<'src, T: 'static>(&'src RefCell>); @@ -183,5 +183,5 @@ impl<'a, T> Deref for NodeRef<'a, T> { } pub fn use_node_ref(cx: Context) -> NodeRef { - cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f }, |_| {}) + cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f }) } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index d023b2c37..41bd45ceb 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -13,22 +13,16 @@ Navigating this crate: Some utilities */ pub(crate) mod bumpframe; -pub(crate) mod childiter; pub(crate) mod component; -pub(crate) mod coroutines; pub(crate) mod diff; pub(crate) mod diff_stack; pub(crate) mod events; -pub(crate) mod heuristics; pub(crate) mod hooklist; pub(crate) mod hooks; pub(crate) mod lazynodes; pub(crate) mod mutations; pub(crate) mod nodes; -pub(crate) mod resources; -pub(crate) mod scheduler; pub(crate) mod scope; -pub(crate) mod tasks; pub(crate) mod test_dom; pub(crate) mod threadsafe; pub(crate) mod util; @@ -39,21 +33,16 @@ pub mod debug_dom; pub(crate) mod innerlude { pub(crate) use crate::bumpframe::*; - pub(crate) use crate::childiter::*; pub use crate::component::*; pub(crate) use crate::diff::*; pub use crate::diff_stack::*; pub use crate::events::*; - pub use crate::heuristics::*; pub(crate) use crate::hooklist::*; pub use crate::hooks::*; pub use crate::lazynodes::*; pub use crate::mutations::*; pub use crate::nodes::*; - pub(crate) use crate::resources::*; - pub use crate::scheduler::*; pub use crate::scope::*; - pub use crate::tasks::*; pub use crate::test_dom::*; pub use crate::threadsafe::*; pub use crate::util::*; @@ -65,8 +54,8 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, LazyNodes, MountType, - Mutations, NodeFactory, Properties, SchedulerMsg, ScopeChildren, ScopeId, TaskHandle, TestDom, - ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC, + Mutations, NodeFactory, Properties, ScopeChildren, ScopeId, TestDom, ThreadsafeVirtualDom, + UserEvent, VNode, VirtualDom, FC, }; pub mod prelude { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1cb06a4d6..fc7b7f76e 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -154,6 +154,14 @@ impl<'src> VNode<'src> { } } + pub fn children(&self) -> &[VNode<'src>] { + match &self { + VNode::Fragment(f) => &f.children, + VNode::Component(c) => &c.children, + _ => &[], + } + } + // Create an "owned" version of the vnode. pub fn decouple(&self) -> VNode<'src> { match self { @@ -165,7 +173,6 @@ impl<'src> VNode<'src> { VNode::Fragment(f) => VNode::Fragment(VFragment { children: f.children, key: f.key, - is_static: f.is_static, }), } } @@ -209,8 +216,6 @@ pub struct VFragment<'src> { pub key: Option<&'src str>, pub children: &'src [VNode<'src>], - - pub is_static: bool, } /// An element like a "div" with children, listeners, and attributes. @@ -316,21 +321,21 @@ 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: BumpBox<'src, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> + 'src>, - - pub(crate) comparator: Option bool + 'src>>, - - pub(crate) drop_props: RefCell>>, - pub(crate) can_memoize: bool, // Raw pointer into the bump arena for the props of the component pub(crate) raw_props: *const (), + + // during the "teardown" process we'll take the caller out so it can be dropped properly + pub(crate) caller: Option>, + pub(crate) comparator: Option bool + 'src>>, +} + +pub enum VCompCaller<'src> { + Borrowed(BumpBox<'src, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> + 'src>), + Owned(Box Fn(&'b ScopeInner) -> Element<'b>>), } pub struct VSuspended<'a> { @@ -473,7 +478,7 @@ impl<'a> NodeFactory<'a> { } } - pub fn component

( + pub fn component( &self, component: fn(Scope<'a, P>) -> Element<'a>, props: P, @@ -482,85 +487,109 @@ impl<'a> NodeFactory<'a> { where P: Properties + 'a, { + /* + our strategy: + - unsafe hackery + - lol + + - we don't want to hit the global allocator + - allocate into our bump arena + - if the props aren't static, then we convert them into a box which we pass off between renders + */ + let bump = self.bump(); - let props = bump.alloc(props); - let raw_props = props as *mut P as *mut (); + + // let p = BumpBox::new_in(x, a) + + // the best place to allocate the props are the other component's arena + // the second best place is the global allocator + + // // if the props are static + // let boxed = if P::IS_STATIC { + // todo!() + // } else { + // todo!() + // } + + // let caller = Box::new(|f: &ScopeInner| -> Element { + // // + // component((f, &props)) + // }); + let user_fc = component as *const (); - let comparator: &mut dyn Fn(&VComponent) -> bool = bump.alloc_with(|| { - move |other: &VComponent| { - if user_fc == other.user_fc { - // Safety - // - We guarantee that FC

is the same by function pointer - // - Because FC

is the same, then P must be the same (even with generics) - // - Non-static P are autoderived to memoize as false - // - This comparator is only called on a corresponding set of bumpframes - let props_memoized = unsafe { - let real_other: &P = &*(other.raw_props as *const _ as *const P); - props.memoize(real_other) - }; + // let comparator: &mut dyn Fn(&VComponent) -> bool = bump.alloc_with(|| { + // move |other: &VComponent| { + // if user_fc == other.user_fc { + // // Safety + // // - We guarantee that FC

is the same by function pointer + // // - Because FC

is the same, then P must be the same (even with generics) + // // - Non-static P are autoderived to memoize as false + // // - This comparator is only called on a corresponding set of bumpframes + // let props_memoized = unsafe { + // let real_other: &P = &*(other.raw_props as *const _ as *const P); + // props.memoize(real_other) + // }; - // It's only okay to memoize if there are no children and the props can be memoized - // Implementing memoize is unsafe and done automatically with the props trait - props_memoized - } else { - false - } - } - }); - let comparator = Some(unsafe { BumpBox::from_raw(comparator) }); - - let drop_props = { - // create a closure to drop the props - let mut has_dropped = false; - - let drop_props: &mut dyn FnMut() = bump.alloc_with(|| { - move || unsafe { - log::debug!("dropping props!"); - 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; - } else { - panic!("Drop props called twice - this is an internal failure of Dioxus"); - } - } - }); - - let drop_props = unsafe { BumpBox::from_raw(drop_props) }; - - RefCell::new(Some(drop_props)) - }; + // // It's only okay to memoize if there are no children and the props can be memoized + // // Implementing memoize is unsafe and done automatically with the props trait + // props_memoized + // } else { + // false + // } + // } + // }); + // let comparator = Some(unsafe { BumpBox::from_raw(comparator) }); let key = key.map(|f| self.raw_text(f).0); - let caller: &'a mut dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = - bump.alloc(move |scope: &ScopeInner| -> Element { - log::debug!("calling component renderr {:?}", scope.our_arena_idx); - let props: &'_ P = unsafe { &*(raw_props as *const P) }; + let caller = match P::IS_STATIC { + true => { + // it just makes sense to box the props + let boxed_props: Box

= Box::new(props); + let props_we_know_are_static = todo!(); + VCompCaller::Owned(Box::new(|f| { + // - let scp: &'a ScopeInner = unsafe { std::mem::transmute(scope) }; - let s: Scope<'a, P> = (scp, props); + let p = todo!(); - let res: Element = component(s); - unsafe { std::mem::transmute(res) } - }); + todo!() + })) + } + false => VCompCaller::Borrowed({ + // - let caller = unsafe { BumpBox::from_raw(caller) }; + todo!() + // let caller = bump.alloc() + }), + }; - VNode::Component(bump.alloc(VComponent { - user_fc, - comparator, - raw_props, - caller, - is_static: P::IS_STATIC, - key, - can_memoize: P::IS_STATIC, - drop_props, - associated_scope: Cell::new(None), - })) + todo!() + // let caller: &'a mut dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = + // bump.alloc(move |scope: &ScopeInner| -> Element { + // log::debug!("calling component renderr {:?}", scope.our_arena_idx); + // let props: &'_ P = unsafe { &*(raw_props as *const P) }; + + // let scp: &'a ScopeInner = unsafe { std::mem::transmute(scope) }; + // let s: Scope<'a, P> = (scp, props); + + // let res: Element = component(s); + // unsafe { std::mem::transmute(res) } + // }); + + // let caller = unsafe { BumpBox::from_raw(caller) }; + + // VNode::Component(bump.alloc(VComponent { + // user_fc, + // comparator, + // raw_props, + // caller, + // is_static: P::IS_STATIC, + // key, + // can_memoize: P::IS_STATIC, + // drop_props, + // associated_scope: Cell::new(None), + // })) } pub fn fragment_from_iter( @@ -604,7 +633,6 @@ impl<'a> NodeFactory<'a> { VNode::Fragment(VFragment { children, key: None, - is_static: false, }) } @@ -684,7 +712,6 @@ impl<'a> IntoVNode<'a> for Option> { Some(lazy) => lazy.call(cx), None => VNode::Fragment(VFragment { children: &[], - is_static: false, key: None, }), } diff --git a/packages/core/src/resources.rs b/packages/core/src/resources.rs index 4163e30d3..df931dcda 100644 --- a/packages/core/src/resources.rs +++ b/packages/core/src/resources.rs @@ -32,18 +32,18 @@ pub(crate) struct ResourcePool { 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<&ScopeInner> { + pub fn get_scope(&self, idx: &ScopeId) -> Option<&ScopeInner> { let inner = unsafe { &*self.components.get() }; inner.get(idx.0) } /// this is unsafe because the caller needs to track which other scopes it's already using - pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut ScopeInner> { + pub fn get_scope_mut(&self, idx: &ScopeId) -> Option<&mut ScopeInner> { let inner = unsafe { &mut *self.components.get() }; inner.get_mut(idx.0) } - pub fn try_remove(&self, id: ScopeId) -> Option { + pub fn try_remove(&self, id: &ScopeId) -> Option { let inner = unsafe { &mut *self.components.get() }; Some(inner.remove(id.0)) // .try_remove(id.0) diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index f6c5ee018..21503fffd 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -68,14 +68,17 @@ 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 crate::heuristics::*; + use crate::innerlude::*; +use bumpalo::Bump; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::{pin_mut, stream::FuturesUnordered, Future, FutureExt, StreamExt}; use fxhash::FxHashMap; use fxhash::FxHashSet; use indexmap::IndexSet; use slab::Slab; +use std::pin::Pin; +use std::task::Poll; use std::{ any::{Any, TypeId}, cell::{Cell, UnsafeCell}, @@ -85,11 +88,7 @@ use std::{ #[derive(Clone)] pub(crate) struct EventChannel { - pub task_counter: Rc>, - pub cur_subtree: Rc>, pub sender: UnboundedSender, - pub schedule_any_immediate: Rc, - pub submit_task: Rc TaskHandle>, pub get_shared_context: GetSharedContext, } @@ -101,16 +100,6 @@ pub enum SchedulerMsg { // setstate Immediate(ScopeId), - - // tasks - Task(TaskMsg), -} - -pub enum TaskMsg { - ToggleTask(u64), - PauseTask(u64), - ResumeTask(u64), - DropTask(u64), } /// The scheduler holds basically everything around "working" @@ -126,16 +115,26 @@ pub enum TaskMsg { /// /// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task. /// +/// There's a lot of raw pointers here... +/// +/// Since we're building self-referential structures for each component, we need to make sure that the referencs stay stable +/// The best way to do that is a bump allocator. +/// +/// /// 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 guarantees cannot prove that this is safe. We will need to maintain the safety guarantees manually. - pub pool: ResourcePool, + // /// 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 guarantees cannot prove that this is safe. We will need to maintain the safety guarantees manually. + // pub pool: ResourcePool, + // + pub component_arena: Bump, - pub heuristics: HeuristicsEngine, + pub free_components: VecDeque<*mut ScopeInner>, + + pub heuristics: FxHashMap, pub receiver: UnboundedReceiver, @@ -145,104 +144,70 @@ pub(crate) struct Scheduler { // Every component that has futures that need to be polled pub pending_futures: FxHashSet, - // In-flight futures - pub async_tasks: FuturesUnordered, - // // scheduler stuff // pub current_priority: EventPriority, pub ui_events: VecDeque, pub pending_immediates: VecDeque, - pub pending_tasks: VecDeque, - pub batched_events: VecDeque, pub garbage_scopes: HashSet, pub dirty_scopes: IndexSet, + pub saved_state: Option>, + pub in_progress: bool, } +pub type FcSlot = *const (); + +pub struct Heuristic { + hook_arena_size: usize, + node_arena_size: usize, +} + impl Scheduler { pub(crate) fn new( sender: UnboundedSender, receiver: UnboundedReceiver, + component_capacity: usize, + element_capacity: usize, ) -> Self { /* Preallocate 2000 elements and 100 scopes to avoid dynamic allocation. 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))); - let heuristics = HeuristicsEngine::new(); - - let task_counter = Rc::new(Cell::new(0)); - let cur_subtree = Rc::new(Cell::new(0)); + // let components = Rc::new(UnsafeCell::new(Slab::with_capacity(component_capacity))); + let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(element_capacity))); let channel = EventChannel { - cur_subtree, - task_counter: task_counter.clone(), sender: sender.clone(), - schedule_any_immediate: { - let sender = sender.clone(); - Rc::new(move |id| { - // - log::debug!("scheduling immediate! {:?}", id); - sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap() - }) - }, - // todo: we want to get the futures out of the scheduler message - // the scheduler message should be send/sync - submit_task: { - Rc::new(move |fiber_task| { - let task_id = task_counter.get(); - task_counter.set(task_id + 1); - - todo!(); - // sender - // .unbounded_send(SchedulerMsg::Task(TaskMsg::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| { - let components = unsafe { &*components.get() }; - let mut search: Option<&ScopeInner> = 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 - }) + todo!() + // let components = components.clone(); + // Rc::new(move |id, ty| { + // let components = unsafe { &*components.get() }; + // let mut search: Option<&ScopeInner> = 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 + // }) }, }; - let pool = ResourcePool { - components, - raw_elements, - channel, - }; - - let async_tasks = FuturesUnordered::new(); - - // push a task that would never resolve - prevents us from immediately aborting the scheduler - async_tasks.push(Box::pin(async { - std::future::pending::<()>().await; - ScopeId(0) - }) as FiberTask); + // let pool = ResourcePool { + // components, + // raw_elements, + // channel, + // }; let saved_state = SavedDiffWork { mutations: Mutations::new(), @@ -251,22 +216,15 @@ impl Scheduler { }; Self { - pool, - + // pool, receiver, - async_tasks, - pending_garbage: FxHashSet::default(), - heuristics, - ui_events: VecDeque::new(), pending_immediates: VecDeque::new(), - pending_tasks: VecDeque::new(), - batched_events: VecDeque::new(), garbage_scopes: HashSet::new(), @@ -275,6 +233,8 @@ impl Scheduler { dirty_scopes: Default::default(), saved_state: Some(saved_state), in_progress: false, + + heuristics: todo!(), } } @@ -282,7 +242,7 @@ impl Scheduler { pub fn handle_ui_event(&mut self, event: UserEvent) -> bool { let (discrete, priority) = event_meta(&event); - if let Some(scope) = self.pool.get_scope_mut(event.scope) { + if let Some(scope) = self.get_scope_mut(&event.scope) { if let Some(element) = event.mounted_dom_id { // TODO: bubble properly here scope.call_listener(event, element); @@ -308,7 +268,7 @@ impl Scheduler { fn prepare_work(&mut self) { // while let Some(trigger) = self.ui_events.pop_back() { - // if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {} + // if let Some(scope) = self.get_scope_mut(&trigger.scope) {} // } } @@ -328,15 +288,9 @@ impl Scheduler { unsafe fn load_work(&mut self) -> SavedDiffWork<'static> { self.saved_state.take().unwrap().extend() } - pub fn handle_task(&mut self, evt: TaskMsg) { - // - } pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { match msg { - // - SchedulerMsg::Task(msg) => todo!(), - SchedulerMsg::Immediate(_) => todo!(), SchedulerMsg::UiEvent(event) => { @@ -344,7 +298,7 @@ impl Scheduler { let (discrete, priority) = event_meta(&event); - if let Some(scope) = self.pool.get_scope_mut(event.scope) { + if let Some(scope) = self.get_scope_mut(&event.scope) { if let Some(element) = event.mounted_dom_id { // TODO: bubble properly here scope.call_listener(event, element); @@ -374,19 +328,19 @@ impl Scheduler { let saved_state = unsafe { self.load_work() }; // We have to split away some parts of ourself - current lane is borrowed mutably - let shared = self.pool.clone(); + let shared = self.clone(); let mut machine = unsafe { saved_state.promote(&shared) }; let mut ran_scopes = FxHashSet::default(); if machine.stack.is_empty() { - let shared = self.pool.clone(); + let shared = self.clone(); self.dirty_scopes - .retain(|id| shared.get_scope(*id).is_some()); + .retain(|id| shared.get_scope(id).is_some()); self.dirty_scopes.sort_by(|a, b| { - let h1 = shared.get_scope(*a).unwrap().height; - let h2 = shared.get_scope(*b).unwrap().height; + let h1 = shared.get_scope(a).unwrap().height; + let h2 = shared.get_scope(b).unwrap().height; h1.cmp(&h2).reverse() }); @@ -396,8 +350,8 @@ impl Scheduler { ran_scopes.insert(scopeid); log::debug!("about to run scope {:?}", scopeid); - if let Some(component) = self.pool.get_scope_mut(scopeid) { - if component.run_scope(&self.pool) { + if let Some(component) = self.get_scope_mut(&scopeid) { + if component.run_scope(&self) { let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); // let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); @@ -494,7 +448,6 @@ impl Scheduler { while self.has_any_work() { while let Ok(Some(msg)) = self.receiver.try_next() { match msg { - SchedulerMsg::Task(t) => todo!(), SchedulerMsg::Immediate(im) => { self.dirty_scopes.insert(im); } @@ -506,7 +459,7 @@ impl Scheduler { // switch our priority, pop off any work while let Some(event) = self.ui_events.pop_front() { - if let Some(scope) = self.pool.get_scope_mut(event.scope) { + if let Some(scope) = self.get_scope_mut(&event.scope) { if let Some(element) = event.mounted_dom_id { log::info!("Calling listener {:?}, {:?}", event.scope, element); @@ -519,7 +472,6 @@ impl Scheduler { self.dirty_scopes.insert(im); } SchedulerMsg::UiEvent(e) => self.ui_events.push_back(e), - SchedulerMsg::Task(_) => todo!(), } } } @@ -562,19 +514,19 @@ impl Scheduler { /// /// Typically used to kickstart the VirtualDOM after initialization. pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations { - let mut shared = self.pool.clone(); + let mut shared = self.clone(); let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared); // TODO: drain any in-flight work let cur_component = self .pool - .get_scope_mut(base_scope) + .get_scope_mut(&base_scope) .expect("The base scope should never be moved"); log::debug!("rebuild {:?}", base_scope); // 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.pool) { + if cur_component.run_scope(&self) { diff_machine .stack .create_node(cur_component.frames.fin_head(), MountType::Append); @@ -596,13 +548,13 @@ impl Scheduler { pub fn hard_diff(&mut self, base_scope: ScopeId) -> Mutations { let cur_component = self .pool - .get_scope_mut(base_scope) + .get_scope_mut(&base_scope) .expect("The base scope should never be moved"); log::debug!("hard diff {:?}", base_scope); - if cur_component.run_scope(&self.pool) { - let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self.pool); + if cur_component.run_scope(&self) { + let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self); diff_machine.cfg.force_diff = true; diff_machine.diff_scope(base_scope); diff_machine.mutations @@ -611,3 +563,35 @@ impl Scheduler { } } } + +impl Future for Scheduler { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let mut all_pending = true; + + for fut in self.pending_futures.iter() { + let scope = self + .pool + .get_scope_mut(&fut) + .expect("Scope should never be moved"); + + let items = scope.items.get_mut(); + for task in items.tasks.iter_mut() { + let t = task.as_mut(); + let g = unsafe { Pin::new_unchecked(t) }; + match g.poll(cx) { + Poll::Ready(r) => { + all_pending = false; + } + Poll::Pending => {} + } + } + } + + match all_pending { + true => Poll::Pending, + false => Poll::Ready(()), + } + } +} diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index f48163710..27bffd90f 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -6,36 +6,27 @@ use std::{ cell::{Cell, RefCell}, collections::HashMap, future::Future, - pin::Pin, rc::Rc, }; -use crate::{innerlude::*, lazynodes::LazyNodes}; use bumpalo::{boxed::Box as BumpBox, Bump}; -use std::ops::Deref; /// Components in Dioxus use the "Context" object to interact with their lifecycle. /// /// This lets components access props, schedule updates, integrate hooks, and expose shared state. /// -/// Note: all of these methods are *imperative* - they do not act as hooks! They are meant to be used by hooks -/// to provide complex behavior. For instance, calling "add_shared_state" on every render is considered a leak. This method -/// exists for the `use_provide_state` hook to provide a shared state object. -/// /// For the most part, the only method you should be using regularly is `render`. /// /// ## Example /// /// ```ignore -/// #[derive(Properties)] -/// struct Props { +/// #[derive(Props)] +/// struct ExampleProps { /// name: String /// } /// -/// fn example(cx: Context) -> VNode { -/// html! { -///

"Hello, {cx.name}"
-/// } +/// fn Example((cx, props): Scope) -> Element { +/// cx.render(rsx!{ div {"Hello, {props.name}"} }) /// } /// ``` pub type Context<'a> = &'a ScopeInner; @@ -59,7 +50,8 @@ pub struct ScopeInner { // Nodes pub(crate) frames: ActiveFrame, - pub(crate) caller: BumpBox<'static, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>>, + + pub(crate) vcomp: *const VComponent<'static>, /* we care about: @@ -67,12 +59,7 @@ pub struct ScopeInner { - borrowed props (and how to drop them when the parent is dropped) - suspended nodes (and how to call their callback when their associated tasks are complete) */ - pub(crate) listeners: RefCell>>, - pub(crate) borrowed_props: RefCell>>, - pub(crate) suspended_nodes: RefCell>>, - - pub(crate) tasks: RefCell>>>, - pub(crate) pending_effects: RefCell>>, + pub(crate) items: RefCell>, // State pub(crate) hooks: HookList, @@ -80,15 +67,221 @@ pub struct ScopeInner { // todo: move this into a centralized place - is more memory efficient pub(crate) shared_contexts: RefCell>>, - // whenever set_state is called, we fire off a message to the scheduler - // this closure _is_ the method called by schedule_update that marks this component as dirty - pub(crate) memoized_updater: Rc, - pub(crate) shared: EventChannel, } -/// Public interface for Scopes. +pub struct SelfReferentialItems<'a> { + pub(crate) listeners: Vec<*const Listener<'a>>, + pub(crate) borrowed_props: Vec<*const VComponent<'a>>, + pub(crate) suspended_nodes: FxHashMap>, + pub(crate) tasks: Vec>>, + pub(crate) pending_effects: Vec>, +} + impl ScopeInner { + // we are being created in the scope of an existing component (where the creator_node lifetime comes into play) + // we are going to break this lifetime by force in order to save it on ourselves. + // To make sure that the lifetime isn't truly broken, we receive a Weak RC so we can't keep it around after the parent dies. + // This should never happen, but is a good check to keep around + // + // Scopes cannot be made anywhere else except for this file + // Therefore, their lifetimes are connected exclusively to the virtual dom + pub(crate) fn new( + vcomp: &VComponent, + our_arena_idx: ScopeId, + parent_idx: Option, + height: u32, + subtree: u32, + shared: EventChannel, + ) -> Self { + let vcomp = unsafe { std::mem::transmute(vcomp as *const VComponent) }; + + Self { + shared, + parent_idx, + our_arena_idx, + height, + subtree: Cell::new(subtree), + is_subtree_root: Cell::new(false), + frames: ActiveFrame::new(), + vcomp, + + hooks: Default::default(), + shared_contexts: Default::default(), + + items: RefCell::new(SelfReferentialItems { + listeners: Default::default(), + borrowed_props: Default::default(), + suspended_nodes: Default::default(), + tasks: Default::default(), + pending_effects: Default::default(), + }), + } + } + + /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from + /// causing 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. + /// + /// References 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. + /// + /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will + /// be dropped. + pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) { + // 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 Reference) + // right now, we don't drop + self.items + .get_mut() + .borrowed_props + .drain(..) + .map(|li| unsafe { &*li }) + .for_each(|comp| { + // First drop the component's undropped references + let scope_id = comp + .associated_scope + .get() + .expect("VComponents should be associated with a valid Scope"); + + if let Some(scope) = pool.get_scope_mut(&scope_id) { + scope.ensure_drop_safety(pool); + + todo!("drop the component's props"); + // let mut drop_props = comp.drop_props.borrow_mut().take().unwrap(); + // drop_props(); + } + }); + + // Now that all the references are gone, we can safely drop our own references in our listeners. + self.items + .get_mut() + .listeners + .drain(..) + .map(|li| unsafe { &*li }) + .for_each(|listener| drop(listener.callback.borrow_mut().take())); + } + + /// A safe wrapper around calling listeners + pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) { + let listners = &mut self.items.get_mut().listeners; + + let raw_listener = listners.iter().find(|lis| { + let search = unsafe { &***lis }; + if search.event == event.name { + let search_id = search.mounted_node.get(); + search_id.map(|f| f == element).unwrap_or(false) + } else { + false + } + }); + + if let Some(raw_listener) = raw_listener { + let listener = unsafe { &**raw_listener }; + let mut cb = listener.callback.borrow_mut(); + if let Some(cb) = cb.as_mut() { + (cb)(event.event); + } + } else { + log::warn!("An event was triggered but there was no listener to handle it"); + } + } + + // General strategy here is to load up the appropriate suspended task and then run it. + // Suspended nodes cannot be called repeatedly. + pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) { + let mut nodes = &mut self.items.get_mut().suspended_nodes; + + if let Some(suspended) = nodes.remove(&task_id) { + let sus: &'a VSuspended<'static> = unsafe { &*suspended }; + let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) }; + let mut boxed = sus.callback.borrow_mut().take().unwrap(); + let new_node: Element<'a> = boxed(); + } + } + + // run the list of effects + pub(crate) fn run_effects(&mut self, pool: &ResourcePool) { + for mut effect in self.items.get_mut().pending_effects.drain(..) { + effect(); + } + } + + /// Render this component. + /// + /// Returns true if the scope completed successfully and false if running failed (IE a None error was propagated). + 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(pool); + + // Safety: + // - We dropped the listeners, so no more &mut T can be used while these are held + // - All children nodes that rely on &mut T are replaced with a new reference + unsafe { self.hooks.reset() }; + + // Safety: + // - We've dropped all references to the wip bump frame + unsafe { self.frames.reset_wip_frame() }; + + let items = self.items.get_mut(); + + // just forget about our suspended nodes while we're at it + items.suspended_nodes.clear(); + + // guarantee that we haven't screwed up - there should be no latent references anywhere + debug_assert!(items.listeners.is_empty()); + debug_assert!(items.suspended_nodes.is_empty()); + debug_assert!(items.borrowed_props.is_empty()); + + log::debug!("Borrowed stuff is successfully cleared"); + + // temporarily cast the vcomponent to the right lifetime + let vcomp = self.load_vcomp(); + + let render: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = todo!(); + + // Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders. + if let Some(builder) = render(self) { + let new_head = builder.into_vnode(NodeFactory { + bump: &self.frames.wip_frame().bump, + }); + log::debug!("Render is successful"); + + // 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(); + + true + } else { + false + } + } + + pub(crate) fn new_subtree(&self) -> Option { + todo!() + // if self.is_subtree_root.get() { + // None + // } else { + // let cur = self.shared.cur_subtree.get(); + // self.shared.cur_subtree.set(cur + 1); + // Some(cur) + // } + } + + pub(crate) fn update_vcomp(&mut self, vcomp: &VComponent) { + let f: *const _ = vcomp; + self.vcomp = unsafe { std::mem::transmute(f) }; + } + + pub(crate) fn load_vcomp<'a>(&'a mut self) -> &'a VComponent<'a> { + unsafe { std::mem::transmute(&*self.vcomp) } + } + /// Get the root VNode for this Scope. /// /// This VNode is the "entrypoint" VNode. If the component renders multiple nodes, then this VNode will be a fragment. @@ -128,16 +321,6 @@ impl ScopeInner { self.subtree.get() } - pub(crate) fn new_subtree(&self) -> Option { - if self.is_subtree_root.get() { - None - } else { - let cur = self.shared.cur_subtree.get(); - self.shared.cur_subtree.set(cur + 1); - Some(cur) - } - } - /// Get the height of this Scope - IE the number of scopes above it. /// /// A Scope with a height of `0` is the root scope - there are no other scopes above it. @@ -192,233 +375,17 @@ impl ScopeInner { pub fn scope_id(&self) -> ScopeId { self.our_arena_idx } -} -// The type of closure that wraps calling components -/// 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>>; - -/// Private interface for Scopes. -impl ScopeInner { - // we are being created in the scope of an existing component (where the creator_node lifetime comes into play) - // we are going to break this lifetime by force in order to save it on ourselves. - // To make sure that the lifetime isn't truly broken, we receive a Weak RC so we can't keep it around after the parent dies. - // This should never happen, but is a good check to keep around - // - // Scopes cannot be made anywhere else except for this file - // Therefore, their lifetimes are connected exclusively to the virtual dom - pub(crate) fn new( - caller: BumpBox Fn(&'b ScopeInner) -> Element<'b>>, - our_arena_idx: ScopeId, - parent_idx: Option, - height: u32, - subtree: u32, - shared: EventChannel, - ) -> Self { - let schedule_any_update = shared.schedule_any_immediate.clone(); - - let memoized_updater = Rc::new(move || schedule_any_update(our_arena_idx)); - - // wipe away the associated lifetime - we are going to manually manage the one-way lifetime graph - let caller = unsafe { std::mem::transmute(caller) }; - - Self { - memoized_updater, - shared, - caller, - parent_idx, - our_arena_idx, - height, - subtree: Cell::new(subtree), - is_subtree_root: Cell::new(false), - tasks: Default::default(), - frames: ActiveFrame::new(), - hooks: Default::default(), - - pending_effects: Default::default(), - suspended_nodes: Default::default(), - shared_contexts: Default::default(), - listeners: Default::default(), - borrowed_props: Default::default(), - } - } - - pub(crate) fn update_scope_dependencies( - &mut self, - caller: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, - ) { - log::debug!("Updating scope dependencies {:?}", self.our_arena_idx); - let caller = caller as *const _; - self.caller = unsafe { std::mem::transmute(caller) }; - } - - /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from - /// causing 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. - /// - /// References 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. - /// - /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will - /// be dropped. - pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) { - // 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 Reference) - // right now, we don't drop - 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() - .expect("VComponents should be associated with a valid Scope"); - - if let Some(scope) = pool.get_scope_mut(scope_id) { - scope.ensure_drop_safety(pool); - - let mut drop_props = comp.drop_props.borrow_mut().take().unwrap(); - drop_props(); - } - }); - - // 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| drop(listener.callback.borrow_mut().take())); - } - - /// A safe wrapper around calling listeners - pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) { - let listners = self.listeners.borrow_mut(); - - let raw_listener = listners.iter().find(|lis| { - let search = unsafe { &***lis }; - if search.event == event.name { - let search_id = search.mounted_node.get(); - search_id.map(|f| f == element).unwrap_or(false) - } else { - false - } - }); - - if let Some(raw_listener) = raw_listener { - let listener = unsafe { &**raw_listener }; - let mut cb = listener.callback.borrow_mut(); - if let Some(cb) = cb.as_mut() { - (cb)(event.event); - } - } else { - log::warn!("An event was triggered but there was no listener to handle it"); - } - } - - /* - General strategy here is to load up the appropriate suspended task and then run it. - Suspended nodes cannot be called repeatedly. - */ - pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) { - let mut nodes = self.suspended_nodes.borrow_mut(); - - if let Some(suspended) = nodes.remove(&task_id) { - let sus: &'a VSuspended<'static> = unsafe { &*suspended }; - let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) }; - let mut boxed = sus.callback.borrow_mut().take().unwrap(); - let new_node: Element<'a> = boxed(); - } - } - - // run the list of effects - pub(crate) fn run_effects(&mut self, pool: &ResourcePool) { - todo!() - // let mut effects = self.frames.effects.borrow_mut(); - // let mut effects = effects.drain(..).collect::>(); - - // for effect in effects { - // let effect = unsafe { &*effect }; - // let effect = effect.as_ref(); - - // let mut effect = effect.borrow_mut(); - // let mut effect = effect.as_mut(); - - // effect.run(pool); - // } - } - - /// Render this component. - /// - /// Returns true if the scope completed successfully and false if running failed (IE a None error was propagated). - 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(pool); - - // Safety: - // - We dropped the listeners, so no more &mut T can be used while these are held - // - All children nodes that rely on &mut T are replaced with a new reference - unsafe { self.hooks.reset() }; - - // Safety: - // - We've dropped all references to the wip bump frame - unsafe { self.frames.reset_wip_frame() }; - - // just forget about our suspended nodes while we're at it - self.suspended_nodes.get_mut().clear(); - - // guarantee that we haven't screwed up - there should be no latent references anywhere - debug_assert!(self.listeners.borrow().is_empty()); - debug_assert!(self.suspended_nodes.borrow().is_empty()); - debug_assert!(self.borrowed_props.borrow().is_empty()); - - log::debug!("Borrowed stuff is successfully cleared"); - - // Cast the caller ptr from static to one with our own reference - let render: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = unsafe { &*self.caller }; - - // Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders. - if let Some(builder) = render(self) { - let new_head = builder.into_vnode(NodeFactory { - bump: &self.frames.wip_frame().bump, - }); - log::debug!("Render is successful"); - - // 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(); - - true - } else { - false - } - } /// Create a subscription that schedules a future render for the reference component /// /// ## Notice: you should prefer using prepare_update and get_scope_id pub fn schedule_update(&self) -> Rc { - self.memoized_updater.clone() - } - - /// Get the [`ScopeId`] of a mounted component. - /// - /// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted. - pub fn needs_update(&self) { - (self.memoized_updater)() - } - - /// Get the [`ScopeId`] of a mounted component. - /// - /// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted. - pub fn needs_update_any(&self, id: ScopeId) { - (self.shared.schedule_any_immediate)(id) + // pub fn schedule_update(&self) -> Rc { + let chan = self.shared.sender.clone(); + let id = self.scope_id(); + Rc::new(move || { + chan.unbounded_send(SchedulerMsg::Immediate(id)); + }) } /// Schedule an update for any component given its ScopeId. @@ -427,7 +394,27 @@ impl ScopeInner { /// /// This method should be used when you want to schedule an update for a component pub fn schedule_update_any(&self) -> Rc { - self.shared.schedule_any_immediate.clone() + let chan = self.shared.sender.clone(); + Rc::new(move |id| { + chan.unbounded_send(SchedulerMsg::Immediate(id)); + }) + } + + /// Get the [`ScopeId`] of a mounted component. + /// + /// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted. + pub fn needs_update(&self) { + self.needs_update_any(self.scope_id()) + } + + /// Get the [`ScopeId`] of a mounted component. + /// + /// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted. + pub fn needs_update_any(&self, id: ScopeId) { + self.shared + .sender + .unbounded_send(SchedulerMsg::Immediate(id)) + .unwrap(); } /// Get the [`ScopeId`] of a mounted component. @@ -475,8 +462,9 @@ impl ScopeInner { // erase the 'src lifetime for self-referential storage let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; - self.pending_effects.borrow_mut().push(self_ref_fut); - self.pending_effects.borrow().len() - 1 + let mut items = self.items.borrow_mut(); + items.pending_effects.push(self_ref_fut); + items.pending_effects.len() - 1 } /// Pushes the future onto the poll queue to be polled @@ -491,8 +479,9 @@ impl ScopeInner { // erase the 'src lifetime for self-referential storage let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; - self.tasks.borrow_mut().push(self_ref_fut); - self.tasks.borrow().len() - 1 + let mut items = self.items.borrow_mut(); + items.tasks.push(self_ref_fut); + items.tasks.len() - 1 } /// This method enables the ability to expose state to children further down the VirtualDOM Tree. @@ -580,8 +569,9 @@ impl ScopeInner { /// /// - Initializer: closure used to create the initial hook state /// - Runner: closure used to output a value every time the hook is used - /// - Cleanup: closure used to teardown the hook once the dom is cleaned up /// + /// To "cleanup" the hook, implement `Drop` on the stored hook value. Whenever the component is dropped, the hook + /// will be dropped as well. /// /// # Example /// @@ -591,32 +581,16 @@ impl ScopeInner { /// use_hook( /// || Rc::new(RefCell::new(initial_value())), /// |state| state, - /// |_| {}, /// ) /// } /// ``` - pub fn use_hook<'src, State, Output, Init, Run, Cleanup>( + pub fn use_hook<'src, State: 'static, Output: 'src>( &'src self, - initializer: Init, - runner: Run, - cleanup: Cleanup, - ) -> Output - where - State: 'static, - Output: 'src, - Init: FnOnce(usize) -> State, - Run: FnOnce(&'src mut State) -> Output, - Cleanup: FnOnce(Box) + 'static, - { - // If the idx is the same as the hook length, then we need to add the current hook + initializer: impl FnOnce(usize) -> State, + runner: impl FnOnce(&'src mut State) -> Output, + ) -> Output { if self.hooks.at_end() { - self.hooks.push_hook( - initializer(self.hooks.len()), - Box::new(|raw| { - let s = raw.downcast::().unwrap(); - cleanup(s); - }), - ); + self.hooks.push_hook(initializer(self.hooks.len())); } runner(self.hooks.next::().expect(HOOK_ERR_MSG)) diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs deleted file mode 100644 index 95540d153..000000000 --- a/packages/core/src/tasks.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::innerlude::*; -use futures_channel::mpsc::UnboundedSender; - -pub struct TaskHandle { - pub(crate) sender: UnboundedSender, - pub(crate) 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) { - self.sender - .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id))) - .unwrap() - } - - /// This method is not synchronous - your task will not stop immediately. - pub fn resume(&self) { - self.sender - .unbounded_send(SchedulerMsg::Task(TaskMsg::ResumeTask(self.our_id))) - .unwrap() - } - - /// This method is not synchronous - your task will not stop immediately. - pub fn stop(&self) { - self.sender - .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id))) - .unwrap() - } - - /// This method is not synchronous - your task will not stop immediately. - pub fn restart(&self) { - self.sender - .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id))) - .unwrap() - } -} diff --git a/packages/core/src/test_dom.rs b/packages/core/src/test_dom.rs index aab7f3373..332a23402 100644 --- a/packages/core/src/test_dom.rs +++ b/packages/core/src/test_dom.rs @@ -13,7 +13,7 @@ impl TestDom { pub fn new() -> TestDom { let bump = Bump::new(); let (sender, receiver) = futures_channel::mpsc::unbounded::(); - let scheduler = Scheduler::new(sender, receiver); + let scheduler = Scheduler::new(sender, receiver, 10, 100); TestDom { bump, scheduler } } diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 1ddd3a51c..fad669026 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -11,59 +11,6 @@ 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(()) - } - } -} - /// A component's unique identifier. /// /// `ScopeId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 6945520e4..c0d3054a2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -19,10 +19,24 @@ //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. +use crate::innerlude::*; +use bumpalo::Bump; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures_util::{pin_mut, stream::FuturesUnordered, Future, FutureExt, StreamExt}; +use fxhash::FxHashMap; +use fxhash::FxHashSet; +use indexmap::IndexSet; +use slab::Slab; +use std::pin::Pin; +use std::task::Poll; +use std::{ + any::{Any, TypeId}, + cell::{Cell, UnsafeCell}, + collections::{HashSet, VecDeque}, + rc::Rc, +}; use crate::innerlude::*; -use std::{any::Any, rc::Rc}; /// An integrated virtual node system that progresses events and diffs UI trees. /// @@ -55,8 +69,6 @@ use std::{any::Any, rc::Rc}; /// } /// ``` pub struct VirtualDom { - scheduler: Scheduler, - base_scope: ScopeId, root_fc: Box, @@ -65,6 +77,49 @@ pub struct VirtualDom { // we need to keep the allocation around, but we don't necessarily use it _root_caller: Box, + + // /// 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 guarantees cannot prove that this is safe. We will need to maintain the safety guarantees manually. + // pub pool: ResourcePool, + // + pub component_arena: Bump, + + pub free_components: VecDeque<*mut ScopeInner>, + + pub heuristics: FxHashMap, + + pub receiver: UnboundedReceiver, + + // Garbage stored + pub pending_garbage: FxHashSet, + + // Every component that has futures that need to be polled + pub pending_futures: FxHashSet, + + pub ui_events: VecDeque, + + pub pending_immediates: VecDeque, + + pub batched_events: VecDeque, + + pub garbage_scopes: HashSet, + + pub dirty_scopes: IndexSet, + + pub saved_state: Option>, + + pub in_progress: bool, +} + +pub enum SchedulerMsg { + // events from the host + UiEvent(UserEvent), + + // setstate + Immediate(ScopeId), } impl VirtualDom { @@ -127,7 +182,7 @@ impl VirtualDom { Self::new_with_props_and_scheduler(root, root_props, sender, receiver) } - /// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler. + /// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the /// /// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the /// VirtualDom to be created just to retrieve its channel receiver. @@ -137,33 +192,71 @@ impl VirtualDom { sender: UnboundedSender, receiver: UnboundedReceiver, ) -> Self { - let root_fc = Box::new(root); + let mut component_arena = Bump::new(); - let root_props: Rc = Rc::new(root_props); + // let root_fc = Box::new(root); - let props = root_props.clone(); + // let root_props: Rc = Rc::new(root_props); - let root_caller: Box Element> = - Box::new(move |scope: &ScopeInner| { - let props = props.downcast_ref::

().unwrap(); - let node = root((scope, props)); - // cast into the right lifetime - unsafe { std::mem::transmute(node) } - }); - let caller = unsafe { bumpalo::boxed::Box::from_raw(root_caller.as_mut() as *mut _) }; + // let props = root_props.clone(); - let scheduler = Scheduler::new(sender, receiver); + // let mut root_caller: Box Element> = + // Box::new(move |scope: &ScopeInner| { + // let props = props.downcast_ref::

().unwrap(); + // let node = root((scope, props)); + // // cast into the right lifetime + // unsafe { std::mem::transmute(node) } + // }); - let base_scope = scheduler.pool.insert_scope_with_key(|myidx| { - ScopeInner::new(caller, myidx, None, 0, 0, scheduler.pool.channel.clone()) - }); + // let caller = unsafe { bumpalo::boxed::Box::from_raw(root_caller.as_mut() as *mut _) }; + + // // todo make the memory footprint congifurable + // let scheduler = Scheduler::new(sender, receiver, 100, 2000); + + // let vcomp = VComponent { + // key: todo!(), + // associated_scope: todo!(), + // user_fc: root as *const _, + // can_memoize: todo!(), + // raw_props: todo!(), + // // drop_props: todo!(), + // // caller, + // comparator: todo!(), + // caller: todo!(), + // }; + + // let boxed_comp = Box::new(vcomp); + + // let base_scope = pool.insert_scope_with_key(|myidx| { + // ScopeInner::new( + // boxed_comp.as_ref(), + // myidx, + // None, + // 0, + // 0, + // pool.channel.clone(), + // ) + // }); Self { - _root_caller: Box::new(root_caller), - root_fc, - base_scope, - scheduler, - root_props, + scheduler: todo!(), + base_scope: todo!(), + root_fc: todo!(), + root_props: todo!(), + _root_caller: todo!(), + component_arena: todo!(), + free_components: todo!(), + heuristics: todo!(), + receiver, + pending_garbage: todo!(), + pending_futures: todo!(), + ui_events: todo!(), + pending_immediates: todo!(), + batched_events: todo!(), + garbage_scopes: todo!(), + dirty_scopes: todo!(), + saved_state: todo!(), + in_progress: todo!(), } } @@ -172,12 +265,12 @@ impl VirtualDom { /// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus /// directly. pub fn base_scope(&self) -> &ScopeInner { - self.scheduler.pool.get_scope(self.base_scope).unwrap() + self.pool.get_scope(&self.base_scope).unwrap() } /// Get the [`Scope`] for a component given its [`ScopeId`] pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeInner> { - self.scheduler.pool.get_scope(id) + self.pool.get_scope(&id) } /// Update the root props of this VirtualDOM. @@ -202,10 +295,10 @@ impl VirtualDom { where P: 'static, { - let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap(); + let root_scope = self.pool.get_scope_mut(&self.base_scope).unwrap(); // Pre-emptively drop any downstream references of the old props - root_scope.ensure_drop_safety(&self.scheduler.pool); + root_scope.ensure_drop_safety(&self.pool); let mut root_props: Rc = Rc::new(root_props); @@ -221,8 +314,6 @@ impl VirtualDom { std::mem::transmute(root((scope, props))) }); - root_scope.update_scope_dependencies(&root_caller); - drop(root_props); Some(self.rebuild()) @@ -249,7 +340,7 @@ impl VirtualDom { /// apply_edits(edits); /// ``` pub fn rebuild(&mut self) -> Mutations { - self.scheduler.rebuild(self.base_scope) + self.rebuild(self.base_scope) } /// Compute a manual diff of the VirtualDOM between states. @@ -287,7 +378,7 @@ impl VirtualDom { /// let edits = dom.diff(); /// ``` pub fn diff(&mut self) -> Mutations { - self.scheduler.hard_diff(self.base_scope) + self.hard_diff(self.base_scope) } /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. @@ -295,8 +386,8 @@ impl VirtualDom { /// 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(&mut self) -> Option> { - if self.scheduler.has_any_work() { - Some(self.scheduler.work_sync()) + if self.has_any_work() { + Some(self.work_sync()) } else { None } @@ -346,18 +437,18 @@ impl VirtualDom { /// /// Mutations are the only link between the RealDOM and the VirtualDOM. pub fn run_with_deadline(&mut self, deadline: impl FnMut() -> bool) -> Vec> { - self.scheduler.work_with_deadline(deadline) + self.work_with_deadline(deadline) } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - self.scheduler.pool.channel.sender.clone() + self.pool.channel.sender.clone() } /// Waits for the scheduler to have work /// This lets us poll async tasks during idle periods without blocking the main thread. pub async fn wait_for_work(&mut self) { // todo: poll the events once even if there is work to do to prevent starvation - if self.scheduler.has_any_work() { + if self.has_any_work() { return; } @@ -365,28 +456,485 @@ impl VirtualDom { // Wait for any new events if we have nothing to do - let tasks_fut = self.scheduler.async_tasks.next(); - let scheduler_fut = self.scheduler.receiver.next(); + // let tasks_fut = self.async_tasks.next(); + // let scheduler_fut = self.receiver.next(); - use futures_util::future::{select, Either}; - match select(tasks_fut, scheduler_fut).await { - // poll the internal futures - Either::Left((_id, _)) => { + // use futures_util::future::{select, Either}; + // match select(tasks_fut, scheduler_fut).await { + // // poll the internal futures + // Either::Left((_id, _)) => { + // // + // } + + // // wait for an external event + // Either::Right((msg, _)) => match msg.unwrap() { + // SchedulerMsg::Task(t) => { + // self.handle_task(t); + // } + // SchedulerMsg::Immediate(im) => { + // self.dirty_scopes.insert(im); + // } + // SchedulerMsg::UiEvent(evt) => { + // self.ui_events.push_back(evt); + // } + // }, + // } + } +} + +pub type FcSlot = *const (); + +pub struct Heuristic { + hook_arena_size: usize, + node_arena_size: usize, +} + +/* +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 in 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 juggling 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). + +# Priority 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-priority 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 priority 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. +*/ + +/// 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 4 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. +/// +/// There's a lot of raw pointers here... +/// +/// Since we're building self-referential structures for each component, we need to make sure that the referencs stay stable +/// The best way to do that is a bump allocator. +/// +/// +/// +impl VirtualDom { + // returns true if the event is discrete + pub fn handle_ui_event(&mut self, event: UserEvent) -> bool { + let (discrete, priority) = event_meta(&event); + + if let Some(scope) = self.get_scope_mut(&event.scope) { + if let Some(element) = event.mounted_dom_id { + // TODO: bubble properly here + scope.call_listener(event, element); + + while let Ok(Some(dirty_scope)) = self.receiver.try_next() { + // + // self.add_dirty_scope(dirty_scope, trigger.priority) + } + } + } + + // use EventPriority::*; + + // match priority { + // Immediate => todo!(), + // High => todo!(), + // Medium => todo!(), + // Low => todo!(), + // } + + discrete + } + + fn prepare_work(&mut self) { + // while let Some(trigger) = self.ui_events.pop_back() { + // if let Some(scope) = self.get_scope_mut(&trigger.scope) {} + // } + } + + // nothing to do, no events on channels, no work + pub fn has_any_work(&self) -> bool { + !(self.dirty_scopes.is_empty() && self.ui_events.is_empty()) + } + + /// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work + fn balance_lanes(&mut self) {} + + fn save_work(&mut self, lane: SavedDiffWork) { + let saved: SavedDiffWork<'static> = unsafe { std::mem::transmute(lane) }; + self.saved_state = Some(saved); + } + + unsafe fn load_work(&mut self) -> SavedDiffWork<'static> { + self.saved_state.take().unwrap().extend() + } + + pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { + match msg { + SchedulerMsg::Immediate(_) => todo!(), + + SchedulerMsg::UiEvent(event) => { // + + let (discrete, priority) = event_meta(&event); + + if let Some(scope) = self.get_scope_mut(&event.scope) { + if let Some(element) = event.mounted_dom_id { + // TODO: bubble properly here + scope.call_listener(event, element); + + while let Ok(Some(dirty_scope)) = self.receiver.try_next() { + // + // self.add_dirty_scope(dirty_scope, trigger.priority) + } + } + } + + discrete; + } + } + } + + /// 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: 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 = unsafe { self.load_work() }; + + // We have to split away some parts of ourself - current lane is borrowed mutably + let shared = self.clone(); + let mut machine = unsafe { saved_state.promote(&shared) }; + + let mut ran_scopes = FxHashSet::default(); + + if machine.stack.is_empty() { + let shared = self.clone(); + + self.dirty_scopes + .retain(|id| shared.get_scope(id).is_some()); + self.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).reverse() + }); + + if let Some(scopeid) = self.dirty_scopes.pop() { + log::info!("handling dirty scope {:?}", scopeid); + if !ran_scopes.contains(&scopeid) { + ran_scopes.insert(scopeid); + log::debug!("about to run scope {:?}", scopeid); + + if let Some(component) = self.get_scope_mut(&scopeid) { + if component.run_scope(&self) { + let (old, new) = + (component.frames.wip_head(), component.frames.fin_head()); + // let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + machine.stack.scope_stack.push(scopeid); + machine.stack.push(DiffInstruction::Diff { new, old }); + } + } + } + } + } + + let work_completed = machine.work(deadline_reached); + + // log::debug!("raw edits {:?}", machine.mutations.edits); + + let mut machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) }; + // let mut saved = machine.save(); + + if work_completed { + for node in machine.seen_scopes.drain() { + // self.dirty_scopes.clear(); + // self.ui_events.clear(); + self.dirty_scopes.remove(&node); + // self.dirty_scopes.remove(&node); } - // wait for an external event - Either::Right((msg, _)) => match msg.unwrap() { - SchedulerMsg::Task(t) => { - self.scheduler.handle_task(t); + let mut new_mutations = Mutations::new(); + + for edit in machine.mutations.edits.drain(..) { + new_mutations.edits.push(edit); + } + + // for edit in saved.edits.drain(..) { + // new_mutations.edits.push(edit); + // } + + // std::mem::swap(&mut new_mutations, &mut saved.mutations); + + mutations.push(new_mutations); + + // log::debug!("saved edits {:?}", mutations); + + let mut saved = machine.save(); + self.save_work(saved); + true + + // self.save_work(saved); + // false + } else { + false + } + } + + /// 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 fn work_with_deadline<'a>( + &'a mut self, + mut deadline: impl FnMut() -> bool, + ) -> 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 priority 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 + + - we batch completely until we run into a discrete event + - all continuous events are batched together + - so D C C C C C would be two separate events - D and C. IE onclick and onscroll + - D C C C C C C D C C C D would be D C D C D in 5 distinct phases. + + - !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future + - we need to keep track of element parents to be able to traverse properly + + + 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(); + + while self.has_any_work() { + while let Ok(Some(msg)) = self.receiver.try_next() { + match msg { + SchedulerMsg::Immediate(im) => { + self.dirty_scopes.insert(im); + } + SchedulerMsg::UiEvent(evt) => { + self.ui_events.push_back(evt); + } } - SchedulerMsg::Immediate(im) => { - self.scheduler.dirty_scopes.insert(im); + } + + // switch our priority, pop off any work + while let Some(event) = self.ui_events.pop_front() { + if let Some(scope) = self.get_scope_mut(&event.scope) { + if let Some(element) = event.mounted_dom_id { + log::info!("Calling listener {:?}, {:?}", event.scope, element); + + // TODO: bubble properly here + scope.call_listener(event, element); + + while let Ok(Some(dirty_scope)) = self.receiver.try_next() { + match dirty_scope { + SchedulerMsg::Immediate(im) => { + self.dirty_scopes.insert(im); + } + SchedulerMsg::UiEvent(e) => self.ui_events.push_back(e), + } + } + } } - SchedulerMsg::UiEvent(evt) => { - self.scheduler.ui_events.push_back(evt); + } + + let work_complete = self.work_on_current_lane(&mut deadline, &mut committed_mutations); + + if !work_complete { + return committed_mutations; + } + } + + committed_mutations + } + + /// 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(); + + while let Ok(Some(msg)) = self.receiver.try_next() { + self.handle_channel_msg(msg); + } + + if !self.has_any_work() { + return committed_mutations; + } + + while self.has_any_work() { + self.prepare_work(); + self.work_on_current_lane(|| false, &mut committed_mutations); + } + + committed_mutations + } + + /// Restart the entire VirtualDOM from scratch, wiping away any old state and components. + /// + /// Typically used to kickstart the VirtualDOM after initialization. + pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations { + let mut shared = self.clone(); + let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared); + + // TODO: drain any in-flight work + let cur_component = self + .pool + .get_scope_mut(&base_scope) + .expect("The base scope should never be moved"); + + log::debug!("rebuild {:?}", base_scope); + + // 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) { + diff_machine + .stack + .create_node(cur_component.frames.fin_head(), MountType::Append); + + diff_machine.stack.scope_stack.push(base_scope); + + diff_machine.work(|| false); + } else { + // todo: should this be a hard error? + log::warn!( + "Component failed to run successfully during rebuild. + This does not result in a failed rebuild, but indicates a logic failure within your app." + ); + } + + unsafe { std::mem::transmute(diff_machine.mutations) } + } + + pub fn hard_diff(&mut self, base_scope: ScopeId) -> Mutations { + let cur_component = self + .pool + .get_scope_mut(&base_scope) + .expect("The base scope should never be moved"); + + log::debug!("hard diff {:?}", base_scope); + + if cur_component.run_scope(&self) { + let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self); + diff_machine.cfg.force_diff = true; + diff_machine.diff_scope(base_scope); + diff_machine.mutations + } else { + Mutations::new() + } + } +} + +impl Future for VirtualDom { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let mut all_pending = true; + + for fut in self.pending_futures.iter() { + let scope = self + .pool + .get_scope_mut(&fut) + .expect("Scope should never be moved"); + + let items = scope.items.get_mut(); + for task in items.tasks.iter_mut() { + let t = task.as_mut(); + let g = unsafe { Pin::new_unchecked(t) }; + match g.poll(cx) { + Poll::Ready(r) => { + all_pending = false; + } + Poll::Pending => {} } - }, + } + } + + match all_pending { + true => Poll::Pending, + false => Poll::Ready(()), } } }