diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 93ef63d60..23bebf042 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,15 +1,15 @@ -use std::task::Context; +use std::pin::Pin; -use futures_util::task::noop_waker_ref; -use futures_util::{pin_mut, Future}; - -use crate::factory::RenderReturn; +use crate::factory::{FiberLeaf, RenderReturn}; use crate::mutations::Mutation; use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; +use crate::suspense::LeafLocation; use crate::virtualdom::VirtualDom; use crate::{AttributeValue, Element, ElementId, TemplateAttribute}; +use bumpalo::boxed::Box as BumpBox; +use futures_util::Future; impl VirtualDom { /// Create this template and write its mutations @@ -177,7 +177,9 @@ impl VirtualDom { 1 } - DynamicNode::Component { props, .. } => { + DynamicNode::Component { + props, placeholder, .. + } => { let id = self.new_scope(unsafe { std::mem::transmute(props.get()) }); let render_ret = self.run_scope(id); @@ -192,39 +194,52 @@ impl VirtualDom { self.scope_stack.pop(); created } - RenderReturn::Sync(None) => todo!("nodes that return nothing"), + + // whenever the future is polled later, we'll revisit it + // For now, just set the placeholder + RenderReturn::Sync(None) => { + let new_id = self.next_element(template); + placeholder.set(Some(new_id)); + mutations.push(AssignId { + id: new_id, + path: &template.template.node_paths[idx][1..], + }); + 0 + } RenderReturn::Async(fut) => { - use futures_util::FutureExt; + let new_id = self.next_element(template); - // Poll the suspense node once to see if we can get any nodes from it - let mut cx = Context::from_waker(&noop_waker_ref()); - let res = fut.poll_unpin(&mut cx); + // move up the tree looking for the first suspense boundary + // our current component can not be a suspense boundary, so we skip it + for scope_id in self.scope_stack.iter().rev().skip(1) { + let scope = &mut self.scopes[scope_id.0]; + if let Some(fiber) = &mut scope.suspense_boundary { + // save the fiber leaf onto the fiber itself + let detached: &mut FiberLeaf<'static> = + unsafe { std::mem::transmute(fut) }; - match res { - std::task::Poll::Ready(Some(val)) => { - let scope = self.get_scope(id).unwrap(); - let ready = &*scope.bump().alloc(val); - let ready = unsafe { std::mem::transmute(ready) }; + // And save the fiber leaf using the placeholder node + // this way, when we resume the fiber, we just need to "pick up placeholder" + fiber.futures.insert( + LeafLocation { + element: new_id, + scope: *scope_id, + }, + detached, + ); - self.scope_stack.push(id); - let created = self.create(mutations, ready); - self.scope_stack.pop(); - created - } - std::task::Poll::Ready(None) => { - todo!("Pending suspense") - } - std::task::Poll::Pending => { - let new_id = self.next_element(template); - // id.set(new_id); - mutations.push(AssignId { - id: new_id, - path: &template.template.node_paths[idx][1..], - }); - - 0 + self.suspended_scopes.insert(*scope_id); + break; } } + + placeholder.set(Some(new_id)); + mutations.push(AssignId { + id: new_id, + path: &template.template.node_paths[idx][1..], + }); + + 0 } } } diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index a25923ca2..15f5d1584 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -97,6 +97,7 @@ impl ScopeState { name: fn_name, is_static: P::IS_STATIC, props: Cell::new(detached_dyn), + placeholder: Cell::new(None), } } } @@ -135,6 +136,8 @@ pub enum RenderReturn<'a> { Async(Pin> + 'a>>), } +pub type FiberLeaf<'a> = Pin> + 'a>>; + pub trait IntoVnode<'a, A = ()> { fn into_dynamic_node(self, cx: &'a ScopeState) -> VNode<'a>; } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index f91b1402e..5cafc7c6c 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -14,6 +14,8 @@ mod nodes; mod properties; mod scope_arena; mod scopes; +// mod subtree; +mod suspense; mod virtualdom; pub(crate) mod innerlude { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 5fa185be9..9e6742ec3 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -79,6 +79,7 @@ pub enum DynamicNode<'a> { name: &'static str, is_static: bool, props: Cell<*mut dyn AnyProps<'a>>, + placeholder: Cell>, }, // Comes in with string interpolation or from format_args, include_str, etc diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index c1c44712b..1ab099faf 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -1,3 +1,7 @@ +use std::task::Context; + +use futures_util::task::noop_waker_ref; + use crate::{ any_props::AnyProps, arena::ElementId, @@ -23,6 +27,7 @@ impl VirtualDom { height, props, tasks: self.pending_futures.clone(), + suspense_boundary: None, node_arena_1: BumpFrame::new(50), node_arena_2: BumpFrame::new(50), render_cnt: Default::default(), @@ -53,7 +58,7 @@ impl VirtualDom { let scope = &mut self.scopes[id.0]; scope.hook_idx.set(0); - let res = { + let mut new_nodes = { let props = unsafe { &mut *scope.props }; let props: &mut dyn AnyProps = unsafe { std::mem::transmute(props) }; let res: RenderReturn = props.render(scope); @@ -61,6 +66,20 @@ impl VirtualDom { res }; + // immediately resolve futures that can be resolved immediatelys + let res = match &mut new_nodes { + RenderReturn::Sync(_) => new_nodes, + RenderReturn::Async(fut) => { + use futures_util::FutureExt; + let mut cx = Context::from_waker(&noop_waker_ref()); + + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(nodes) => RenderReturn::Sync(nodes), + std::task::Poll::Pending => new_nodes, + } + } + }; + let frame = match scope.render_cnt % 2 { 0 => &mut scope.node_arena_1, 1 => &mut scope.node_arena_2, diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index fe73bbbbb..8a87658a6 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -11,7 +11,7 @@ use futures_util::Future; use crate::{ any_props::AnyProps, arena::ElementId, bump_frame::BumpFrame, future_container::FutureQueue, - innerlude::SchedulerMsg, lazynodes::LazyNodes, nodes::VNode, TaskId, + innerlude::SchedulerMsg, lazynodes::LazyNodes, nodes::VNode, suspense::Fiber, TaskId, }; pub struct Scope<'a, T = ()> { @@ -66,6 +66,8 @@ pub struct ScopeState { pub tasks: FutureQueue, + pub suspense_boundary: Option>, + pub props: *mut dyn AnyProps<'static>, } diff --git a/packages/core/src/subtree.rs b/packages/core/src/subtree.rs new file mode 100644 index 000000000..0d29f3d06 --- /dev/null +++ b/packages/core/src/subtree.rs @@ -0,0 +1,58 @@ +/* +This is a WIP module + +Subtrees allow the virtualdom to split up the mutation stream into smaller chunks which can be directed to different parts of the dom. +It's core to implementing multiwindow desktop support, portals, and alternative inline renderers like react-three-fiber. + +The primary idea is to give each renderer a linear element tree managed by Dioxus to maximize performance and minimize memory usage. +This can't be done if two renderers need to share the same native tree. +With subtrees, we have an entirely different slab of elements + +*/ + +use std::borrow::Cow; + +use slab::Slab; + +use crate::{ElementPath, ScopeId}; + +/// A collection of elements confined to a single scope under a chunk of the tree +/// +/// All elements in this collection are guaranteed to be in the same scope and share the same numbering +/// +/// This unit can be multithreaded +/// Whenever multiple subtrees are present, we can perform **parallel diffing** +pub struct Subtree { + id: usize, + namespace: Cow<'static, str>, + root: ScopeId, + elements: Slab, +} + +// fn app(cx: Scope) -> Element { +// // whenever a user connects, they get a new connection +// // this requires the virtualdom to be Send + Sync +// rsx! { +// ClientForEach(|req| rsx!{ +// Route {} +// Route {} +// Route {} +// Route {} +// Route {} +// Route {} +// }) + +// // windows.map(|w| { +// // WebviewWindow {} +// // WebviewWindow {} +// // WebviewWindow {} +// // WebviewWindow {} +// // }) + +// // if show_settings { +// // WebviewWindow { +// // Settings {} +// // } +// // } +// } +// } diff --git a/packages/core/src/suspense.rs b/packages/core/src/suspense.rs new file mode 100644 index 000000000..65dd8b3ca --- /dev/null +++ b/packages/core/src/suspense.rs @@ -0,0 +1,149 @@ +//! Container for polling suspended nodes +//! +//! Whenever a future is returns a value, we walk the tree upwards and check if any of the parents are suspended. + +use bumpalo::boxed::Box as BumpBox; +use futures_util::Future; +use std::{ + collections::{HashMap, HashSet}, + future::poll_fn, + pin::Pin, + task::Poll, +}; + +use crate::{ + factory::FiberLeaf, innerlude::Mutation, Element, ElementId, ScopeId, VNode, VirtualDom, +}; + +impl VirtualDom { + // todo: lots of hammering lifetimes here... + async fn wait_for_suspense(&mut self) { + let res = poll_fn(|cx| { + let all_suspended_complete = true; + + let suspended_scopes: Vec<_> = self.suspended_scopes.iter().copied().collect(); + + for scope in suspended_scopes { + let mut fiber = self.scopes[scope.0] + .suspense_boundary + .as_mut() + .expect(" A fiber to be present if the scope is suspended"); + + let mut fiber: &mut Fiber = unsafe { std::mem::transmute(fiber) }; + + let mutations = &mut fiber.mutations; + let mutations: &mut Vec = unsafe { std::mem::transmute(mutations) }; + + let keys = fiber.futures.keys().copied().collect::>(); + for loc in keys { + let fut = *fiber.futures.get_mut(&loc).unwrap(); + let fut = unsafe { &mut *fut }; + let fut: &mut FiberLeaf<'_> = unsafe { std::mem::transmute(fut) }; + + use futures_util::FutureExt; + + match fut.poll_unpin(cx) { + Poll::Ready(nodes) => { + // remove the future from the fiber + fiber.futures.remove(&loc).unwrap(); + + // set the original location to the new nodes + // todo!("set the original location to the new nodes"); + let template = nodes.unwrap(); + + let scope = &self.scopes[scope.0]; + let template = scope.bump().alloc(template); + let template: &VNode = unsafe { std::mem::transmute(template) }; + + // now create the template + self.create(mutations, template); + } + Poll::Pending => todo!("still working huh"), + } + } + + // let mut fiber = Pin::new(&mut fiber); + + // let mut scope = scope; + // let mut vnode = self.scopes[scope.0].vnode.take().unwrap(); + + // let mut vnode = Pin::new(&mut vnode); + + // let mut vnode = poll_fn(|cx| { + // let mut vnode = Pin::new(&mut vnode); + // let mut fiber = Pin::new(&mut fiber); + + // let res = vnode.as_mut().poll(cx); + + // if let Poll::Ready(res) = res { + // Poll::Ready(res) + // } else { + // Poll::Pending + // } + // }) + // .await; + + // self.scopes[scope.0].vnode = Some(vnode); + // self.scopes[scope.0].suspense_boundary = Some(fiber); + } + + match all_suspended_complete { + true => Poll::Ready(()), + false => Poll::Pending, + } + }); + + todo!() + } +} + +// impl SuspenseGenerator { +// async fn wait_for_work(&mut self) { +// use futures_util::future::{select, Either}; + +// // let scopes = &mut self.scopes; +// let suspense_status = poll_fn(|cx| { +// // let mut tasks = scopes.tasks.tasks.borrow_mut(); +// // tasks.retain(|_, task| task.as_mut().poll(cx).is_pending()); + +// match true { +// // match tasks.is_empty() { +// true => Poll::Ready(()), +// false => Poll::Pending, +// } +// }); + +// // Suspense { +// // maybe generate futures +// // only render when all the futures are ready +// // } + +// /* +// div { +// as1 {} +// as2 {} +// as3 {} +// } +// */ +// // match select(task_poll, self.channel.1.next()).await { +// // Either::Left((_, _)) => {} +// // Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()), +// // } +// } +// } + +#[derive(Default)] +pub struct Fiber<'a> { + // The work-in progress of this suspended tree + pub mutations: Vec>, + + // All the pending futures (DFS) + pub futures: + HashMap> + 'a>>>, +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug)] +pub struct LeafLocation { + pub scope: ScopeId, + pub element: ElementId, +} diff --git a/packages/core/src/virtualdom.rs b/packages/core/src/virtualdom.rs index d2ee2dd93..312d5ae85 100644 --- a/packages/core/src/virtualdom.rs +++ b/packages/core/src/virtualdom.rs @@ -7,6 +7,7 @@ use crate::future_container::FutureQueue; use crate::innerlude::SchedulerMsg; use crate::mutations::Mutation; use crate::nodes::{Template, TemplateId}; +use crate::suspense::Fiber; use crate::{ arena::ElementId, scopes::{ScopeId, ScopeState}, @@ -26,6 +27,7 @@ pub struct VirtualDom { pub(crate) pending_futures: FutureQueue, pub(crate) sender: UnboundedSender, pub(crate) receiver: UnboundedReceiver, + pub(crate) suspended_scopes: BTreeSet, } impl VirtualDom { @@ -40,6 +42,7 @@ impl VirtualDom { element_stack: vec![ElementId(0)], dirty_scopes: BTreeSet::new(), pending_futures: FutureQueue::new(sender.clone()), + suspended_scopes: BTreeSet::new(), receiver, sender, }; @@ -49,6 +52,9 @@ impl VirtualDom { let root = res.new_scope(props); + // the root component is always a suspense boundary for any async children + res.scopes[root.0].suspense_boundary = Some(Fiber::default()); + assert_eq!(root, ScopeId(0)); res @@ -56,8 +62,6 @@ impl VirtualDom { /// Render the virtualdom, without processing any suspense. pub fn rebuild<'a>(&'a mut self, mutations: &mut Vec>) { - // let root = self.scopes.get(0).unwrap(); - let root_node: &RenderReturn = self.run_scope(ScopeId(0)); let root_node: &RenderReturn = unsafe { std::mem::transmute(root_node) }; match root_node { @@ -104,3 +108,18 @@ impl Drop for VirtualDom { // self.drop_scope(ScopeId(0)); } } + +/* +div { + Window {} + Window {} +} + +edits -> Vec + +Subtree { + id: 0, + namespace: "react-three-fiber", + edits: [] +} +*/