Add flush_sync and pre_render methods to core

This commit is contained in:
Jonathan Kelley committed 2024-01-28 01:30:33 -08:00
1 parent 3ccf520205
commit 7b0dcb3206
17 files changed
+378 -282

No files matched your search

+2
View File
@@ -14,12 +14,14 @@ rustc-hash = { workspace = true }
longest-increasing-subsequence = "0.1.0" longest-increasing-subsequence = "0.1.0"
futures-util = { workspace = true, default-features = false, features = [ futures-util = { workspace = true, default-features = false, features = [
"alloc", "alloc",
"std",
] } ] }
slab = { workspace = true } slab = { workspace = true }
futures-channel = { workspace = true } futures-channel = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true } dioxus = { workspace = true }
+2 -10
View File
@@ -41,7 +41,7 @@ use dioxus::prelude::*;
fn app() -> Element { fn app() -> Element {
rsx!{ rsx!{
div { "hello world" } div { "hello world" }
}) }
} }
fn main() { fn main() {
@@ -49,13 +49,8 @@ fn main() {
let mut dom = VirtualDom::new(app); let mut dom = VirtualDom::new(app);
// The initial render of the dom will generate a stream of edits for the real dom to apply // The initial render of the dom will generate a stream of edits for the real dom to apply
let mutations = dom.rebuild(); let mutations = dom.rebuild_to_vec();
// Somehow, you can apply these edits to the real dom
apply_edits_to_real_dom(mutations);
} }
# 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) { # async fn wait(mut dom: VirtualDom) {
// Wait for the dom to be marked dirty internally // Wait for the dom to be marked dirty internally
dom.wait_for_work().await; 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)));
# } # }
``` ```
+1 -1
View File
@@ -70,7 +70,7 @@ impl VirtualDom {
pub(crate) fn drop_scope(&mut self, id: ScopeId) { pub(crate) fn drop_scope(&mut self, id: ScopeId) {
let height = { let height = {
let scope = self.scopes.remove(id.0); let scope = self.scopes.remove(id.0);
let context = scope.context(); let context = scope.state();
context.height context.height
}; };
+3 -3
View File
@@ -82,7 +82,7 @@ impl VNode {
tracing::trace!( tracing::trace!(
"Memoized props for component {:#?} ({})", "Memoized props for component {:#?} ({})",
scope_id, scope_id,
old_scope.context().name old_scope.state().name
); );
return; return;
} }
@@ -94,7 +94,7 @@ impl VNode {
let new = dom.run_scope(scope_id); let new = dom.run_scope(scope_id);
dom.diff_scope(to, scope_id, new); 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 { dom.dirty_scopes.remove(&DirtyScope {
height, height,
id: scope_id, 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 // Load up a ScopeId for this vcomponent. If it's already mounted, then we can just use that
let scope = dom let scope = dom
.new_scope(component.props.duplicate(), component.name) .new_scope(component.props.duplicate(), component.name)
.context() .state()
.id; .id;
// Store the scope id for the next render // Store the scope id for the next render
+48 -11
View File
@@ -1,8 +1,6 @@
use std::sync::Arc;
use futures_util::Future;
use crate::{runtime::Runtime, Element, ScopeId, Task}; use crate::{runtime::Runtime, Element, ScopeId, Task};
use futures_util::{future::poll_fn, Future};
use std::sync::Arc;
/// Get the current scope id /// Get the current scope id
pub fn current_scope_id() -> Option<ScopeId> { pub fn current_scope_id() -> Option<ScopeId> {
@@ -30,7 +28,7 @@ pub fn consume_context<T: 'static + Clone>() -> T {
/// Consume context from the current scope /// Consume context from the current scope
pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> { pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> {
Runtime::with(|rt| { Runtime::with(|rt| {
rt.get_context(scope_id) rt.get_state(scope_id)
.and_then(|cx| cx.consume_context::<T>()) .and_then(|cx| cx.consume_context::<T>())
}) })
.flatten() .flatten()
@@ -53,9 +51,7 @@ pub fn provide_root_context<T: 'static + Clone>(value: T) -> Option<T> {
/// Suspends the current component /// Suspends the current component
pub fn suspend() -> Option<Element> { pub fn suspend() -> Option<Element> {
Runtime::with_current_scope(|cx| { Runtime::with_current_scope(|cx| cx.suspend());
cx.suspend();
});
None None
} }
@@ -87,7 +83,7 @@ pub fn remove_future(id: Task) {
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use dioxus_core::ScopeState; /// use dioxus_core::use_hook;
/// ///
/// // prints a greeting on the initial render /// // prints a greeting on the initial render
/// pub fn use_hello_world() { /// pub fn use_hello_world() {
@@ -98,6 +94,47 @@ pub fn use_hook<State: Clone + 'static>(initializer: impl FnOnce() -> State) ->
Runtime::with_current_scope(|cx| cx.use_hook(initializer)).expect("to be in a dioxus runtime") 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 /// Get the current render since the inception of this component
/// ///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc /// This can be used as a helpful diagnostic when debugging hooks/renders, etc
@@ -138,7 +175,7 @@ pub fn schedule_update_any() -> Arc<dyn Fn(ScopeId) + Send + Sync> {
/// (created with [`use_effect`](crate::use_effect)). /// (created with [`use_effect`](crate::use_effect)).
/// ///
/// Example: /// Example:
/// ```rust /// ```rust, ignore
/// use dioxus::prelude::*; /// use dioxus::prelude::*;
/// ///
/// fn app() -> Element { /// fn app() -> Element {
@@ -176,7 +213,7 @@ pub fn schedule_update_any() -> Arc<dyn Fn(ScopeId) + Send + Sync> {
/// } /// }
/// }); /// });
/// ///
/// use_on_destroy({ /// use_drop({
/// to_owned![original_scroll_position]; /// to_owned![original_scroll_position];
/// /// restore scroll to the top of the page /// /// restore scroll to the top of the page
/// move || { /// move || {
+9 -8
View File
@@ -59,6 +59,7 @@ pub(crate) mod innerlude {
/// example() /// example()
/// ) /// )
/// ``` /// ```
///
/// ## React-Style /// ## React-Style
/// ```rust, ignore /// ```rust, ignore
/// fn Example(cx: Props) -> Element { /// fn Example(cx: Props) -> Element {
@@ -86,14 +87,14 @@ pub use crate::innerlude::{
/// This includes types like [`Scope`], [`Element`], and [`Component`]. /// This includes types like [`Scope`], [`Element`], and [`Component`].
pub mod prelude { pub mod prelude {
pub use crate::innerlude::{ pub use crate::innerlude::{
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, generation, consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, flush_sync,
has_context, needs_update, parent_scope, provide_context, provide_root_context, generation, has_context, needs_update, parent_scope, provide_context, provide_root_context,
remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, suspend, 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, try_consume_context, use_before_render, use_drop, use_error_boundary, use_hook,
AnyValue, Attribute, Component, ComponentFunction, Element, ErrorBoundary, Event, use_hook_with_cleanup, AnyValue, Attribute, Component, ComponentFunction, Element,
EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue,
OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId,
SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner, ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw,
VirtualDom, VNode, VNodeInner, VirtualDom,
}; };
} }
+2 -2
View File
@@ -105,7 +105,7 @@ pub struct VNodeInner {
/// The inner list *must* be in the format [static named attributes, remaining dynamically named attributes]. /// The inner list *must* be in the format [static named attributes, remaining dynamically named attributes].
/// ///
/// For example: /// For example:
/// ```rust /// ```rust, ignore
/// div { /// div {
/// class: "{class}", /// class: "{class}",
/// ..attrs, /// ..attrs,
@@ -116,7 +116,7 @@ pub struct VNodeInner {
/// ``` /// ```
/// ///
/// Would be represented as: /// Would be represented as:
/// ```rust /// ```rust, ignore
/// [ /// [
/// [class, every attribute in attrs sorted by name], // Slot 0 in the template /// [class, every attribute in attrs sorted by name], // Slot 0 in the template
/// [color], // Slot 1 in the template /// [color], // Slot 1 in the template
+36 -41
View File
@@ -1,12 +1,13 @@
use rustc_hash::FxHashSet;
use crate::{ use crate::{
innerlude::{LocalTask, SchedulerMsg}, innerlude::{LocalTask, SchedulerMsg},
scope_context::ScopeContext, scope_context::Scope,
scopes::ScopeId, scopes::ScopeId,
Task, Task,
}; };
use std::{ use std::{
cell::{Cell, Ref, RefCell}, cell::{Cell, Ref, RefCell},
collections::VecDeque,
rc::Rc, 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 /// A global runtime that is shared across all scopes that provides the async runtime and context API
pub struct Runtime { pub struct Runtime {
pub(crate) scope_contexts: RefCell<Vec<Option<ScopeContext>>>, pub(crate) scope_states: RefCell<Vec<Option<Scope>>>,
// We use this to track the current scope // We use this to track the current scope
pub(crate) scope_stack: RefCell<Vec<ScopeId>>, pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
@@ -29,10 +30,10 @@ pub struct Runtime {
/// Tasks created with cx.spawn /// Tasks created with cx.spawn
pub(crate) tasks: RefCell<slab::Slab<Rc<LocalTask>>>, pub(crate) tasks: RefCell<slab::Slab<Rc<LocalTask>>>,
/// Queued tasks that are waiting to be polled
pub(crate) queued_tasks: Rc<RefCell<VecDeque<Task>>>,
pub(crate) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>, pub(crate) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
// Tasks waiting to be manually resumed when we call wait_for_work
pub(crate) flush_table: RefCell<FxHashSet<Task>>,
} }
impl Runtime { impl Runtime {
@@ -40,11 +41,11 @@ impl Runtime {
Rc::new(Self { Rc::new(Self {
sender, sender,
rendering: Cell::new(true), rendering: Cell::new(true),
scope_contexts: Default::default(), scope_states: Default::default(),
scope_stack: Default::default(), scope_stack: Default::default(),
current_task: Default::default(), current_task: Default::default(),
tasks: 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. /// Create a scope context. This slab is synchronized with the scope slab.
pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) { pub(crate) fn create_scope(&self, context: Scope) {
let mut contexts = self.scope_contexts.borrow_mut(); let id = context.id;
if contexts.len() <= id.0 { let mut scopes = self.scope_states.borrow_mut();
contexts.resize_with(id.0 + 1, Default::default); 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<Self>, id: ScopeId) { pub(crate) fn remove_scope(self: &Rc<Self>, id: ScopeId) {
{ {
let borrow = self.scope_contexts.borrow(); let borrow = self.scope_states.borrow();
if let Some(scope) = &borrow[id.0] { if let Some(scope) = &borrow[id.0] {
let _runtime_guard = RuntimeGuard::new(self.clone()); let _runtime_guard = RuntimeGuard::new(self.clone());
// Manually drop tasks, hooks, and contexts inside of the runtime // Manually drop tasks, hooks, and contexts inside of the runtime
self.on_scope(id, || { 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() { for id in scope.spawned_tasks.take() {
self.remove_task(id); self.remove_task(id);
} }
// Drop all hooks // Drop all hooks in reverse order in case a hook depends on another hook.
scope.hooks.take(); for hook in scope.hooks.take().drain(..).rev() {
drop(hook);
}
// Drop all contexts // Drop all contexts
scope.shared_contexts.take(); 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 /// Get the current scope id
@@ -104,11 +109,11 @@ impl Runtime {
o 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 /// 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<'_, ScopeContext>> { pub(crate) fn get_state(&self, id: ScopeId) -> Option<Ref<'_, Scope>> {
Ref::filter_map(self.scope_contexts.borrow(), |contexts| { Ref::filter_map(self.scope_states.borrow(), |contexts| {
contexts.get(id.0).and_then(|f| f.as_ref()) contexts.get(id.0).and_then(|f| f.as_ref())
}) })
.ok() .ok()
@@ -125,34 +130,22 @@ impl Runtime {
} }
/// Runs a function with the current runtime /// Runs a function with the current runtime
pub(crate) fn with<F, R>(f: F) -> Option<R> pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> Option<R> {
where RUNTIMES.with(|stack| stack.borrow().last().map(|r| f(r)))
F: FnOnce(&Runtime) -> R,
{
RUNTIMES.with(|stack| {
let stack = stack.borrow();
stack.last().map(|r| f(r))
})
} }
/// Runs a function with the current scope /// Runs a function with the current scope
pub(crate) fn with_current_scope<F, R>(f: F) -> Option<R> pub(crate) fn with_current_scope<R>(f: impl FnOnce(&Scope) -> R) -> Option<R> {
where
F: FnOnce(&ScopeContext) -> R,
{
Self::with(|rt| { Self::with(|rt| {
rt.current_scope_id() 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() .flatten()
} }
/// Runs a function with the current scope /// Runs a function with the current scope
pub(crate) fn with_scope<F, R>(scope: ScopeId, f: F) -> Option<R> pub(crate) fn with_scope<R>(scope: ScopeId, f: impl FnOnce(&Scope) -> R) -> Option<R> {
where Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc))).flatten()
F: FnOnce(&ScopeContext) -> R,
{
Self::with(|rt| rt.get_context(scope).map(|sc| f(&sc))).flatten()
} }
} }
@@ -182,7 +175,9 @@ impl Runtime {
/// } /// }
/// ///
/// fn Component(cx: ComponentProps) -> Element { /// fn Component(cx: ComponentProps) -> Element {
/// cx.use_hook(|| RuntimeGuard::new(cx.runtime.clone())); /// use_hook(|| {
/// let _guard = RuntimeGuard::new(cx.runtime.clone());
/// });
/// ///
/// rsx! { div {} } /// rsx! { div {} }
/// } /// }
+18 -9
View File
@@ -2,7 +2,7 @@ use crate::{
any_props::{AnyProps, BoxedAnyProps}, any_props::{AnyProps, BoxedAnyProps},
innerlude::{DirtyScope, ScopeState}, innerlude::{DirtyScope, ScopeState},
nodes::RenderReturn, nodes::RenderReturn,
scope_context::ScopeContext, scope_context::Scope,
scopes::ScopeId, scopes::ScopeId,
virtual_dom::VirtualDom, virtual_dom::VirtualDom,
}; };
@@ -11,7 +11,7 @@ impl VirtualDom {
pub(super) fn new_scope(&mut self, props: BoxedAnyProps, name: &'static str) -> &ScopeState { pub(super) fn new_scope(&mut self, props: BoxedAnyProps, name: &'static str) -> &ScopeState {
let parent_id = self.runtime.current_scope_id(); let parent_id = self.runtime.current_scope_id();
let height = parent_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); .unwrap_or(0);
let entry = self.scopes.vacant_entry(); let entry = self.scopes.vacant_entry();
let id = ScopeId(entry.key()); let id = ScopeId(entry.key());
@@ -19,34 +19,43 @@ impl VirtualDom {
let scope = entry.insert(ScopeState { let scope = entry.insert(ScopeState {
runtime: self.runtime.clone(), runtime: self.runtime.clone(),
context_id: id, context_id: id,
props, props,
last_rendered_node: Default::default(), last_rendered_node: Default::default(),
}); });
let context = ScopeContext::new(name, id, parent_id, height); self.runtime
self.runtime.create_context_at(id, context); .create_scope(Scope::new(name, id, parent_id, height));
scope scope
} }
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn { 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 scope = &self.scopes[scope_id.0];
let new_nodes = { let new_nodes = {
let context = scope.context(); let context = scope.state();
context.suspended.set(false); context.suspended.set(false);
context.hook_index.set(0); 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 // safety: due to how we traverse the tree, we know that the scope is not currently aliased
let props: &dyn AnyProps = &*scope.props; 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()) span.in_scope(|| props.render())
}; };
let context = scope.context(); let context = scope.state();
// And move the render generation forward by one // And move the render generation forward by one
context.render_count.set(context.render_count.get() + 1); context.render_count.set(context.render_count.get() + 1);
+13 -8
View File
@@ -10,7 +10,7 @@ use std::{
/// A component's state separate from its props. /// A component's state separate from its props.
/// ///
/// This struct exists to provide a common interface for all scopes without relying on generics. /// 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) name: &'static str,
pub(crate) id: ScopeId, pub(crate) id: ScopeId,
pub(crate) parent_id: Option<ScopeId>, pub(crate) parent_id: Option<ScopeId>,
@@ -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. // 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<Vec<Box<dyn Any>>>, pub(crate) hooks: RefCell<Vec<Box<dyn Any>>>,
pub(crate) hook_index: Cell<usize>, pub(crate) hook_index: Cell<usize>,
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>, pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>, pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>,
pub(crate) before_render: RefCell<Vec<Box<dyn FnMut()>>>,
} }
impl ScopeContext { impl Scope {
pub(crate) fn new( pub(crate) fn new(
name: &'static str, name: &'static str,
id: ScopeId, id: ScopeId,
@@ -45,6 +44,7 @@ impl ScopeContext {
spawned_tasks: RefCell::new(FxHashSet::default()), spawned_tasks: RefCell::new(FxHashSet::default()),
hooks: RefCell::new(vec![]), hooks: RefCell::new(vec![]),
hook_index: Cell::new(0), hook_index: Cell::new(0),
before_render: RefCell::new(vec![]),
} }
} }
@@ -109,7 +109,7 @@ impl ScopeContext {
let mut search_parent = self.parent_id; let mut search_parent = self.parent_id;
let cur_runtime = Runtime::with(|runtime| { let cur_runtime = Runtime::with(|runtime| {
while let Some(parent_id) = search_parent { 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!( tracing::trace!(
"looking for context {} ({:?}) in {}", "looking for context {} ({:?}) in {}",
std::any::type_name::<T>(), std::any::type_name::<T>(),
@@ -212,7 +212,7 @@ impl ScopeContext {
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T { pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
Runtime::with(|runtime| { Runtime::with(|runtime| {
runtime runtime
.get_context(ScopeId::ROOT) .get_state(ScopeId::ROOT)
.unwrap() .unwrap()
.provide_context(context) .provide_context(context)
}) })
@@ -256,9 +256,10 @@ impl ScopeContext {
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use dioxus::prelude::*;
/// // prints a greeting on the initial render /// // prints a greeting on the initial render
/// pub fn use_hello_world() { /// pub fn use_hello_world() {
/// cx.use_hook(|| println!("Hello, world!")); /// use_hook(|| println!("Hello, world!"));
/// } /// }
/// ``` /// ```
pub fn use_hook<State: Clone + 'static>(&self, initializer: impl FnOnce() -> State) -> State { pub fn use_hook<State: Clone + 'static>(&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 /// Get the current render since the inception of this component
/// ///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc /// 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 /// Consume context from the current scope
pub fn consume_context_from_scope<T: 'static + Clone>(self, scope_id: ScopeId) -> Option<T> { pub fn consume_context_from_scope<T: 'static + Clone>(self, scope_id: ScopeId) -> Option<T> {
Runtime::with(|rt| { Runtime::with(|rt| {
rt.get_context(scope_id) rt.get_state(scope_id)
.and_then(|cx| cx.consume_context::<T>()) .and_then(|cx| cx.consume_context::<T>())
}) })
.flatten() .flatten()
+4 -4
View File
@@ -1,5 +1,5 @@
use crate::{ 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}; use std::{cell::Ref, fmt::Debug, rc::Rc};
@@ -39,7 +39,7 @@ pub struct ScopeState {
impl Drop for ScopeState { impl Drop for ScopeState {
fn drop(&mut self) { 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() self.last_rendered_node.as_ref()
} }
pub(crate) fn context(&self) -> Ref<'_, ScopeContext> { pub(crate) fn state(&self) -> Ref<'_, Scope> {
self.runtime.get_context(self.context_id).unwrap() self.runtime.get_state(self.context_id).unwrap()
} }
} }
+53 -34
View File
@@ -1,10 +1,10 @@
use crate::innerlude::{remove_future, spawn, Runtime}; use crate::innerlude::{remove_future, spawn, Runtime};
use crate::ScopeId; use crate::ScopeId;
use futures_util::task::ArcWake; use futures_util::task::ArcWake;
use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::task::Waker; use std::task::Waker;
use std::{cell::Cell, future::Future};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
/// A task's unique identifier. /// A task's unique identifier.
@@ -34,6 +34,25 @@ impl Task {
pub fn stop(self) { pub fn stop(self) {
remove_future(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 { impl Runtime {
@@ -55,9 +74,10 @@ impl Runtime {
let task_id = Task(entry.key()); let task_id = Task(entry.key());
let task = Rc::new(LocalTask { let task = Rc::new(LocalTask {
scope,
active: Cell::new(true),
parent: self.current_task(), parent: self.current_task(),
task: RefCell::new(Box::pin(task)), task: RefCell::new(Box::pin(task)),
scope,
waker: futures_util::task::waker(Arc::new(LocalTaskHandle { waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
id: task_id, id: task_id,
tx: self.sender.clone(), tx: self.sender.clone(),
@@ -73,18 +93,33 @@ impl Runtime {
debug_assert!(self.tasks.try_borrow_mut().is_ok()); debug_assert!(self.tasks.try_borrow_mut().is_ok());
debug_assert!(task.task.try_borrow_mut().is_ok()); debug_assert!(task.task.try_borrow_mut().is_ok());
let mut cx = std::task::Context::from_waker(&task.waker); self.sender
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { .expect("Scheduler should exist");
self.sender
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
.expect("Scheduler should exist");
}
task_id task_id
} }
/// Get the currently running task
pub fn current_task(&self) -> Option<Task> {
self.current_task.get()
}
/// Get the parent task of the given task, if it exists
pub fn parent_task(&self, task: Task) -> Option<Task> {
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) { 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(); let task = self.tasks.borrow().get(id.0).cloned();
// The task was removed from the scheduler, so we can just ignore it // The task was removed from the scheduler, so we can just ignore it
@@ -92,6 +127,11 @@ impl Runtime {
return; 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); let mut cx = std::task::Context::from_waker(&task.waker);
// update the scope stack // update the scope stack
@@ -99,10 +139,9 @@ impl Runtime {
self.rendering.set(false); self.rendering.set(false);
self.current_task.set(Some(id)); self.current_task.set(Some(id));
// If the task completes...
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { 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 // 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() .unwrap()
.spawned_tasks .spawned_tasks
.borrow_mut() .borrow_mut()
@@ -118,31 +157,11 @@ impl Runtime {
self.current_task.set(None); self.current_task.set(None);
} }
/// Take a queued task from the scheduler
pub(crate) fn take_queued_task(&self) -> Option<Task> {
self.queued_tasks.borrow_mut().pop_front()
}
/// Drop the future with the given TaskId /// 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 /// 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<Rc<LocalTask>> { pub(crate) fn remove_task(&self, id: Task) -> Option<Rc<LocalTask>> {
let task = self.tasks.borrow_mut().try_remove(id.0); 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<Task> {
self.current_task.get()
}
/// Get the parent task of the given task, if it exists
pub fn parent_task(&self, task: Task) -> Option<Task> {
self.tasks.borrow().get(task.0)?.parent
} }
} }
@@ -152,6 +171,7 @@ pub(crate) struct LocalTask {
parent: Option<Task>, parent: Option<Task>,
task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>, task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
waker: Waker, waker: Waker,
active: Cell<bool>,
} }
/// The type of message that can be sent to the scheduler. /// The type of message that can be sent to the scheduler.
@@ -173,8 +193,7 @@ struct LocalTaskHandle {
impl ArcWake for LocalTaskHandle { impl ArcWake for LocalTaskHandle {
fn wake_by_ref(arc_self: &Arc<Self>) { fn wake_by_ref(arc_self: &Arc<Self>) {
// This can fail if the scheduler has been dropped while the application is shutting down _ = arc_self
let _ = arc_self
.tx .tx
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)); .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
} }
+72 -129
View File
@@ -13,12 +13,12 @@ use crate::{
nodes::{Template, TemplateId}, nodes::{Template, TemplateId},
runtime::{Runtime, RuntimeGuard}, runtime::{Runtime, RuntimeGuard},
scopes::ScopeId, 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 rustc_hash::{FxHashMap, FxHashSet};
use slab::Slab; 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. /// 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 /// ```rust
/// # use dioxus::prelude::*; /// # use dioxus::prelude::*;
/// ///
/// #[derive(Props, PartialEq)] /// #[derive(Props, PartialEq, Clone)]
/// struct AppProps { /// struct AppProps {
/// title: String /// title: String
/// } /// }
@@ -37,7 +37,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
/// fn app(cx: AppProps) -> Element { /// fn app(cx: AppProps) -> Element {
/// rsx!( /// rsx!(
/// div {"hello, {cx.title}"} /// div {"hello, {cx.title}"}
/// )) /// )
/// } /// }
/// ``` /// ```
/// ///
@@ -47,7 +47,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
/// # #![allow(unused)] /// # #![allow(unused)]
/// # use dioxus::prelude::*; /// # use dioxus::prelude::*;
/// ///
/// # #[derive(Props, PartialEq)] /// # #[derive(Props, PartialEq, Clone)]
/// # struct AppProps { /// # struct AppProps {
/// # title: String /// # title: String
/// # } /// # }
@@ -60,26 +60,26 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
/// NavBar { routes: ROUTES } /// NavBar { routes: ROUTES }
/// Title { "{cx.title}" } /// Title { "{cx.title}" }
/// Footer {} /// Footer {}
/// )) /// )
/// } /// }
/// ///
/// #[component] /// #[component]
/// fn NavBar( routes: &'static str) -> Element { /// fn NavBar( routes: &'static str) -> Element {
/// rsx! { /// rsx! {
/// div { "Routes: {routes}" } /// div { "Routes: {routes}" }
/// }) /// }
/// } /// }
/// ///
/// #[component] /// #[component]
/// fn Footer() -> Element { /// fn Footer() -> Element {
/// rsx! { div { "Footer" } }) /// rsx! { div { "Footer" } }
/// } /// }
/// ///
/// #[component] /// #[component]
/// fn Title<'a>( children: Element) -> Element { /// fn Title( children: Element) -> Element {
/// rsx! { /// rsx! {
/// div { id: "title", {children} } /// div { id: "title", {children} }
/// }) /// }
/// } /// }
/// ``` /// ```
/// ///
@@ -88,10 +88,10 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
/// ///
/// ```rust /// ```rust
/// # use dioxus::prelude::*; /// # use dioxus::prelude::*;
/// # fn app() -> Element { rsx! { div {} }) } /// # fn app() -> Element { rsx! { div {} } }
/// ///
/// let mut vdom = VirtualDom::new(app); /// 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. /// 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 { /// fn app() -> Element {
/// rsx! { /// rsx! {
/// div { "Hello World" } /// div { "Hello World" }
/// }) /// }
/// } /// }
/// ///
/// let dom = VirtualDom::new(app); /// let dom = VirtualDom::new(app);
@@ -247,7 +247,7 @@ impl VirtualDom {
/// } /// }
/// ///
/// fn Example(cx: SomeProps) -> Element { /// fn Example(cx: SomeProps) -> Element {
/// rsx!{ div{ "hello {cx.name}" } }) /// rsx!{ div { "hello {cx.name}" } }
/// } /// }
/// ///
/// let dom = VirtualDom::new(Example); /// let dom = VirtualDom::new(Example);
@@ -291,7 +291,7 @@ impl VirtualDom {
/// } /// }
/// ///
/// fn Example(cx: SomeProps) -> Element { /// fn Example(cx: SomeProps) -> Element {
/// rsx!{ div{ "hello {cx.name}" } }) /// rsx!{ div{ "hello {cx.name}" } }
/// } /// }
/// ///
/// let dom = VirtualDom::new(Example); /// let dom = VirtualDom::new(Example);
@@ -321,7 +321,7 @@ impl VirtualDom {
let root = dom.new_scope(Box::new(root), "app"); 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 // 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))); .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 // 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 /// This is useful for what is essentially dependency injection when building the app
pub fn with_root_context<T: Clone + 'static>(self, context: T) -> Self { pub fn with_root_context<T: Clone + 'static>(self, context: T) -> Self {
self.base_scope().context().provide_context(context); self.base_scope().state().provide_context(context);
self 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 /// 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<dyn Any>) { pub fn insert_any_root_context(&mut self, context: Box<dyn Any>) {
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 /// Manually mark a scope as requiring a re-render
/// ///
/// Whenever the Runtime "works", it will re-render this scope /// Whenever the Runtime "works", it will re-render this scope
pub fn mark_dirty(&mut self, id: ScopeId) { 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(); let height = context.height();
tracing::trace!("Marking scope {:?} ({}) as dirty", id, context.name); tracing::trace!("Marking scope {:?} ({}) as dirty", id, context.name);
self.dirty_scopes.insert(DirtyScope { height, id }); self.dirty_scopes.insert(DirtyScope { height, id });
@@ -417,52 +417,50 @@ impl VirtualDom {
/// ///
/// ```rust, ignore /// ```rust, ignore
/// let dom = VirtualDom::new(app); /// let dom = VirtualDom::new(app);
/// let sender = dom.get_scheduler_channel();
/// ``` /// ```
pub async fn wait_for_work(&mut self) { 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 { loop {
// If a bunch of messages are ready in a sequence, try to pop them off synchronously // Process all events - Scopes are marked dirty, etc
if let Some(msg) = some_msg.take() { self.process_events();
match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id), // 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
SchedulerMsg::TaskNotified(task) => self.queue_task_wakeup(task), 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 // Make sure we set the runtime since we're running user code
match self.rx.try_next() { let _runtime = RuntimeGuard::new(self.runtime.clone());
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();
if !has_dirty_scopes { match self.rx.next().await.expect("channel should never close") {
// If we have no dirty scopes, then we should poll any tasks that have been notified SchedulerMsg::Immediate(id) => self.mark_dirty(id),
while let Some(task) = self.runtime.take_queued_task() { SchedulerMsg::TaskNotified(id) => self.runtime.handle_task_wakeup(id),
self.handle_task_wakeup(task); };
} }
} }
// If we have any dirty scopes, or finished fiber trees then we should exit fn clear_flush_table(&mut self) {
if has_dirty_scopes || !self.suspended_scopes.is_empty() { // Make sure we set the runtime since we're running user code
return; 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 /// Process all events in the queue until there are no more left
pub fn process_events(&mut self) { 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() { while let Ok(Some(msg)) = self.rx.try_next() {
match msg { match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id), 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 if sync.template.get().name.rsplit_once(':').unwrap().0
== template.name.rsplit_once(':').unwrap().0 == template.name.rsplit_once(':').unwrap().0
{ {
let context = scope.context(); let context = scope.state();
let height = context.height; let height = context.height;
self.dirty_scopes.insert(DirtyScope { self.dirty_scopes.insert(DirtyScope {
height, height,
@@ -542,18 +540,25 @@ impl VirtualDom {
/// suspended subtrees. /// suspended subtrees.
pub fn render_immediate(&mut self, to: &mut impl WriteMutations) { pub fn render_immediate(&mut self, to: &mut impl WriteMutations) {
self.flush_templates(to); 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 // Process any events that might be pending in the queue
let fut = self.render_with_deadline(std::future::ready(()), to); self.process_events();
pin_mut!(fut);
// The root component is not allowed to be async // Next, diff any dirty scopes
match fut.poll(&mut cx) { // We choose not to poll the deadline since we complete pretty quickly anyways
std::task::Poll::Ready(mutations) => mutations, while let Some(dirty) = self.dirty_scopes.pop_first() {
std::task::Poll::Pending => panic!("render_immediate should never return pending"), // 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 /// 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 /// 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) { pub async fn wait_for_suspense(&mut self) {
loop { loop {
if self.suspended_scopes.is_empty() { if self.suspended_scopes.is_empty() {
return; return;
} }
// not sure if we should be doing this?
self.runtime.flush_table.borrow_mut().clear();
self.wait_for_work().await; self.wait_for_work().await;
self.render_immediate(&mut NoOpMutations); 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<Output = ()>,
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<Output = ()>,
) -> Mutations {
let mut mutations = Mutations::default();
self.render_with_deadline(deadline, &mut mutations).await;
mutations
}
/// Get the current runtime /// Get the current runtime
pub fn runtime(&self) -> Rc<Runtime> { pub fn runtime(&self) -> Rc<Runtime> {
self.runtime.clone() 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 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) { fn drop(&mut self) {
// Drop all scopes in order of height // Drop all scopes in order of height
let mut scopes = self.scopes.drain().collect::<Vec<_>>(); let mut scopes = self.scopes.drain().collect::<Vec<_>>();
scopes.sort_by_key(|scope| scope.context().height); scopes.sort_by_key(|scope| scope.state().height);
for scope in scopes.into_iter().rev() { for scope in scopes.into_iter().rev() {
drop(scope); drop(scope);
} }
+102 -12
View File
@@ -1,11 +1,22 @@
//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely //! 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] #[tokio::test]
async fn it_works() { async fn it_works() {
use dioxus::prelude::*;
use std::{sync::atomic::AtomicUsize, time::Duration};
static POLL_COUNT: AtomicUsize = AtomicUsize::new(0); static POLL_COUNT: AtomicUsize = AtomicUsize::new(0);
fn app() -> Element { fn app() -> Element {
@@ -28,14 +39,7 @@ async fn it_works() {
rsx!({ () }) rsx!({ () })
} }
let mut dom = VirtualDom::new(app); run_vdom(app).await;
dom.rebuild(&mut dioxus_core::NoOpMutations);
tokio::select! {
_ = dom.wait_for_work() => {}
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
};
// By the time the tasks are finished, we should've accumulated ticks from two tasks // 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 // 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 135
); );
} }
/// Prove that yield_now doesn't cause a deadlock
#[tokio::test]
async fn yield_now_works() {
thread_local! {
static SEQUENCE: std::cell::RefCell<Vec<usize>> = 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<Vec<usize>> = 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));
}
+3 -3
View File
@@ -65,7 +65,7 @@ struct BoxProps {
alpha: f32, alpha: f32,
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn Box(cx: Scope<BoxProps>) -> Element { fn Box(cx: ScopeState<BoxProps>) -> Element {
let count = use_signal(|| 0); let count = use_signal(|| 0);
let x = cx.props.x * 2; let x = cx.props.x * 2;
@@ -94,7 +94,7 @@ struct GridProps {
update_count: usize, update_count: usize,
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn Grid(cx: Scope<GridProps>) -> Element { fn Grid(cx: ScopeState<GridProps>) -> Element {
let size = cx.props.size; let size = cx.props.size;
let count = use_signal(|| 0); let count = use_signal(|| 0);
let counts = use_signal(|| vec![0; size * size]); let counts = use_signal(|| vec![0; size * size]);
@@ -151,7 +151,7 @@ fn Grid(cx: Scope<GridProps>) -> Element {
} }
} }
fn app(cx: Scope<GridProps>) -> Element { fn app(cx: ScopeState<GridProps>) -> Element {
rsx! { rsx! {
div{ div{
width: "100%", width: "100%",
+9 -6
View File
@@ -6,7 +6,7 @@ use crate::{
LiveViewError, LiveViewError,
}; };
use dioxus_core::prelude::*; use dioxus_core::prelude::*;
use dioxus_html::{EventData, HtmlEvent, PlatformEventData}; use dioxus_html::{select, EventData, HtmlEvent, PlatformEventData};
use dioxus_interpreter_js::MutationState; use dioxus_interpreter_js::MutationState;
use futures_util::{pin_mut, SinkExt, StreamExt}; use futures_util::{pin_mut, SinkExt, StreamExt};
use serde::Serialize; use serde::Serialize;
@@ -227,11 +227,14 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
} }
} }
vdom.render_with_deadline( // wait for suspense to resolve in a 10ms window
tokio::time::sleep(Duration::from_millis(10)), tokio::select! {
&mut mutations, _ = tokio::time::sleep(Duration::from_millis(10)) => {}
) _ = vdom.wait_for_suspense() => {}
.await; }
// render the vdom
vdom.render_immediate(&mut mutations);
if let Some(edits) = take_edits(&mut mutations) { if let Some(edits) = take_edits(&mut mutations) {
ws.send(edits).await?; ws.send(edits).await?;
+1 -1
View File
@@ -151,7 +151,7 @@ where
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
/// A component that renders the current route. /// A component that renders the current route.
pub fn Router<R: Routable + Clone>(cx: Scope<RouterProps<R>>) -> Element pub fn Router<R: Routable + Clone>(cx: ScopeState<RouterProps<R>>) -> Element
where where
<R as FromStr>::Err: std::fmt::Display, <R as FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned, R: serde::Serialize + serde::de::DeserializeOwned,