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"
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 }
+2 -10
View File
@@ -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)));
# }
```
+1 -1
View File
@@ -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
};
+3 -3
View File
@@ -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
+48 -11
View File
@@ -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<ScopeId> {
@@ -30,7 +28,7 @@ pub fn consume_context<T: 'static + Clone>() -> T {
/// Consume context from the current scope
pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> {
Runtime::with(|rt| {
rt.get_context(scope_id)
rt.get_state(scope_id)
.and_then(|cx| cx.consume_context::<T>())
})
.flatten()
@@ -53,9 +51,7 @@ pub fn provide_root_context<T: 'static + Clone>(value: T) -> Option<T> {
/// Suspends the current component
pub fn suspend() -> Option<Element> {
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<State: Clone + 'static>(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<dyn Fn(ScopeId) + Send + Sync> {
/// (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<dyn Fn(ScopeId) + Send + Sync> {
/// }
/// });
///
/// use_on_destroy({
/// use_drop({
/// to_owned![original_scroll_position];
/// /// restore scroll to the top of the page
/// move || {
+9 -8
View File
@@ -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,
};
}
+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].
///
/// 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
+36 -41
View File
@@ -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<Vec<Option<ScopeContext>>>,
pub(crate) scope_states: RefCell<Vec<Option<Scope>>>,
// We use this to track the current scope
pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
@@ -29,10 +30,10 @@ pub struct Runtime {
/// Tasks created with cx.spawn
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>,
// Tasks waiting to be manually resumed when we call wait_for_work
pub(crate) flush_table: RefCell<FxHashSet<Task>>,
}
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<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] {
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<'_, ScopeContext>> {
Ref::filter_map(self.scope_contexts.borrow(), |contexts| {
pub(crate) fn get_state(&self, id: ScopeId) -> Option<Ref<'_, Scope>> {
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, R>(f: F) -> Option<R>
where
F: FnOnce(&Runtime) -> R,
{
RUNTIMES.with(|stack| {
let stack = stack.borrow();
stack.last().map(|r| f(r))
})
pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> Option<R> {
RUNTIMES.with(|stack| stack.borrow().last().map(|r| f(r)))
}
/// Runs a function with the current scope
pub(crate) fn with_current_scope<F, R>(f: F) -> Option<R>
where
F: FnOnce(&ScopeContext) -> R,
{
pub(crate) fn with_current_scope<R>(f: impl FnOnce(&Scope) -> R) -> Option<R> {
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<F, R>(scope: ScopeId, f: F) -> Option<R>
where
F: FnOnce(&ScopeContext) -> R,
{
Self::with(|rt| rt.get_context(scope).map(|sc| f(&sc))).flatten()
pub(crate) fn with_scope<R>(scope: ScopeId, f: impl FnOnce(&Scope) -> R) -> Option<R> {
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 {} }
/// }
+18 -9
View File
@@ -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);
+13 -8
View File
@@ -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<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.
pub(crate) hooks: RefCell<Vec<Box<dyn Any>>>,
pub(crate) hook_index: Cell<usize>,
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>,
pub(crate) before_render: RefCell<Vec<Box<dyn FnMut()>>>,
}
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::<T>(),
@@ -212,7 +212,7 @@ impl ScopeContext {
pub fn provide_root_context<T: 'static + Clone>(&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<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
///
/// 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<T: 'static + Clone>(self, scope_id: ScopeId) -> Option<T> {
Runtime::with(|rt| {
rt.get_context(scope_id)
rt.get_state(scope_id)
.and_then(|cx| cx.consume_context::<T>())
})
.flatten()
+4 -4
View File
@@ -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()
}
}
+53 -34
View File
@@ -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<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) {
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<Task> {
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<Rc<LocalTask>> {
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<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
self.tasks.borrow_mut().try_remove(id.0)
}
}
@@ -152,6 +171,7 @@ pub(crate) struct LocalTask {
parent: Option<Task>,
task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
waker: Waker,
active: Cell<bool>,
}
/// 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<Self>) {
// 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));
}
+72 -129
View File
@@ -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<T: Clone + 'static>(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<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
///
/// 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<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
pub fn runtime(&self) -> Rc<Runtime> {
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::<Vec<_>>();
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);
}
+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
#[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<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,
}
#[allow(non_snake_case)]
fn Box(cx: Scope<BoxProps>) -> Element {
fn Box(cx: ScopeState<BoxProps>) -> 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<GridProps>) -> Element {
fn Grid(cx: ScopeState<GridProps>) -> 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<GridProps>) -> Element {
}
}
fn app(cx: Scope<GridProps>) -> Element {
fn app(cx: ScopeState<GridProps>) -> Element {
rsx! {
div{
width: "100%",
+9 -6
View File
@@ -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?;
+1 -1
View File
@@ -151,7 +151,7 @@ where
#[cfg(feature = "serde")]
/// 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
<R as FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,