diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 10367fdd1..05e2de912 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,7 +1,5 @@ use std::marker::PhantomData; -use std::future::Future; - use crate::{ factory::{ComponentReturn, RenderReturn}, innerlude::Scoped, @@ -25,17 +23,6 @@ where pub _marker: PhantomData, } -impl<'a> VComponentProps<'a, (), ()> { - pub fn new_empty(render_fn: fn(Scope) -> Element) -> Self { - Self { - render_fn, - memo: <() as PartialEq>::eq, - props: (), - _marker: PhantomData, - } - } -} - impl<'a, P, A, F: ComponentReturn<'a, A>> VComponentProps<'a, P, A, F> { pub(crate) fn new( render_fn: fn(Scope<'a, P>) -> F, diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index d4fbc22bb..fbfd9cd8f 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -8,6 +8,15 @@ pub struct ElementPath { pub element: usize, } +impl ElementPath { + pub fn null() -> Self { + Self { + template: std::ptr::null_mut(), + element: 0, + } + } +} + impl VirtualDom { pub fn next_element(&mut self, template: &VNode) -> ElementId { let entry = self.elements.vacant_entry(); diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs index d1b3a2027..f80cda4ae 100644 --- a/packages/core/src/bump_frame.rs +++ b/packages/core/src/bump_frame.rs @@ -21,4 +21,8 @@ impl BumpFrame { self.bump.reset(); self.node.set(std::ptr::null_mut()); } + + pub unsafe fn load_node<'b>(&self) -> &'b RenderReturn<'b> { + unsafe { std::mem::transmute(&*self.node.get()) } + } } diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs deleted file mode 100644 index d3bbbe09b..000000000 --- a/packages/core/src/component.rs +++ /dev/null @@ -1,11 +0,0 @@ -// pub trait IntoComponentType { -// fn into_component_type(self) -> ComponentType; -// } - -use std::marker::PhantomData; - -use std::future::Future; - -use crate::{scopes::Scope, Element}; - -pub type Component<'a, T = ()> = fn(Scope<'a, T>) -> Element<'a>; diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 30c9881cb..ce28d53a3 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -130,7 +130,10 @@ impl VirtualDom { ) { match *node { // Todo: create the children's template - TemplateNode::Dynamic(_) => mutations.push(CreatePlaceholder { id: ElementId(0) }), + TemplateNode::Dynamic(_) => { + let id = self.next_element(template); + mutations.push(CreatePlaceholder { id }) + } TemplateNode::Text(value) => mutations.push(CreateText { value }), TemplateNode::DynamicText { .. } => mutations.push(CreateText { value: "placeholder", @@ -220,7 +223,7 @@ impl VirtualDom { let split_off = unsafe { std::mem::transmute(split_off) }; - boundary_mut.mutations.mutations = split_off; + boundary_mut.mutations.edits = split_off; boundary_mut .waiting_on .extend(self.collected_leaves.drain(..)); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f5a51b6ba..3b7a10106 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,12 +1,12 @@ use std::any::Any; +use crate::factory::RenderReturn; use crate::innerlude::Mutations; use crate::virtual_dom::VirtualDom; use crate::{Attribute, AttributeValue, TemplateNode}; use crate::any_props::VComponentProps; -use crate::component::Component; use crate::mutations::Mutation; use crate::nodes::{DynamicNode, Template, TemplateId}; use crate::scopes::Scope; @@ -41,6 +41,62 @@ impl Ord for DirtyScope { impl<'b> VirtualDom { pub fn diff_scope(&mut self, mutations: &mut Mutations<'b>, scope: ScopeId) { let scope_state = &mut self.scopes[scope.0]; + + let cur_arena = scope_state.current_frame(); + let prev_arena = scope_state.previous_frame(); + + // relax the borrow checker + let cur_arena: &BumpFrame = unsafe { std::mem::transmute(cur_arena) }; + let prev_arena: &BumpFrame = unsafe { std::mem::transmute(prev_arena) }; + + // Make sure the nodes arent null (they've been set properly) + assert_ne!( + cur_arena.node.get(), + std::ptr::null_mut(), + "Call rebuild before diffing" + ); + assert_ne!( + prev_arena.node.get(), + std::ptr::null_mut(), + "Call rebuild before diffing" + ); + + self.scope_stack.push(scope); + let left = unsafe { prev_arena.load_node() }; + let right = unsafe { cur_arena.load_node() }; + self.diff_maybe_node(mutations, left, right); + self.scope_stack.pop(); + } + + fn diff_maybe_node( + &mut self, + m: &mut Mutations<'b>, + left: &'b RenderReturn<'b>, + right: &'b RenderReturn<'b>, + ) { + use RenderReturn::{Async, Sync}; + match (left, right) { + // diff + (Sync(Some(l)), Sync(Some(r))) => self.diff_node(m, l, r), + + // remove old with placeholder + (Sync(Some(l)), Sync(None)) | (Sync(Some(l)), Async(_)) => { + // + let id = self.next_element(l); // todo! + m.push(Mutation::CreatePlaceholder { id }); + self.drop_template(m, l, true); + } + + // remove placeholder with nodes + (Sync(None), Sync(Some(_))) => {} + (Async(_), Sync(Some(v))) => {} + + // nothing... + (Async(_), Async(_)) + | (Sync(None), Sync(None)) + | (Sync(None), Async(_)) + | (Async(_), Sync(None)) => {} + } } pub fn diff_node( diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 80a1e401b..6cb098e60 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -98,53 +98,9 @@ pub enum EventPriority { Low = 0, } -impl VirtualDom { - /// Returns None if no element could be found - pub fn handle_event(&mut self, event: &UiEvent) -> Option<()> { - let path = self.elements.get(event.element.0)?; - - let location = unsafe { &mut *path.template } - .dynamic_attrs - .iter() - .position(|attr| attr.mounted_element.get() == event.element)?; - - let mut index = Some((path.template, location)); - - let mut listeners = Vec::<&Attribute>::new(); - - while let Some((raw_parent, dyn_index)) = index { - let parent = unsafe { &mut *raw_parent }; - let path = parent.template.node_paths[dyn_index]; - - listeners.extend( - parent - .dynamic_attrs - .iter() - .enumerate() - .filter_map(|(idx, attr)| { - match is_path_ascendant(parent.template.node_paths[idx], path) { - true if attr.name == event.name => Some(attr), - _ => None, - } - }), - ); - - index = parent.parent; - } - - for listener in listeners { - if let AttributeValue::Listener(listener) = &listener.value { - (listener.borrow_mut())(&event.event) - } - } - - Some(()) - } -} - // ensures a strict descendant relationship // returns false if the paths are equal -fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool { +pub fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool { if small.len() >= big.len() { return false; } diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index b71dac515..fbe6bdf57 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -1,4 +1,4 @@ -use std::{cell::Cell, fmt::Arguments, pin::Pin}; +use std::{cell::Cell, fmt::Arguments}; use bumpalo::boxed::Box as BumpBox; use bumpalo::Bump; @@ -123,13 +123,6 @@ where } } -#[test] -fn takes_it() { - fn demo(cx: Scope) -> Element { - todo!() - } -} - pub enum RenderReturn<'a> { Sync(Element<'a>), Async(BumpBox<'a, dyn Future> + 'a>), @@ -144,8 +137,6 @@ impl<'a> RenderReturn<'a> { } } -pub type FiberLeaf<'a> = Pin> + 'a>>; - pub trait IntoVnode<'a, A = ()> { fn into_dynamic_node(self, cx: &'a ScopeState) -> VNode<'a>; } @@ -162,6 +153,12 @@ impl<'a, 'b> IntoVnode<'a> for VNode<'a> { } } +impl<'a, 'b> IntoVnode<'a> for Option> { + fn into_dynamic_node(self, _cx: &'a ScopeState) -> VNode<'a> { + self.expect("please allow optional nodes in") + } +} + impl<'a, 'b> IntoVnode<'a> for &'a VNode<'a> { fn into_dynamic_node(self, _cx: &'a ScopeState) -> VNode<'a> { VNode { diff --git a/packages/core/src/garbage.rs b/packages/core/src/garbage.rs index bc28f7a84..2f77dba8d 100644 --- a/packages/core/src/garbage.rs +++ b/packages/core/src/garbage.rs @@ -1,16 +1,22 @@ -use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode}; +use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode, Mutations}; -impl VirtualDom { +impl<'b> VirtualDom { pub fn drop_scope(&mut self, id: ScopeId) { - let scope = self.scopes.get(id.0).unwrap(); + // let scope = self.scopes.get(id.0).unwrap(); - let root = scope.root_node(); - let root = unsafe { std::mem::transmute(root) }; + // let root = scope.root_node(); + // let root = unsafe { std::mem::transmute(root) }; - self.drop_template(root); + // self.drop_template(root, false); + todo!() } - pub fn drop_template<'a>(&'a mut self, template: &'a VNode<'a>) { + pub fn drop_template( + &mut self, + mutations: &mut Mutations, + template: &'b VNode<'b>, + gen_roots: bool, + ) { for node in template.dynamic_nodes.iter() { match node { DynamicNode::Text { id, .. } => {} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 77fd8c452..77f9146e4 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,7 +1,6 @@ mod any_props; mod arena; mod bump_frame; -mod component; mod create; mod diff; mod events; @@ -77,7 +76,10 @@ pub use crate::innerlude::{ ElementId, ElementPath, EventPriority, + Fragment, LazyNodes, + Mutation, + Mutations, NodeFactory, Properties, Scope, @@ -100,9 +102,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Attribute, DynamicNode, Element, EventPriority, LazyNodes, NodeFactory, - Properties, Scope, ScopeId, ScopeState, Scoped, SuspenseBoundary, SuspenseContext, TaskId, - Template, TemplateAttribute, TemplateNode, UiEvent, VNode, VirtualDom, + fc_to_builder, Element, EventPriority, Fragment, LazyNodes, NodeFactory, Properties, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, UiEvent, + VNode, VirtualDom, }; } @@ -135,24 +137,3 @@ macro_rules! to_owned { let mut $es = $es.to_owned(); )*} } - -/// A helper macro for values into callbacks for async environements. -/// -/// -macro_rules! callback { - () => {}; -} - -/// Convert a hook into a hook with an implicit dependency list by analyzing the closure. -/// -/// ``` -/// // Convert hooks with annoying dependencies into... -/// -/// let val = use_effect(cx, (val,) |(val,)| println!("thing {val}")) -/// -/// // a simple closure -/// let val = use_effect!(cx, |val| async { println!("thing {val}")) }); -/// ``` -macro_rules! make_dep_fn { - () => {}; -} diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 5f76dc5ad..8e8affae0 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -4,14 +4,14 @@ use crate::arena::ElementId; pub struct Mutations<'a> { pub subtree: usize, pub template_mutations: Vec>, - pub mutations: Vec>, + pub edits: Vec>, } impl<'a> Mutations<'a> { pub fn new(subtree: usize) -> Self { Self { subtree, - mutations: Vec::new(), + edits: Vec::new(), template_mutations: Vec::new(), } } @@ -21,13 +21,13 @@ impl<'a> std::ops::Deref for Mutations<'a> { type Target = Vec>; fn deref(&self) -> &Self::Target { - &self.mutations + &self.edits } } impl std::ops::DerefMut for Mutations<'_> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.mutations + &mut self.edits } } @@ -35,7 +35,7 @@ impl std::ops::DerefMut for Mutations<'_> { each subtree has its own numbering scheme */ -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Mutation<'a> { SetAttribute { name: &'a str, diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index 44e58c9d0..43afff79c 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -64,36 +64,37 @@ impl<'a> Properties for FragmentProps<'a> { } } -// /// Create inline fragments using Component syntax. -// /// -// /// ## Details -// /// -// /// Fragments capture a series of children without rendering extra nodes. -// /// -// /// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and -// /// a key is needed to identify each item. -// /// -// /// ## Example -// /// -// /// ```rust, ignore -// /// rsx!{ -// /// Fragment { key: "abc" } -// /// } -// /// ``` -// /// -// /// ## Usage -// /// -// /// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. -// /// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments. -// /// -// /// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. -// /// -// /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. -// #[allow(non_upper_case_globals, non_snake_case)] -// pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { -// let i = cx.props.0.as_ref().map(|f| f.decouple()); -// cx.render(LazyNodes::new(|f| f.fragment_from_iter(i))) -// } +/// Create inline fragments using Component syntax. +/// +/// ## Details +/// +/// Fragments capture a series of children without rendering extra nodes. +/// +/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and +/// a key is needed to identify each item. +/// +/// ## Example +/// +/// ```rust, ignore +/// rsx!{ +/// Fragment { key: "abc" } +/// } +/// ``` +/// +/// ## Usage +/// +/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. +/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments. +/// +/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. +/// +/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. +#[allow(non_upper_case_globals, non_snake_case)] +pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { + // let i = cx.props.0.as_ref().map(|f| f.decouple()); + // cx.render(LazyNodes::new(|f| f.fragment_from_iter(i))) + todo!("Fragment") +} /// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus /// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index dbdbb9d58..a6442184d 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -14,16 +14,10 @@ pub use waker::RcWake; /// /// These messages control how the scheduler will process updates to the UI. #[derive(Debug)] -pub enum SchedulerMsg { - /// Events from the Renderer - Event, - +pub(crate) enum SchedulerMsg { /// Immediate updates from Components that mark them as dirty Immediate(ScopeId), - /// Mark all components as dirty and update them - DirtyAll, - /// A task has woken and needs to be progressed TaskNotified(TaskId), diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 8eb92b7fb..ecdce3c93 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -29,12 +29,11 @@ impl SuspenseBoundary { } } -pub struct SuspenseLeaf { +pub(crate) struct SuspenseLeaf { pub id: SuspenseId, pub scope_id: ScopeId, pub tx: futures_channel::mpsc::UnboundedSender, pub notified: Cell, - pub task: *mut dyn Future>, } diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index ec293a08c..f71431a84 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -57,11 +57,6 @@ impl Scheduler { pub fn remove(&self, id: TaskId) { // } - - // Aborts the future - pub fn abort(&self, id: TaskId) { - // - } } impl RcWake for LocalTask { diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index ff9afe79f..62462ff86 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -1,110 +1,92 @@ -use futures_util::{FutureExt, StreamExt}; +use futures_util::FutureExt; use std::task::{Context, Poll}; use crate::{ - diff::DirtyScope, factory::RenderReturn, innerlude::{Mutation, Mutations, SuspenseContext}, - VNode, VirtualDom, + ScopeId, TaskId, VNode, VirtualDom, }; -use super::{waker::RcWake, SchedulerMsg, SuspenseLeaf}; +use super::{waker::RcWake, SuspenseId, SuspenseLeaf}; impl VirtualDom { - /// Wait for futures internal to the virtualdom + /// Handle notifications by tasks inside the scheduler /// - /// This is cancel safe, so if the future is dropped, you can push events into the virtualdom - pub async fn wait_for_work(&mut self) { - // todo: make sure the scheduler queue is completely drained - loop { - match self.rx.next().await.unwrap() { - SchedulerMsg::Event => break, + /// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the + /// queue + pub fn handle_task_wakeup(&mut self, id: TaskId) { + let mut tasks = self.scheduler.tasks.borrow_mut(); + let task = &tasks[id.0]; - SchedulerMsg::Immediate(id) => { - let height = self.scopes[id.0].height; - self.dirty_scopes.insert(DirtyScope { height, id }); - break; - } + // If the task completes... + if task.progress() { + // Remove it from the scope so we dont try to double drop it when the scope dropes + self.scopes[task.scope.0].spawned_tasks.remove(&id); - SchedulerMsg::DirtyAll => todo!(), + // Remove it from the scheduler + tasks.remove(id.0); + } + } - SchedulerMsg::TaskNotified(id) => { - let mut tasks = self.scheduler.tasks.borrow_mut(); - let task = &tasks[id.0]; + pub fn handle_suspense_wakeup(&mut self, id: SuspenseId) { + println!("suspense notified"); - // If the task completes... - if task.progress() { - // Remove it from the scope so we dont try to double drop it when the scope dropes - self.scopes[task.scope.0].spawned_tasks.remove(&id); + let leaf = self + .scheduler + .leaves + .borrow_mut() + .get(id.0) + .unwrap() + .clone(); - // Remove it from the scheduler - tasks.remove(id.0); - } - } + let scope_id = leaf.scope_id; - SchedulerMsg::SuspenseNotified(id) => { - println!("suspense notified"); + // todo: cache the waker + let waker = leaf.waker(); + let mut cx = Context::from_waker(&waker); - let leaf = self - .scheduler - .leaves - .borrow_mut() - .get(id.0) - .unwrap() - .clone(); + // Safety: the future is always pinned to the bump arena + let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; + let as_pinned_mut = &mut pinned; - let scope_id = leaf.scope_id; + // the component finished rendering and gave us nodes + // we should attach them to that component and then render its children + // continue rendering the tree until we hit yet another suspended component + if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { + let boundary = &self.scopes[leaf.scope_id.0] + .consume_context::() + .unwrap(); - // todo: cache the waker - let waker = leaf.waker(); - let mut cx = Context::from_waker(&waker); + println!("ready pool"); - // Safety: the future is always pinned to the bump arena - let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; - let as_pinned_mut = &mut pinned; + let mut fiber = boundary.borrow_mut(); - // the component finished rendering and gave us nodes - // we should attach them to that component and then render its children - // continue rendering the tree until we hit yet another suspended component - if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { - let boundary = &self.scopes[leaf.scope_id.0] - .consume_context::() - .unwrap(); + println!( + "Existing mutations {:?}, scope {:?}", + fiber.mutations, fiber.id + ); - println!("ready pool"); + let scope = &mut self.scopes[scope_id.0]; + let arena = scope.current_frame(); - let mut fiber = boundary.borrow_mut(); + let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes)); + arena.node.set(ret); - println!( - "Existing mutations {:?}, scope {:?}", - fiber.mutations, fiber.id - ); + if let RenderReturn::Sync(Some(template)) = ret { + let mutations = &mut fiber.mutations; + let template: &VNode = unsafe { std::mem::transmute(template) }; + let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; - let scope = &mut self.scopes[scope_id.0]; - let arena = scope.current_arena(); + self.scope_stack.push(scope_id); + self.create(mutations, template); + self.scope_stack.pop(); - let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes)); - arena.node.set(ret); - - if let RenderReturn::Sync(Some(template)) = ret { - let mutations = &mut fiber.mutations; - let template: &VNode = unsafe { std::mem::transmute(template) }; - let mutations: &mut Mutations = - unsafe { std::mem::transmute(mutations) }; - - self.scope_stack.push(scope_id); - self.create(mutations, template); - self.scope_stack.pop(); - - println!("{:#?}", mutations); - } else { - println!("nodes arent right"); - } - } else { - println!("not ready"); - } - } + println!("{:#?}", mutations); + } else { + println!("nodes arent right"); } + } else { + println!("not ready"); } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 81bbd93c5..29e23396a 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -7,7 +7,6 @@ use std::{ }; use bumpalo::Bump; -use futures_channel::mpsc::UnboundedSender; use std::future::Future; use crate::{ @@ -69,20 +68,27 @@ pub struct ScopeState { } impl ScopeState { - pub fn current_arena(&self) -> &BumpFrame { + pub fn current_frame(&self) -> &BumpFrame { match self.render_cnt % 2 { 0 => &self.node_arena_1, 1 => &self.node_arena_2, _ => unreachable!(), } } + pub fn previous_frame(&self) -> &BumpFrame { + match self.render_cnt % 2 { + 1 => &self.node_arena_1, + 0 => &self.node_arena_2, + _ => unreachable!(), + } + } pub fn bump(&self) -> &Bump { - &self.current_arena().bump + &self.current_frame().bump } pub fn root_node<'a>(&'a self) -> &'a VNode<'a> { - let r = unsafe { &*self.current_arena().node.get() }; + let r = unsafe { &*self.current_frame().node.get() }; unsafe { std::mem::transmute(r) } } @@ -142,11 +148,6 @@ impl ScopeState { self.id } - /// Get a handle to the raw update scheduler channel - pub fn scheduler_channel(&self) -> UnboundedSender { - self.tasks.sender.clone() - } - /// Create a subscription that schedules a future render for the reference component /// /// ## Notice: you should prefer using [`schedule_update_any`] and [`scope_id`] diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 74ba68748..c5632b06f 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -1,17 +1,17 @@ -use crate::any_props::VComponentProps; -use crate::arena::ElementPath; -use crate::diff::DirtyScope; -use crate::factory::RenderReturn; -use crate::innerlude::{Mutations, Scheduler, SchedulerMsg}; -use crate::mutations::Mutation; -use crate::nodes::{Template, TemplateId}; use crate::{ + any_props::VComponentProps, arena::ElementId, + arena::ElementPath, + diff::DirtyScope, + factory::RenderReturn, + innerlude::{is_path_ascendant, Mutations, Scheduler, SchedulerMsg}, + mutations::Mutation, + nodes::{Template, TemplateId}, + scheduler::{SuspenseBoundary, SuspenseId}, scopes::{ScopeId, ScopeState}, + Attribute, AttributeValue, Element, Scope, SuspenseContext, UiEvent, }; -use crate::{scheduler, Element, Scope}; -use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use scheduler::{SuspenseBoundary, SuspenseId}; +use futures_util::{pin_mut, FutureExt, StreamExt}; use slab::Slab; use std::collections::{BTreeSet, HashMap}; use std::future::Future; @@ -56,56 +56,88 @@ use std::rc::Rc; /// let edits = vdom.rebuild(); /// ``` /// -/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler. +/// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data. /// /// ```rust, ignore -/// let channel = vdom.get_scheduler_channel(); -/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent { -/// // ... -/// })) +/// vdom.handle_event(event); /// ``` /// -/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. +/// While no events are ready, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. /// /// ```rust, ignore /// vdom.wait_for_work().await; /// ``` /// -/// Once work is ready, call [`VirtualDom::work_with_deadline`] to compute the differences between the previous and +/// Once work is ready, call [`VirtualDom::render_with_deadline`] to compute the differences between the previous and /// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be /// handled by the renderer. /// /// ```rust, ignore -/// let mutations = vdom.work_with_deadline(|| false); -/// for edit in mutations { -/// apply(edit); +/// let mutations = vdom.work_with_deadline(tokio::time::sleep(Duration::from_millis(100))); +/// +/// for edit in mutations.edits { +/// real_dom.apply(edit); /// } /// ``` /// +/// To not wait for suspense while diffing the VirtualDom, call [`VirtualDom::render_immediate`] or pass an immediately +/// ready future to [`VirtualDom::render_with_deadline`]. +/// +/// /// ## Building an event loop around Dioxus: /// /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. -/// -/// ```rust, ignore -/// fn App(cx: Scope) -> Element { +/// ```rust +/// fn app(cx: Scope) -> Element { /// cx.render(rsx!{ /// div { "Hello World" } /// }) /// } /// -/// async fn main() { -/// let mut dom = VirtualDom::new(App); +/// let dom = VirtualDom::new(app); /// -/// let mut inital_edits = dom.rebuild(); -/// apply_edits(inital_edits); +/// real_dom.apply(dom.rebuild()); /// -/// loop { -/// dom.wait_for_work().await; -/// let frame_timeout = TimeoutFuture::new(Duration::from_millis(16)); -/// let deadline = || (&mut frame_timeout).now_or_never(); -/// let edits = dom.run_with_deadline(deadline).await; -/// apply_edits(edits); +/// loop { +/// select! { +/// _ = dom.wait_for_work() => {} +/// evt = real_dom.wait_for_event() => dom.handle_event(evt), /// } +/// +/// real_dom.apply(dom.render_immediate()); +/// } +/// ``` +/// +/// ## Waiting for suspense +/// +/// Because Dioxus supports suspense, you can use it for server-side rendering, static site generation, and other usecases +/// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the +/// [`VirtualDom::render_with_deadline`] method: +/// +/// ```rust +/// let dom = VirtualDom::new(app); +/// +/// let deadline = tokio::time::sleep(Duration::from_millis(100)); +/// let edits = dom.render_with_deadline(deadline).await; +/// ``` +/// +/// ## Use with streaming +/// +/// If not all rendering is done by the deadline, it might be worthwhile to stream the rest later. To do this, we +/// suggest rendering with a deadline, and then looping between [`VirtualDom::wait_for_work`] and render_immediate until +/// no suspended work is left. +/// +/// ``` +/// let dom = VirtualDom::new(app); +/// +/// let deadline = tokio::time::sleep(Duration::from_millis(20)); +/// let edits = dom.render_with_deadline(deadline).await; +/// +/// real_dom.apply(edits); +/// +/// while dom.has_suspended_work() { +/// dom.wait_for_work().await; +/// real_dom.apply(dom.render_immediate()); /// } /// ``` pub struct VirtualDom { @@ -186,27 +218,7 @@ impl VirtualDom { where P: 'static, { - let channel = futures_channel::mpsc::unbounded(); - Self::new_with_props_and_scheduler(root, root_props, channel) - } - - /// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler - /// - /// 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. - /// - /// ```rust, ignore - /// let channel = futures_channel::mpsc::unbounded(); - /// let dom = VirtualDom::new_with_scheduler(Example, (), channel); - /// ``` - pub fn new_with_props_and_scheduler( - root: fn(Scope

) -> Element, - root_props: P, - (tx, rx): ( - UnboundedSender, - UnboundedReceiver, - ), - ) -> Self { + let (tx, rx) = futures_channel::mpsc::unbounded(); let mut dom = Self { rx, scheduler: Scheduler::new(tx), @@ -220,18 +232,138 @@ impl VirtualDom { finished_fibers: Vec::new(), }; - dom.new_scope(Box::into_raw(Box::new(VComponentProps::new( + let root = dom.new_scope(Box::into_raw(Box::new(VComponentProps::new( root, |_, _| unreachable!(), root_props, - )))) + )))); + // The root component is always a suspense boundary for any async children // This could be unexpected, so we might rethink this behavior - .provide_context(SuspenseBoundary::new(ScopeId(0))); + root.provide_context(SuspenseBoundary::new(ScopeId(0))); + + // the root element is always given element 0 + dom.elements.insert(ElementPath::null()); dom } + pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { + self.scopes.get(id.0) + } + + pub fn base_scope(&self) -> &ScopeState { + self.scopes.get(0).unwrap() + } + + fn mark_dirty_scope(&mut self, id: ScopeId) { + let height = self.scopes[id.0].height; + self.dirty_scopes.insert(DirtyScope { height, id }); + } + + fn is_scope_suspended(&self, id: ScopeId) -> bool { + !self.scopes[id.0] + .consume_context::() + .unwrap() + .borrow() + .waiting_on + .is_empty() + } + + /// Returns true if there is any suspended work left to be done. + pub fn has_suspended_work(&self) -> bool { + !self.scheduler.leaves.borrow().is_empty() + } + + /// Returns None if no element could be found + pub fn handle_event(&mut self, event: &UiEvent) -> Option<()> { + let path = self.elements.get(event.element.0)?; + + let location = unsafe { &mut *path.template } + .dynamic_attrs + .iter() + .position(|attr| attr.mounted_element.get() == event.element)?; + + let mut index = Some((path.template, location)); + + let mut listeners = Vec::<&Attribute>::new(); + + while let Some((raw_parent, dyn_index)) = index { + let parent = unsafe { &mut *raw_parent }; + let path = parent.template.node_paths[dyn_index]; + + listeners.extend( + parent + .dynamic_attrs + .iter() + .enumerate() + .filter_map(|(idx, attr)| { + match is_path_ascendant(parent.template.node_paths[idx], path) { + true if attr.name == event.name => Some(attr), + _ => None, + } + }), + ); + + index = parent.parent; + } + + for listener in listeners { + if let AttributeValue::Listener(listener) = &listener.value { + (listener.borrow_mut())(&event.event) + } + } + + Some(()) + } + + /// Wait for the scheduler to have any work. + /// + /// This method polls the internal future queue, waiting for suspense nodes, tasks, or other work. This completes when + /// any work is ready. If multiple scopes are marked dirty from a task or a suspense tree is finished, this method + /// will exit. + /// + /// This method is cancel-safe, so you're fine to discard the future in a select block. + /// + /// This lets us poll async tasks during idle periods without blocking the main thread. + /// + /// # Example + /// + /// ```rust, ignore + /// let dom = VirtualDom::new(App); + /// let sender = dom.get_scheduler_channel(); + /// ``` + pub async fn wait_for_work(&mut self) { + let mut some_msg = None; + + loop { + match some_msg.take() { + // If a bunch of messages are ready in a sequence, try to pop them off synchronously + Some(msg) => match msg { + SchedulerMsg::Immediate(id) => self.mark_dirty_scope(id), + SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), + SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), + }, + + // If they're not ready, then we should wait for them to be ready + None => { + match self.rx.try_next() { + Ok(Some(val)) => some_msg = Some(val), + Ok(None) => return, + Err(_) => { + // If we have any dirty scopes, or finished fiber trees then we should exit + if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() { + return; + } + + some_msg = self.rx.next().await + } + } + } + } + } + } + /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch. /// /// The diff machine expects the RealDom's stack to be the root of the application. @@ -268,22 +400,71 @@ impl VirtualDom { mutations } + /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress + /// suspended subtrees. + pub fn render_immediate(&mut self) -> Mutations { + // Build a waker that won't wake up since our deadline is already expired when it's polled + let waker = futures_util::task::noop_waker(); + let mut cx = std::task::Context::from_waker(&waker); + + // Now run render with deadline but dont even try to poll any async tasks + let fut = self.render_with_deadline(std::future::ready(())); + pin_mut!(fut); + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(mutations) => mutations, + std::task::Poll::Pending => panic!("render_immediate should never return pending"), + } + } + /// Render what you can given the timeline and then move on /// /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues. - pub async fn render_with_deadline( - &mut self, + /// + /// If no suspense trees are present + pub async fn render_with_deadline<'a>( + &'a mut self, deadline: impl Future, - ) -> Vec { - todo!() - } + ) -> Mutations<'a> { + use futures_util::future::{select, Either}; - pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { - self.scopes.get(id.0) - } + let mut mutations = Mutations::new(0); + pin_mut!(deadline); - pub fn base_scope(&self) -> &ScopeState { - self.scopes.get(0).unwrap() + loop { + // first, unload any complete suspense trees + for finished_fiber in self.finished_fibers.drain(..) { + let scope = &mut self.scopes[finished_fiber.0]; + let context = scope.has_context::().unwrap(); + let mut context = context.borrow_mut(); + mutations.extend(context.mutations.drain(..)); + } + + // Next, diff any dirty scopes + // We choose not to poll the deadline since we complete pretty quickly anyways + if let Some(dirty) = self.dirty_scopes.iter().next().cloned() { + self.dirty_scopes.remove(&dirty); + + // if the scope is currently suspended, then we should skip it, ignoring any tasks calling for an update + if !self.is_scope_suspended(dirty.id) { + self.run_scope(dirty.id); + self.diff_scope(&mut mutations, dirty.id); + } + } + + // Wait for suspense, or a deadline + if self.dirty_scopes.is_empty() { + if self.scheduler.leaves.borrow().is_empty() { + return mutations; + } + + let (work, deadline) = (self.wait_for_work(), &mut deadline); + pin_mut!(work); + + if let Either::Left((_, _)) = select(deadline, work).await { + return mutations; + } + } + } } } diff --git a/packages/core/tests/simple_syntax.rs b/packages/core/tests/simple_syntax.rs deleted file mode 100644 index b2c3c8e2e..000000000 --- a/packages/core/tests/simple_syntax.rs +++ /dev/null @@ -1,25 +0,0 @@ -use dioxus_core::prelude::*; - -fn app(cx: Scope) -> Element { - todo!(); - // render! { - // Suspend { - // delay: Duration::from_millis(100), - // fallback: rsx! { "Loading..." }, - // ChildAsync {} - // ChildAsync {} - // ChildAsync {} - // } - // } -} - -async fn ChildAsync(cx: Scope<'_>) -> Element { - todo!() -} - -#[test] -fn it_works() { - let mut dom = VirtualDom::new(app); - - let mut mutations = dom.rebuild(); -} diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index 45b5e2588..c92978118 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -6,26 +6,29 @@ use dioxus_core::*; async fn it_works() { let mut dom = VirtualDom::new(app); - let mutations = dom.rebuild(); + let _ = dom.rebuild(); - println!("mutations: {:?}", mutations); - - dom.wait_for_work().await; + tokio::select! { + _ = dom.wait_for_work() => {} + _ = tokio::time::sleep(Duration::from_millis(1000)) => {} + }; } fn app(cx: Scope) -> Element { - cx.spawn(async { - for x in 0..10 { - tokio::time::sleep(Duration::from_millis(500)).await; - println!("Hello, world! {x}"); - } - }); + cx.use_hook(|| { + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_millis(50)).await; + println!("Hello, world! {x}"); + } + }); - cx.spawn(async { - for x in 0..10 { - tokio::time::sleep(Duration::from_millis(250)).await; - println!("Hello, world does! {x}"); - } + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_millis(25)).await; + println!("Hello, world from second thread! {x}"); + } + }); }); None diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 0675cd2af..801ef7ac5 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -35,9 +35,6 @@ env_logger = "0.9.0" tokio = { version = "1.21.2", features = ["full"] } # dioxus-edit-stream = { path = "../edit-stream" } -[[bench]] -name = "create" -harness = false [[bench]] name = "jsframework" diff --git a/packages/dioxus/benches/create.rs b/packages/dioxus/benches/create.rs deleted file mode 100644 index f328e4d9d..000000000 --- a/packages/dioxus/benches/create.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/packages/dioxus/benches/jsframework.rs b/packages/dioxus/benches/jsframework.rs index 3a7198012..24ca2fe69 100644 --- a/packages/dioxus/benches/jsframework.rs +++ b/packages/dioxus/benches/jsframework.rs @@ -5,9 +5,14 @@ //! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence, //! we are measuring the overhead of Dioxus, not the performance of the "apply" phase. //! -//! On my MBP 2019: -//! - Dioxus takes 3ms to create 1_000 rows -//! - Dioxus takes 30ms to create 10_000 rows +//! +//! Pre-templates (Mac M1): +//! - 3ms to create 1_000 rows +//! - 30ms to create 10_000 rows +//! +//! Post-templates +//! - 580us to create 1_000 rows +//! - 6.2ms to create 10_000 rows //! //! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator. //! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster. @@ -20,25 +25,26 @@ criterion_group!(mbenches, create_rows); criterion_main!(mbenches); fn create_rows(c: &mut Criterion) { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let mut rng = SmallRng::from_entropy(); - render!(table { - tbody { - (0..10_000_usize).map(|f| { - let label = Label::new(&mut rng); - rsx!(Row { - row_id: f, - label: label + render!( + table { + tbody { + (0..10_000_usize).map(|f| { + let label = Label::new(&mut rng); + rsx!( Row { row_id: f, label: label } ) }) - }) + } } - }) - }; + ) + } c.bench_function("create rows", |b| { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + b.iter(|| { - let mut dom = VirtualDom::new(App); let g = dom.rebuild(); assert!(g.edits.len() > 1); }) diff --git a/packages/dioxus/examples/stress.rs b/packages/dioxus/examples/stress.rs new file mode 100644 index 000000000..22560543e --- /dev/null +++ b/packages/dioxus/examples/stress.rs @@ -0,0 +1,101 @@ +use dioxus::prelude::*; +use rand::prelude::*; + +fn main() { + let mut dom = VirtualDom::new(app); + + dom.rebuild(); + + for _ in 0..1000 { + dom.rebuild(); + } +} + +fn app(cx: Scope) -> Element { + let mut rng = SmallRng::from_entropy(); + + render! ( + table { + tbody { + (0..10_000_usize).map(|f| { + let label = Label::new(&mut rng); + rsx!( Row { row_id: f, label: label } ) + }) + } + } + ) +} + +#[derive(PartialEq, Props)] +struct RowProps { + row_id: usize, + label: Label, +} +fn Row(cx: Scope) -> Element { + let [adj, col, noun] = cx.props.label.0; + render! { + tr { + td { class:"col-md-1", "{cx.props.row_id}" } + td { class:"col-md-1", onclick: move |_| { /* run onselect */ }, + a { class: "lbl", "{adj}" "{col}" "{noun}" } + } + td { class: "col-md-1", + a { class: "remove", onclick: move |_| {/* remove */}, + span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" } + } + } + td { class: "col-md-6" } + } + } +} + +#[derive(PartialEq)] +struct Label([&'static str; 3]); + +impl Label { + fn new(rng: &mut SmallRng) -> Self { + Label([ + ADJECTIVES.choose(rng).unwrap(), + COLOURS.choose(rng).unwrap(), + NOUNS.choose(rng).unwrap(), + ]) + } +} + +static ADJECTIVES: &[&str] = &[ + "pretty", + "large", + "big", + "small", + "tall", + "short", + "long", + "handsome", + "plain", + "quaint", + "clean", + "elegant", + "easy", + "angry", + "crazy", + "helpful", + "mushy", + "odd", + "unsightly", + "adorable", + "important", + "inexpensive", + "cheap", + "expensive", + "fancy", +]; + +static COLOURS: &[&str] = &[ + "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", + "orange", +]; + +static NOUNS: &[&str] = &[ + "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", + "pizza", "mouse", "keyboard", +]; diff --git a/packages/dioxus/tests/rsx_syntax.rs b/packages/dioxus/tests/rsx_syntax.rs index fe5e9c171..9c5440508 100644 --- a/packages/dioxus/tests/rsx_syntax.rs +++ b/packages/dioxus/tests/rsx_syntax.rs @@ -1,5 +1,3 @@ -use dioxus::prelude::*; - fn basic_syntax_is_a_template(cx: Scope) -> Element { let asd = 123; let var = 123; @@ -23,8 +21,63 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { }) } +// imports come after the test since the rsx! macro is sensitive to its location in the file +// (the byte index is used to differentiate sub templates) +use dioxus::core::{ElementId, Mutation}; +use dioxus::prelude::*; + #[test] fn dual_stream() { let mut dom = VirtualDom::new(basic_syntax_is_a_template); - dbg!(dom.rebuild()); + let edits = dom.rebuild(); + + use Mutation::*; + assert_eq!( + edits.template_mutations, + vec![ + CreateElement { name: "div", namespace: None, id: ElementId(1) }, + SetAttribute { name: "class", value: "asd", id: ElementId(1) }, + CreateElement { name: "div", namespace: None, id: ElementId(2) }, + CreatePlaceholder { id: ElementId(3) }, + AppendChildren { m: 1 }, + CreateElement { name: "div", namespace: None, id: ElementId(4) }, + CreateElement { name: "h1", namespace: None, id: ElementId(5) }, + CreateText { value: "var" }, + AppendChildren { m: 1 }, + CreateElement { name: "p", namespace: None, id: ElementId(6) }, + CreateText { value: "you're great!" }, + AppendChildren { m: 1 }, + CreateElement { name: "div", namespace: None, id: ElementId(7) }, + SetAttribute { name: "background-color", value: "red", id: ElementId(7) }, + CreateElement { name: "h1", namespace: None, id: ElementId(8) }, + CreateText { value: "var" }, + AppendChildren { m: 1 }, + CreateElement { name: "div", namespace: None, id: ElementId(9) }, + CreateElement { name: "b", namespace: None, id: ElementId(10) }, + CreateText { value: "asd" }, + AppendChildren { m: 1 }, + CreateText { value: "not great" }, + AppendChildren { m: 2 }, + AppendChildren { m: 2 }, + CreateElement { name: "p", namespace: None, id: ElementId(11) }, + CreateText { value: "you're great!" }, + AppendChildren { m: 1 }, + AppendChildren { m: 4 }, + AppendChildren { m: 2 }, + SaveTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", m: 1 } + ] + ); + + assert_eq!( + edits.edits, + vec![ + LoadTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", index: 0 }, + AssignId { path: &[], id: ElementId(12) }, + SetAttribute { name: "class", value: "123", id: ElementId(12) }, + SetAttribute { name: "onclick", value: "asd", id: ElementId(12) }, // ---- todo: listeners + HydrateText { path: &[0, 0], value: "123", id: ElementId(13) }, + ReplacePlaceholder { m: 1, path: &[0, 0] }, + AppendChildren { m: 1 } + ] + ); } diff --git a/packages/dioxus/tests/suspense.rs b/packages/dioxus/tests/suspense.rs index 3a385fd37..1d29df962 100644 --- a/packages/dioxus/tests/suspense.rs +++ b/packages/dioxus/tests/suspense.rs @@ -1,6 +1,7 @@ use std::future::IntoFuture; use dioxus::prelude::*; +use dioxus_core::SuspenseBoundary; #[inline_props] fn suspense_boundary<'a>(cx: Scope<'a>, children: Element<'a>) -> Element { diff --git a/packages/dioxus/tests/task.rs b/packages/dioxus/tests/task.rs deleted file mode 100644 index f328e4d9d..000000000 --- a/packages/dioxus/tests/task.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {}