From 7b0dcb3206b113a559b982567d9c4e8e1e1763e5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 28 Jan 2024 01:30:33 -0800 Subject: [PATCH] Add flush_sync and pre_render methods to core --- packages/core/Cargo.toml | 2 + packages/core/README.md | 12 +- packages/core/src/arena.rs | 2 +- packages/core/src/diff/component.rs | 6 +- packages/core/src/global_context.rs | 59 +++++-- packages/core/src/lib.rs | 17 +- packages/core/src/nodes.rs | 4 +- packages/core/src/runtime.rs | 77 ++++----- packages/core/src/scope_arena.rs | 27 ++- packages/core/src/scope_context.rs | 21 ++- packages/core/src/scopes.rs | 8 +- packages/core/src/tasks.rs | 87 ++++++---- packages/core/src/virtual_dom.rs | 201 ++++++++--------------- packages/core/tests/task.rs | 114 +++++++++++-- packages/dioxus-tui/benches/update.rs | 6 +- packages/liveview/src/pool.rs | 15 +- packages/router/src/components/router.rs | 2 +- 17 files changed, 378 insertions(+), 282 deletions(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index a07dee04e..6a164b36f 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -14,12 +14,14 @@ rustc-hash = { workspace = true } longest-increasing-subsequence = "0.1.0" futures-util = { workspace = true, default-features = false, features = [ "alloc", + "std", ] } slab = { workspace = true } futures-channel = { workspace = true } tracing = { workspace = true } serde = { version = "1", features = ["derive"], optional = true } + [dev-dependencies] tokio = { workspace = true, features = ["full"] } dioxus = { workspace = true } diff --git a/packages/core/README.md b/packages/core/README.md index f09e379e6..16f39fad8 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -41,7 +41,7 @@ use dioxus::prelude::*; fn app() -> Element { rsx!{ div { "hello world" } - }) + } } fn main() { @@ -49,13 +49,8 @@ fn main() { let mut dom = VirtualDom::new(app); // The initial render of the dom will generate a stream of edits for the real dom to apply - let mutations = dom.rebuild(); - - // Somehow, you can apply these edits to the real dom - apply_edits_to_real_dom(mutations); + let mutations = dom.rebuild_to_vec(); } - -# fn apply_edits_to_real_dom(mutations: Mutations) {} ``` @@ -68,9 +63,6 @@ We can then wait for any asynchronous components or pending futures using the `w # async fn wait(mut dom: VirtualDom) { // Wait for the dom to be marked dirty internally dom.wait_for_work().await; - -// Or wait for a deadline and then collect edits -let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16))); # } ``` diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 602657810..365999b83 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -70,7 +70,7 @@ impl VirtualDom { pub(crate) fn drop_scope(&mut self, id: ScopeId) { let height = { let scope = self.scopes.remove(id.0); - let context = scope.context(); + let context = scope.state(); context.height }; diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 19ac6eb9a..a468b8ae1 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -82,7 +82,7 @@ impl VNode { tracing::trace!( "Memoized props for component {:#?} ({})", scope_id, - old_scope.context().name + old_scope.state().name ); return; } @@ -94,7 +94,7 @@ impl VNode { let new = dom.run_scope(scope_id); dom.diff_scope(to, scope_id, new); - let height = dom.runtime.get_context(scope_id).unwrap().height; + let height = dom.runtime.get_state(scope_id).unwrap().height; dom.dirty_scopes.remove(&DirtyScope { height, id: scope_id, @@ -130,7 +130,7 @@ impl VNode { // Load up a ScopeId for this vcomponent. If it's already mounted, then we can just use that let scope = dom .new_scope(component.props.duplicate(), component.name) - .context() + .state() .id; // Store the scope id for the next render diff --git a/packages/core/src/global_context.rs b/packages/core/src/global_context.rs index aae650135..5d5ba99d8 100644 --- a/packages/core/src/global_context.rs +++ b/packages/core/src/global_context.rs @@ -1,8 +1,6 @@ -use std::sync::Arc; - -use futures_util::Future; - use crate::{runtime::Runtime, Element, ScopeId, Task}; +use futures_util::{future::poll_fn, Future}; +use std::sync::Arc; /// Get the current scope id pub fn current_scope_id() -> Option { @@ -30,7 +28,7 @@ pub fn consume_context() -> T { /// Consume context from the current scope pub fn consume_context_from_scope(scope_id: ScopeId) -> Option { Runtime::with(|rt| { - rt.get_context(scope_id) + rt.get_state(scope_id) .and_then(|cx| cx.consume_context::()) }) .flatten() @@ -53,9 +51,7 @@ pub fn provide_root_context(value: T) -> Option { /// Suspends the current component pub fn suspend() -> Option { - Runtime::with_current_scope(|cx| { - cx.suspend(); - }); + Runtime::with_current_scope(|cx| cx.suspend()); None } @@ -87,7 +83,7 @@ pub fn remove_future(id: Task) { /// # Example /// /// ``` -/// use dioxus_core::ScopeState; +/// use dioxus_core::use_hook; /// /// // prints a greeting on the initial render /// pub fn use_hello_world() { @@ -98,6 +94,47 @@ pub fn use_hook(initializer: impl FnOnce() -> State) -> Runtime::with_current_scope(|cx| cx.use_hook(initializer)).expect("to be in a dioxus runtime") } +/// Push a function to be run before the next render +/// This is a hook and will always run, so you can't unschedule it +/// Will run for every progression of suspense, though this might change in the future +pub fn use_before_render(f: impl FnMut() + 'static) { + Runtime::with_current_scope(|cx| cx.push_before_render(f)); +} + +/// Wait for the virtualdom to finish its sync work before proceeding +/// +/// This is useful if you've just triggered an update and want to wait for it to finish before proceeding with valid +/// DOM nodes. +pub async fn flush_sync() { + let mut polled = false; + + let _task = + FlushKey(Runtime::with(|rt| rt.add_to_flush_table()).expect("to be in a dioxus runtime")); + + // Poll without giving the waker to anyone + // The runtime will manually wake this task up when it's ready + poll_fn(|_| { + if !polled { + polled = true; + futures_util::task::Poll::Pending + } else { + futures_util::task::Poll::Ready(()) + } + }) + .await; + + // If the the future got polled, then we don't need to prevent it from being dropped + // This would all be solved with generational indicies on tasks + std::mem::forget(_task); + + struct FlushKey(Task); + impl Drop for FlushKey { + fn drop(&mut self) { + Runtime::with(|rt| rt.flush_table.borrow_mut().remove(&self.0)); + } + } +} + /// Get the current render since the inception of this component /// /// This can be used as a helpful diagnostic when debugging hooks/renders, etc @@ -138,7 +175,7 @@ pub fn schedule_update_any() -> Arc { /// (created with [`use_effect`](crate::use_effect)). /// /// Example: -/// ```rust +/// ```rust, ignore /// use dioxus::prelude::*; /// /// fn app() -> Element { @@ -176,7 +213,7 @@ pub fn schedule_update_any() -> Arc { /// } /// }); /// -/// use_on_destroy({ +/// use_drop({ /// to_owned![original_scroll_position]; /// /// restore scroll to the top of the page /// move || { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 3b3689f3e..d34a5c6de 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -59,6 +59,7 @@ pub(crate) mod innerlude { /// example() /// ) /// ``` + /// /// ## React-Style /// ```rust, ignore /// fn Example(cx: Props) -> Element { @@ -86,14 +87,14 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, generation, - has_context, needs_update, parent_scope, provide_context, provide_root_context, + consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, flush_sync, + generation, has_context, needs_update, parent_scope, provide_context, provide_root_context, remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, suspend, - try_consume_context, use_drop, use_error_boundary, use_hook, use_hook_with_cleanup, - AnyValue, Attribute, Component, ComponentFunction, Element, ErrorBoundary, Event, - EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, - OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, - SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner, - VirtualDom, + try_consume_context, use_before_render, use_drop, use_error_boundary, use_hook, + use_hook_with_cleanup, AnyValue, Attribute, Component, ComponentFunction, Element, + ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue, + IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId, + ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw, + VNode, VNodeInner, VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c5f80a326..70dd3c246 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -105,7 +105,7 @@ pub struct VNodeInner { /// The inner list *must* be in the format [static named attributes, remaining dynamically named attributes]. /// /// For example: - /// ```rust + /// ```rust, ignore /// div { /// class: "{class}", /// ..attrs, @@ -116,7 +116,7 @@ pub struct VNodeInner { /// ``` /// /// Would be represented as: - /// ```rust + /// ```rust, ignore /// [ /// [class, every attribute in attrs sorted by name], // Slot 0 in the template /// [color], // Slot 1 in the template diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 2b2809019..efa7e8da2 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -1,12 +1,13 @@ +use rustc_hash::FxHashSet; + use crate::{ innerlude::{LocalTask, SchedulerMsg}, - scope_context::ScopeContext, + scope_context::Scope, scopes::ScopeId, Task, }; use std::{ cell::{Cell, Ref, RefCell}, - collections::VecDeque, rc::Rc, }; @@ -16,7 +17,7 @@ thread_local! { /// A global runtime that is shared across all scopes that provides the async runtime and context API pub struct Runtime { - pub(crate) scope_contexts: RefCell>>, + pub(crate) scope_states: RefCell>>, // We use this to track the current scope pub(crate) scope_stack: RefCell>, @@ -29,10 +30,10 @@ pub struct Runtime { /// Tasks created with cx.spawn pub(crate) tasks: RefCell>>, - /// Queued tasks that are waiting to be polled - pub(crate) queued_tasks: Rc>>, - pub(crate) sender: futures_channel::mpsc::UnboundedSender, + + // Tasks waiting to be manually resumed when we call wait_for_work + pub(crate) flush_table: RefCell>, } impl Runtime { @@ -40,11 +41,11 @@ impl Runtime { Rc::new(Self { sender, rendering: Cell::new(true), - scope_contexts: Default::default(), + scope_states: Default::default(), scope_stack: Default::default(), current_task: Default::default(), tasks: Default::default(), - queued_tasks: Rc::new(RefCell::new(VecDeque::new())), + flush_table: Default::default(), }) } @@ -54,35 +55,39 @@ impl Runtime { } /// Create a scope context. This slab is synchronized with the scope slab. - pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) { - let mut contexts = self.scope_contexts.borrow_mut(); - if contexts.len() <= id.0 { - contexts.resize_with(id.0 + 1, Default::default); + pub(crate) fn create_scope(&self, context: Scope) { + let id = context.id; + let mut scopes = self.scope_states.borrow_mut(); + if scopes.len() <= id.0 { + scopes.resize_with(id.0 + 1, Default::default); } - contexts[id.0] = Some(context); + scopes[id.0] = Some(context); } - pub(crate) fn remove_context(self: &Rc, id: ScopeId) { + pub(crate) fn remove_scope(self: &Rc, id: ScopeId) { { - let borrow = self.scope_contexts.borrow(); + let borrow = self.scope_states.borrow(); if let Some(scope) = &borrow[id.0] { let _runtime_guard = RuntimeGuard::new(self.clone()); // Manually drop tasks, hooks, and contexts inside of the runtime self.on_scope(id, || { - // Drop all spawned tasks + // Drop all spawned tasks - order doesn't matter since tasks don't rely on eachother + // In theory nested tasks might not like this for id in scope.spawned_tasks.take() { self.remove_task(id); } - // Drop all hooks - scope.hooks.take(); + // Drop all hooks in reverse order in case a hook depends on another hook. + for hook in scope.hooks.take().drain(..).rev() { + drop(hook); + } // Drop all contexts scope.shared_contexts.take(); }); } } - self.scope_contexts.borrow_mut()[id.0].take(); + self.scope_states.borrow_mut()[id.0].take(); } /// Get the current scope id @@ -104,11 +109,11 @@ impl Runtime { o } - /// Get the context for any scope given its ID + /// Get the state for any scope given its ID /// /// This is useful for inserting or removing contexts from a scope, or rendering out its root node - pub(crate) fn get_context(&self, id: ScopeId) -> Option> { - Ref::filter_map(self.scope_contexts.borrow(), |contexts| { + pub(crate) fn get_state(&self, id: ScopeId) -> Option> { + Ref::filter_map(self.scope_states.borrow(), |contexts| { contexts.get(id.0).and_then(|f| f.as_ref()) }) .ok() @@ -125,34 +130,22 @@ impl Runtime { } /// Runs a function with the current runtime - pub(crate) fn with(f: F) -> Option - where - F: FnOnce(&Runtime) -> R, - { - RUNTIMES.with(|stack| { - let stack = stack.borrow(); - stack.last().map(|r| f(r)) - }) + pub(crate) fn with(f: impl FnOnce(&Runtime) -> R) -> Option { + RUNTIMES.with(|stack| stack.borrow().last().map(|r| f(r))) } /// Runs a function with the current scope - pub(crate) fn with_current_scope(f: F) -> Option - where - F: FnOnce(&ScopeContext) -> R, - { + pub(crate) fn with_current_scope(f: impl FnOnce(&Scope) -> R) -> Option { Self::with(|rt| { rt.current_scope_id() - .and_then(|scope| rt.get_context(scope).map(|sc| f(&sc))) + .and_then(|scope| rt.get_state(scope).map(|sc| f(&sc))) }) .flatten() } /// Runs a function with the current scope - pub(crate) fn with_scope(scope: ScopeId, f: F) -> Option - where - F: FnOnce(&ScopeContext) -> R, - { - Self::with(|rt| rt.get_context(scope).map(|sc| f(&sc))).flatten() + pub(crate) fn with_scope(scope: ScopeId, f: impl FnOnce(&Scope) -> R) -> Option { + Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc))).flatten() } } @@ -182,7 +175,9 @@ impl Runtime { /// } /// /// fn Component(cx: ComponentProps) -> Element { -/// cx.use_hook(|| RuntimeGuard::new(cx.runtime.clone())); +/// use_hook(|| { +/// let _guard = RuntimeGuard::new(cx.runtime.clone()); +/// }); /// /// rsx! { div {} } /// } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index fe66adb8a..2a6e6aa00 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -2,7 +2,7 @@ use crate::{ any_props::{AnyProps, BoxedAnyProps}, innerlude::{DirtyScope, ScopeState}, nodes::RenderReturn, - scope_context::ScopeContext, + scope_context::Scope, scopes::ScopeId, virtual_dom::VirtualDom, }; @@ -11,7 +11,7 @@ impl VirtualDom { pub(super) fn new_scope(&mut self, props: BoxedAnyProps, name: &'static str) -> &ScopeState { let parent_id = self.runtime.current_scope_id(); let height = parent_id - .and_then(|parent_id| self.runtime.get_context(parent_id).map(|f| f.height + 1)) + .and_then(|parent_id| self.runtime.get_state(parent_id).map(|f| f.height + 1)) .unwrap_or(0); let entry = self.scopes.vacant_entry(); let id = ScopeId(entry.key()); @@ -19,34 +19,43 @@ impl VirtualDom { let scope = entry.insert(ScopeState { runtime: self.runtime.clone(), context_id: id, - props, last_rendered_node: Default::default(), }); - let context = ScopeContext::new(name, id, parent_id, height); - self.runtime.create_context_at(id, context); + self.runtime + .create_scope(Scope::new(name, id, parent_id, height)); scope } pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn { - self.runtime.scope_stack.borrow_mut().push(scope_id); + debug_assert!( + crate::Runtime::current().is_some(), + "Must be in a dioxus runtime" + ); + self.runtime.scope_stack.borrow_mut().push(scope_id); let scope = &self.scopes[scope_id.0]; let new_nodes = { - let context = scope.context(); + let context = scope.state(); + context.suspended.set(false); context.hook_index.set(0); + // Run all pre-render hooks + for pre_run in context.before_render.borrow_mut().iter_mut() { + pre_run(); + } + // safety: due to how we traverse the tree, we know that the scope is not currently aliased let props: &dyn AnyProps = &*scope.props; - let span = tracing::trace_span!("render", scope = %scope.context().name); + let span = tracing::trace_span!("render", scope = %scope.state().name); span.in_scope(|| props.render()) }; - let context = scope.context(); + let context = scope.state(); // And move the render generation forward by one context.render_count.set(context.render_count.get() + 1); diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index f3e4a87bf..b76803ed9 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -10,7 +10,7 @@ use std::{ /// A component's state separate from its props. /// /// This struct exists to provide a common interface for all scopes without relying on generics. -pub(crate) struct ScopeContext { +pub(crate) struct Scope { pub(crate) name: &'static str, pub(crate) id: ScopeId, pub(crate) parent_id: Option, @@ -21,13 +21,12 @@ pub(crate) struct ScopeContext { // Note: the order of the hook and context fields is important. The hooks field must be dropped before the contexts field in case a hook drop implementation tries to access a context. pub(crate) hooks: RefCell>>, pub(crate) hook_index: Cell, - pub(crate) shared_contexts: RefCell>>, - pub(crate) spawned_tasks: RefCell>, + pub(crate) before_render: RefCell>>, } -impl ScopeContext { +impl Scope { pub(crate) fn new( name: &'static str, id: ScopeId, @@ -45,6 +44,7 @@ impl ScopeContext { spawned_tasks: RefCell::new(FxHashSet::default()), hooks: RefCell::new(vec![]), hook_index: Cell::new(0), + before_render: RefCell::new(vec![]), } } @@ -109,7 +109,7 @@ impl ScopeContext { let mut search_parent = self.parent_id; let cur_runtime = Runtime::with(|runtime| { while let Some(parent_id) = search_parent { - let parent = runtime.get_context(parent_id).unwrap(); + let parent = runtime.get_state(parent_id).unwrap(); tracing::trace!( "looking for context {} ({:?}) in {}", std::any::type_name::(), @@ -212,7 +212,7 @@ impl ScopeContext { pub fn provide_root_context(&self, context: T) -> T { Runtime::with(|runtime| { runtime - .get_context(ScopeId::ROOT) + .get_state(ScopeId::ROOT) .unwrap() .provide_context(context) }) @@ -256,9 +256,10 @@ impl ScopeContext { /// # Example /// /// ``` + /// # use dioxus::prelude::*; /// // prints a greeting on the initial render /// pub fn use_hello_world() { - /// cx.use_hook(|| println!("Hello, world!")); + /// use_hook(|| println!("Hello, world!")); /// } /// ``` pub fn use_hook(&self, initializer: impl FnOnce() -> State) -> State { @@ -287,6 +288,10 @@ impl ScopeContext { ) } + pub fn push_before_render(&self, f: impl FnMut() + 'static) { + self.before_render.borrow_mut().push(Box::new(f)); + } + /// Get the current render since the inception of this component /// /// This can be used as a helpful diagnostic when debugging hooks/renders, etc @@ -320,7 +325,7 @@ impl ScopeId { /// Consume context from the current scope pub fn consume_context_from_scope(self, scope_id: ScopeId) -> Option { Runtime::with(|rt| { - rt.get_context(scope_id) + rt.get_state(scope_id) .and_then(|cx| cx.consume_context::()) }) .flatten() diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 2409f9c07..632b42fa3 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,5 +1,5 @@ use crate::{ - any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::ScopeContext, + any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::Scope, }; use std::{cell::Ref, fmt::Debug, rc::Rc}; @@ -39,7 +39,7 @@ pub struct ScopeState { impl Drop for ScopeState { fn drop(&mut self) { - self.runtime.remove_context(self.context_id); + self.runtime.remove_scope(self.context_id); } } @@ -63,7 +63,7 @@ impl ScopeState { self.last_rendered_node.as_ref() } - pub(crate) fn context(&self) -> Ref<'_, ScopeContext> { - self.runtime.get_context(self.context_id).unwrap() + pub(crate) fn state(&self) -> Ref<'_, Scope> { + self.runtime.get_state(self.context_id).unwrap() } } diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs index 91590d93a..db6ba7d30 100644 --- a/packages/core/src/tasks.rs +++ b/packages/core/src/tasks.rs @@ -1,10 +1,10 @@ use crate::innerlude::{remove_future, spawn, Runtime}; use crate::ScopeId; use futures_util::task::ArcWake; -use std::future::Future; use std::pin::Pin; use std::sync::Arc; use std::task::Waker; +use std::{cell::Cell, future::Future}; use std::{cell::RefCell, rc::Rc}; /// A task's unique identifier. @@ -34,6 +34,25 @@ impl Task { pub fn stop(self) { remove_future(self); } + + /// Pause the task. + pub fn pause(&self) { + Runtime::with(|rt| rt.tasks.borrow()[self.0].active.set(false)); + } + + /// Check if the task is paused. + pub fn paused(&self) -> bool { + Runtime::with(|rt| !rt.tasks.borrow()[self.0].active.get()).unwrap_or_default() + } + + /// Resume the task. + pub fn resume(&self) { + Runtime::with(|rt| { + // set the active flag, and then ping the scheduler to ensure the task gets queued + rt.tasks.borrow()[self.0].active.set(true); + _ = rt.sender.unbounded_send(SchedulerMsg::TaskNotified(*self)); + }); + } } impl Runtime { @@ -55,9 +74,10 @@ impl Runtime { let task_id = Task(entry.key()); let task = Rc::new(LocalTask { + scope, + active: Cell::new(true), parent: self.current_task(), task: RefCell::new(Box::pin(task)), - scope, waker: futures_util::task::waker(Arc::new(LocalTaskHandle { id: task_id, tx: self.sender.clone(), @@ -73,18 +93,33 @@ impl Runtime { debug_assert!(self.tasks.try_borrow_mut().is_ok()); debug_assert!(task.task.try_borrow_mut().is_ok()); - let mut cx = std::task::Context::from_waker(&task.waker); - - if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { - self.sender - .unbounded_send(SchedulerMsg::TaskNotified(task_id)) - .expect("Scheduler should exist"); - } + self.sender + .unbounded_send(SchedulerMsg::TaskNotified(task_id)) + .expect("Scheduler should exist"); task_id } + /// Get the currently running task + pub fn current_task(&self) -> Option { + self.current_task.get() + } + + /// Get the parent task of the given task, if it exists + pub fn parent_task(&self, task: Task) -> Option { + self.tasks.borrow().get(task.0)?.parent + } + + /// Add this task to the queue of tasks that will manually get poked when the scheduler is flushed + pub(crate) fn add_to_flush_table(&self) -> Task { + let value = self.current_task().unwrap(); + self.flush_table.borrow_mut().insert(value); + value + } + pub(crate) fn handle_task_wakeup(&self, id: Task) { + debug_assert!(Runtime::current().is_some(), "Must be in a dioxus runtime"); + let task = self.tasks.borrow().get(id.0).cloned(); // The task was removed from the scheduler, so we can just ignore it @@ -92,6 +127,11 @@ impl Runtime { return; }; + // If a task woke up but is paused, we can just ignore it + if !task.active.get() { + return; + } + let mut cx = std::task::Context::from_waker(&task.waker); // update the scope stack @@ -99,10 +139,9 @@ impl Runtime { self.rendering.set(false); self.current_task.set(Some(id)); - // If the task completes... if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { // Remove it from the scope so we dont try to double drop it when the scope dropes - self.get_context(task.scope) + self.get_state(task.scope) .unwrap() .spawned_tasks .borrow_mut() @@ -118,31 +157,11 @@ impl Runtime { self.current_task.set(None); } - /// Take a queued task from the scheduler - pub(crate) fn take_queued_task(&self) -> Option { - self.queued_tasks.borrow_mut().pop_front() - } - /// Drop the future with the given TaskId /// /// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you pub(crate) fn remove_task(&self, id: Task) -> Option> { - let task = self.tasks.borrow_mut().try_remove(id.0); - - // Remove the task from the queued tasks so we don't poll a different task with the same id - self.queued_tasks.borrow_mut().retain(|t| *t != id); - - task - } - - /// Get the currently running task - pub fn current_task(&self) -> Option { - self.current_task.get() - } - - /// Get the parent task of the given task, if it exists - pub fn parent_task(&self, task: Task) -> Option { - self.tasks.borrow().get(task.0)?.parent + self.tasks.borrow_mut().try_remove(id.0) } } @@ -152,6 +171,7 @@ pub(crate) struct LocalTask { parent: Option, task: RefCell + 'static>>>, waker: Waker, + active: Cell, } /// The type of message that can be sent to the scheduler. @@ -173,8 +193,7 @@ struct LocalTaskHandle { impl ArcWake for LocalTaskHandle { fn wake_by_ref(arc_self: &Arc) { - // This can fail if the scheduler has been dropped while the application is shutting down - let _ = arc_self + _ = arc_self .tx .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)); } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index daa844097..6cef9f69a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -13,12 +13,12 @@ use crate::{ nodes::{Template, TemplateId}, runtime::{Runtime, RuntimeGuard}, scopes::ScopeId, - AttributeValue, ComponentFunction, Element, Event, Mutations, Task, + AttributeValue, ComponentFunction, Element, Event, Mutations, }; -use futures_util::{pin_mut, StreamExt}; +use futures_util::StreamExt; use rustc_hash::{FxHashMap, FxHashSet}; use slab::Slab; -use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc}; +use std::{any::Any, collections::BTreeSet, rc::Rc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -29,7 +29,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc}; /// ```rust /// # use dioxus::prelude::*; /// -/// #[derive(Props, PartialEq)] +/// #[derive(Props, PartialEq, Clone)] /// struct AppProps { /// title: String /// } @@ -37,7 +37,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc}; /// fn app(cx: AppProps) -> Element { /// rsx!( /// div {"hello, {cx.title}"} -/// )) +/// ) /// } /// ``` /// @@ -47,7 +47,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc}; /// # #![allow(unused)] /// # use dioxus::prelude::*; /// -/// # #[derive(Props, PartialEq)] +/// # #[derive(Props, PartialEq, Clone)] /// # struct AppProps { /// # title: String /// # } @@ -60,26 +60,26 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc}; /// NavBar { routes: ROUTES } /// Title { "{cx.title}" } /// Footer {} -/// )) +/// ) /// } /// /// #[component] /// fn NavBar( routes: &'static str) -> Element { /// rsx! { /// div { "Routes: {routes}" } -/// }) +/// } /// } /// /// #[component] /// fn Footer() -> Element { -/// rsx! { div { "Footer" } }) +/// rsx! { div { "Footer" } } /// } /// /// #[component] -/// fn Title<'a>( children: Element) -> Element { +/// fn Title( children: Element) -> Element { /// rsx! { /// div { id: "title", {children} } -/// }) +/// } /// } /// ``` /// @@ -88,10 +88,10 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc}; /// /// ```rust /// # use dioxus::prelude::*; -/// # fn app() -> Element { rsx! { div {} }) } +/// # fn app() -> Element { rsx! { div {} } } /// /// let mut vdom = VirtualDom::new(app); -/// let edits = vdom.rebuild(); +/// let edits = vdom.rebuild_to_vec(); /// ``` /// /// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data. @@ -130,7 +130,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc}; /// fn app() -> Element { /// rsx! { /// div { "Hello World" } -/// }) +/// } /// } /// /// let dom = VirtualDom::new(app); @@ -247,7 +247,7 @@ impl VirtualDom { /// } /// /// fn Example(cx: SomeProps) -> Element { - /// rsx!{ div{ "hello {cx.name}" } }) + /// rsx!{ div { "hello {cx.name}" } } /// } /// /// let dom = VirtualDom::new(Example); @@ -291,7 +291,7 @@ impl VirtualDom { /// } /// /// fn Example(cx: SomeProps) -> Element { - /// rsx!{ div{ "hello {cx.name}" } }) + /// rsx!{ div{ "hello {cx.name}" } } /// } /// /// let dom = VirtualDom::new(Example); @@ -321,7 +321,7 @@ impl VirtualDom { let root = dom.new_scope(Box::new(root), "app"); // Unlike react, we provide a default error boundary that just renders the error as a string - root.context() + root.state() .provide_context(Rc::new(ErrorBoundary::new_in_scope(ScopeId::ROOT))); // the root element is always given element ID 0 since it's the container for the entire tree @@ -354,7 +354,7 @@ impl VirtualDom { /// /// This is useful for what is essentially dependency injection when building the app pub fn with_root_context(self, context: T) -> Self { - self.base_scope().context().provide_context(context); + self.base_scope().state().provide_context(context); self } @@ -362,14 +362,14 @@ impl VirtualDom { /// /// This method is useful for when you want to provide a context in your app without knowing its type pub fn insert_any_root_context(&mut self, context: Box) { - self.base_scope().context().provide_any_context(context); + self.base_scope().state().provide_any_context(context); } /// Manually mark a scope as requiring a re-render /// /// Whenever the Runtime "works", it will re-render this scope pub fn mark_dirty(&mut self, id: ScopeId) { - if let Some(context) = self.runtime.get_context(id) { + if let Some(context) = self.runtime.get_state(id) { let height = context.height(); tracing::trace!("Marking scope {:?} ({}) as dirty", id, context.name); self.dirty_scopes.insert(DirtyScope { height, id }); @@ -417,52 +417,50 @@ impl VirtualDom { /// /// ```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; + // Ping tasks waiting on the flush table - they're waiting for sync stuff to be done before progressing + self.clear_flush_table(); loop { - // If a bunch of messages are ready in a sequence, try to pop them off synchronously - if let Some(msg) = some_msg.take() { - match msg { - SchedulerMsg::Immediate(id) => self.mark_dirty(id), - SchedulerMsg::TaskNotified(task) => self.queue_task_wakeup(task), - } + // Process all events - Scopes are marked dirty, etc + self.process_events(); + + // Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures + if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() { + return; } - // If they're not ready, then we should wait for them to be ready - match self.rx.try_next() { - Ok(Some(val)) => some_msg = Some(val), - Ok(None) => return, - Err(_) => { - // Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures - let has_dirty_scopes = !self.dirty_scopes.is_empty(); + // Make sure we set the runtime since we're running user code + let _runtime = RuntimeGuard::new(self.runtime.clone()); - if !has_dirty_scopes { - // If we have no dirty scopes, then we should poll any tasks that have been notified - while let Some(task) = self.runtime.take_queued_task() { - self.handle_task_wakeup(task); - } - } + match self.rx.next().await.expect("channel should never close") { + SchedulerMsg::Immediate(id) => self.mark_dirty(id), + SchedulerMsg::TaskNotified(id) => self.runtime.handle_task_wakeup(id), + }; + } + } - // If we have any dirty scopes, or finished fiber trees then we should exit - if has_dirty_scopes || !self.suspended_scopes.is_empty() { - return; - } + fn clear_flush_table(&mut self) { + // Make sure we set the runtime since we're running user code + let _runtime = RuntimeGuard::new(self.runtime.clone()); - some_msg = self.rx.next().await - } - } + // Manually flush tasks that called `flush().await` + // Tasks that might've been waiting for `flush` finally have a chance to run to their next await point + for task in self.runtime.flush_table.take() { + self.runtime.handle_task_wakeup(task); } } /// Process all events in the queue until there are no more left pub fn process_events(&mut self) { + let _runtime = RuntimeGuard::new(self.runtime.clone()); + + // Prevent a task from deadlocking the runtime by repeatedly queueing itself while let Ok(Some(msg)) = self.rx.try_next() { match msg { SchedulerMsg::Immediate(id) => self.mark_dirty(id), - SchedulerMsg::TaskNotified(task) => self.queue_task_wakeup(task), + SchedulerMsg::TaskNotified(task) => self.runtime.handle_task_wakeup(task), } } } @@ -481,7 +479,7 @@ impl VirtualDom { if sync.template.get().name.rsplit_once(':').unwrap().0 == template.name.rsplit_once(':').unwrap().0 { - let context = scope.context(); + let context = scope.state(); let height = context.height; self.dirty_scopes.insert(DirtyScope { height, @@ -542,18 +540,25 @@ impl VirtualDom { /// suspended subtrees. pub fn render_immediate(&mut self, to: &mut impl WriteMutations) { self.flush_templates(to); - // 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(()), to); - pin_mut!(fut); + // Process any events that might be pending in the queue + self.process_events(); - // The root component is not allowed to be async - match fut.poll(&mut cx) { - std::task::Poll::Ready(mutations) => mutations, - std::task::Poll::Pending => panic!("render_immediate should never return pending"), + // Next, diff any dirty scopes + // We choose not to poll the deadline since we complete pretty quickly anyways + while let Some(dirty) = self.dirty_scopes.pop_first() { + // If the scope doesn't exist for whatever reason, then we should skip it + if !self.scopes.contains(dirty.id.0) { + continue; + } + + { + let _runtime = RuntimeGuard::new(self.runtime.clone()); + // Run the scope and get the mutations + let new_nodes = self.run_scope(dirty.id); + + self.diff_scope(to, dirty.id, new_nodes); + } } } @@ -567,72 +572,24 @@ impl VirtualDom { /// Render the virtual dom, waiting for all suspense to be finished /// /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content + /// + /// Tasks waiting to be flushed are *cleared* here *without running them* + /// This behavior is subject to change, but configured this way so use_future/use_memo/use_future won't react on the server pub async fn wait_for_suspense(&mut self) { loop { if self.suspended_scopes.is_empty() { return; } + // not sure if we should be doing this? + self.runtime.flush_table.borrow_mut().clear(); + self.wait_for_work().await; self.render_immediate(&mut NoOpMutations); } } - /// 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. - /// - /// If no suspense trees are present - pub async fn render_with_deadline( - &mut self, - deadline: impl Future, - to: &mut impl WriteMutations, - ) { - self.flush_templates(to); - pin_mut!(deadline); - - self.process_events(); - - loop { - // Next, diff any dirty scopes - // We choose not to poll the deadline since we complete pretty quickly anyways - while let Some(dirty) = self.dirty_scopes.pop_first() { - // If the scope doesn't exist for whatever reason, then we should skip it - if !self.scopes.contains(dirty.id.0) { - continue; - } - - { - let _runtime = RuntimeGuard::new(self.runtime.clone()); - // Run the scope and get the mutations - let new_nodes = self.run_scope(dirty.id); - - self.diff_scope(to, dirty.id, new_nodes); - } - } - - // Wait until the deadline is ready or we have work if there's no work ready - let work = self.wait_for_work(); - pin_mut!(work); - - use futures_util::future::{select, Either}; - if let Either::Left((_, _)) = select(&mut deadline, &mut work).await { - return; - } - } - } - - /// [`Self::render_with_deadline`] to a vector of mutations for testing purposes - pub async fn render_with_deadline_to_vec( - &mut self, - deadline: impl Future, - ) -> Mutations { - let mut mutations = Mutations::default(); - self.render_with_deadline(deadline, &mut mutations).await; - mutations - } - /// Get the current runtime pub fn runtime(&self) -> Rc { self.runtime.clone() @@ -645,20 +602,6 @@ impl VirtualDom { } } - /// Queue a task to be polled after all dirty scopes have been rendered - fn queue_task_wakeup(&mut self, id: Task) { - self.runtime.queued_tasks.borrow_mut().push_back(id); - } - - /// Handle notifications by tasks inside the scheduler - /// - /// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the - /// queue - fn handle_task_wakeup(&mut self, id: Task) { - let _runtime = RuntimeGuard::new(self.runtime.clone()); - self.runtime.handle_task_wakeup(id); - } - /* ------------------------ The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when @@ -764,7 +707,7 @@ impl Drop for VirtualDom { fn drop(&mut self) { // Drop all scopes in order of height let mut scopes = self.scopes.drain().collect::>(); - scopes.sort_by_key(|scope| scope.context().height); + scopes.sort_by_key(|scope| scope.state().height); for scope in scopes.into_iter().rev() { drop(scope); } diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index ea9f2ab13..8a6764246 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -1,11 +1,22 @@ //! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely -#[cfg(not(miri))] +use std::{sync::atomic::AtomicUsize, time::Duration}; + +use dioxus::prelude::*; + +async fn run_vdom(app: fn() -> Element) { + let mut dom = VirtualDom::new(app); + + dom.rebuild(&mut dioxus_core::NoOpMutations); + + tokio::select! { + _ = dom.wait_for_work() => {} + _ = tokio::time::sleep(Duration::from_millis(500)) => {} + }; +} + #[tokio::test] async fn it_works() { - use dioxus::prelude::*; - use std::{sync::atomic::AtomicUsize, time::Duration}; - static POLL_COUNT: AtomicUsize = AtomicUsize::new(0); fn app() -> Element { @@ -28,14 +39,7 @@ async fn it_works() { rsx!({ () }) } - let mut dom = VirtualDom::new(app); - - dom.rebuild(&mut dioxus_core::NoOpMutations); - - tokio::select! { - _ = dom.wait_for_work() => {} - _ = tokio::time::sleep(Duration::from_millis(500)) => {} - }; + run_vdom(app).await; // By the time the tasks are finished, we should've accumulated ticks from two tasks // Be warned that by setting the delay to too short, tokio might not schedule in the tasks @@ -44,3 +48,89 @@ async fn it_works() { 135 ); } + +/// Prove that yield_now doesn't cause a deadlock +#[tokio::test] +async fn yield_now_works() { + thread_local! { + static SEQUENCE: std::cell::RefCell> = std::cell::RefCell::new(Vec::new()); + } + + fn app() -> Element { + // these two tasks should yield to eachother + use_hook(|| { + spawn(async move { + for x in 0..10 { + tokio::task::yield_now().await; + SEQUENCE.with(|s| s.borrow_mut().push(1)); + } + }) + }); + + use_hook(|| { + spawn(async move { + for x in 0..10 { + tokio::task::yield_now().await; + SEQUENCE.with(|s| s.borrow_mut().push(2)); + } + }) + }); + + rsx!({ () }) + } + + run_vdom(app).await; + + SEQUENCE.with(|s| assert_eq!(s.borrow().len(), 20)); +} + +/// Ensure that calling wait_for_flush waits for dioxus to finish its syncrhonous work +#[tokio::test] +async fn flushing() { + thread_local! { + static SEQUENCE: std::cell::RefCell> = std::cell::RefCell::new(Vec::new()); + } + + fn app() -> Element { + use_hook(|| { + spawn(async move { + for x in 0..10 { + flush_sync().await; + SEQUENCE.with(|s| s.borrow_mut().push(1)); + } + }) + }); + + use_hook(|| { + spawn(async move { + for x in 0..10 { + flush_sync().await; + SEQUENCE.with(|s| s.borrow_mut().push(2)); + } + }) + }); + + rsx!({ () }) + } + + let mut dom = VirtualDom::new(app); + + dom.rebuild(&mut dioxus_core::NoOpMutations); + + let fut = async { + // Trigger the flush by waiting for work + for _ in 0..40 { + tokio::select! { + _ = dom.wait_for_work() => {} + _ = tokio::time::sleep(Duration::from_millis(1)) => {} + }; + } + }; + + tokio::select! { + _ = fut => {} + _ = tokio::time::sleep(Duration::from_millis(500)) => {} + }; + + SEQUENCE.with(|s| assert_eq!(s.borrow().len(), 20)); +} diff --git a/packages/dioxus-tui/benches/update.rs b/packages/dioxus-tui/benches/update.rs index 489bcd212..ce9682fba 100644 --- a/packages/dioxus-tui/benches/update.rs +++ b/packages/dioxus-tui/benches/update.rs @@ -65,7 +65,7 @@ struct BoxProps { alpha: f32, } #[allow(non_snake_case)] -fn Box(cx: Scope) -> Element { +fn Box(cx: ScopeState) -> Element { let count = use_signal(|| 0); let x = cx.props.x * 2; @@ -94,7 +94,7 @@ struct GridProps { update_count: usize, } #[allow(non_snake_case)] -fn Grid(cx: Scope) -> Element { +fn Grid(cx: ScopeState) -> Element { let size = cx.props.size; let count = use_signal(|| 0); let counts = use_signal(|| vec![0; size * size]); @@ -151,7 +151,7 @@ fn Grid(cx: Scope) -> Element { } } -fn app(cx: Scope) -> Element { +fn app(cx: ScopeState) -> Element { rsx! { div{ width: "100%", diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index acc128c32..aa4fe663b 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -6,7 +6,7 @@ use crate::{ LiveViewError, }; use dioxus_core::prelude::*; -use dioxus_html::{EventData, HtmlEvent, PlatformEventData}; +use dioxus_html::{select, EventData, HtmlEvent, PlatformEventData}; use dioxus_interpreter_js::MutationState; use futures_util::{pin_mut, SinkExt, StreamExt}; use serde::Serialize; @@ -227,11 +227,14 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li } } - vdom.render_with_deadline( - tokio::time::sleep(Duration::from_millis(10)), - &mut mutations, - ) - .await; + // wait for suspense to resolve in a 10ms window + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(10)) => {} + _ = vdom.wait_for_suspense() => {} + } + + // render the vdom + vdom.render_immediate(&mut mutations); if let Some(edits) = take_edits(&mut mutations) { ws.send(edits).await?; diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 858ff8ce2..af04fdacb 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -151,7 +151,7 @@ where #[cfg(feature = "serde")] /// A component that renders the current route. -pub fn Router(cx: Scope>) -> Element +pub fn Router(cx: ScopeState>) -> Element where ::Err: std::fmt::Display, R: serde::Serialize + serde::de::DeserializeOwned,