|
@@ -2,14 +2,14 @@
|
|
//!
|
|
//!
|
|
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
|
|
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
|
|
|
|
|
|
-use crate::innerlude::ScopeOrder;
|
|
|
|
|
|
+use crate::innerlude::{DirtyTasks, ScopeOrder};
|
|
use crate::Task;
|
|
use crate::Task;
|
|
use crate::{
|
|
use crate::{
|
|
any_props::AnyProps,
|
|
any_props::AnyProps,
|
|
arena::ElementId,
|
|
arena::ElementId,
|
|
innerlude::{
|
|
innerlude::{
|
|
- DirtyScopes, ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeState,
|
|
|
|
- VNodeMount, VProps, WriteMutations,
|
|
|
|
|
|
+ ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeState, VNodeMount, VProps,
|
|
|
|
+ WriteMutations,
|
|
},
|
|
},
|
|
nodes::RenderReturn,
|
|
nodes::RenderReturn,
|
|
nodes::{Template, TemplateId},
|
|
nodes::{Template, TemplateId},
|
|
@@ -18,8 +18,9 @@ use crate::{
|
|
AttributeValue, ComponentFunction, Element, Event, Mutations,
|
|
AttributeValue, ComponentFunction, Element, Event, Mutations,
|
|
};
|
|
};
|
|
use futures_util::StreamExt;
|
|
use futures_util::StreamExt;
|
|
-use rustc_hash::{FxHashMap, FxHashSet};
|
|
|
|
|
|
+use rustc_hash::FxHashMap;
|
|
use slab::Slab;
|
|
use slab::Slab;
|
|
|
|
+use std::collections::BTreeSet;
|
|
use std::{any::Any, rc::Rc};
|
|
use std::{any::Any, rc::Rc};
|
|
use tracing::instrument;
|
|
use tracing::instrument;
|
|
|
|
|
|
@@ -185,7 +186,8 @@ use tracing::instrument;
|
|
pub struct VirtualDom {
|
|
pub struct VirtualDom {
|
|
pub(crate) scopes: Slab<ScopeState>,
|
|
pub(crate) scopes: Slab<ScopeState>,
|
|
|
|
|
|
- pub(crate) dirty_scopes: DirtyScopes,
|
|
|
|
|
|
+ pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
|
|
|
|
+ pub(crate) dirty_tasks: BTreeSet<DirtyTasks>,
|
|
|
|
|
|
// Maps a template path to a map of byte indexes to templates
|
|
// Maps a template path to a map of byte indexes to templates
|
|
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template>>,
|
|
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template>>,
|
|
@@ -201,9 +203,6 @@ pub struct VirtualDom {
|
|
|
|
|
|
pub(crate) runtime: Rc<Runtime>,
|
|
pub(crate) runtime: Rc<Runtime>,
|
|
|
|
|
|
- // Currently suspended scopes
|
|
|
|
- pub(crate) suspended_scopes: FxHashSet<ScopeId>,
|
|
|
|
-
|
|
|
|
rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
|
|
rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
|
|
}
|
|
}
|
|
|
|
|
|
@@ -315,11 +314,11 @@ impl VirtualDom {
|
|
runtime: Runtime::new(tx),
|
|
runtime: Runtime::new(tx),
|
|
scopes: Default::default(),
|
|
scopes: Default::default(),
|
|
dirty_scopes: Default::default(),
|
|
dirty_scopes: Default::default(),
|
|
|
|
+ dirty_tasks: Default::default(),
|
|
templates: Default::default(),
|
|
templates: Default::default(),
|
|
queued_templates: Default::default(),
|
|
queued_templates: Default::default(),
|
|
elements: Default::default(),
|
|
elements: Default::default(),
|
|
mounts: Default::default(),
|
|
mounts: Default::default(),
|
|
- suspended_scopes: Default::default(),
|
|
|
|
};
|
|
};
|
|
|
|
|
|
let root = dom.new_scope(Box::new(root), "app");
|
|
let root = dom.new_scope(Box::new(root), "app");
|
|
@@ -380,7 +379,8 @@ impl VirtualDom {
|
|
|
|
|
|
tracing::event!(tracing::Level::TRACE, "Marking scope {:?} as dirty", id);
|
|
tracing::event!(tracing::Level::TRACE, "Marking scope {:?} as dirty", id);
|
|
let order = ScopeOrder::new(scope.height(), id);
|
|
let order = ScopeOrder::new(scope.height(), id);
|
|
- self.dirty_scopes.queue_scope(order);
|
|
|
|
|
|
+ drop(scope);
|
|
|
|
+ self.queue_scope(order);
|
|
}
|
|
}
|
|
|
|
|
|
/// Mark a task as dirty
|
|
/// Mark a task as dirty
|
|
@@ -400,7 +400,8 @@ impl VirtualDom {
|
|
);
|
|
);
|
|
|
|
|
|
let order = ScopeOrder::new(scope.height(), scope.id);
|
|
let order = ScopeOrder::new(scope.height(), scope.id);
|
|
- self.dirty_scopes.queue_task(task, order);
|
|
|
|
|
|
+ drop(scope);
|
|
|
|
+ self.queue_task(task, order);
|
|
}
|
|
}
|
|
|
|
|
|
/// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an element with a listener, not a static node or a text node.**
|
|
/// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an element with a listener, not a static node or a text node.**
|
|
@@ -448,20 +449,13 @@ impl VirtualDom {
|
|
/// ```
|
|
/// ```
|
|
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_work")]
|
|
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_work")]
|
|
pub async fn wait_for_work(&mut self) {
|
|
pub async fn wait_for_work(&mut self) {
|
|
- // And then poll the futures
|
|
|
|
- self.poll_tasks().await;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /// Poll the scheduler for any work
|
|
|
|
- #[instrument(skip(self), level = "trace", name = "VirtualDom::poll_tasks")]
|
|
|
|
- async fn poll_tasks(&mut self) {
|
|
|
|
loop {
|
|
loop {
|
|
// Process all events - Scopes are marked dirty, etc
|
|
// Process all events - Scopes are marked dirty, etc
|
|
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
|
|
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
|
|
self.process_events();
|
|
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
|
|
// 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.has_dirty_scopes() {
|
|
|
|
|
|
+ if self.has_dirty_scopes() {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -469,17 +463,22 @@ impl VirtualDom {
|
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
|
|
|
|
|
// There isn't any more work we can do synchronously. Wait for any new work to be ready
|
|
// There isn't any more work we can do synchronously. Wait for any new work to be ready
|
|
- match self.rx.next().await.expect("channel should never close") {
|
|
|
|
- SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
|
|
|
- SchedulerMsg::TaskNotified(id) => {
|
|
|
|
- // Instead of running the task immediately, we insert it into the runtime's task queue.
|
|
|
|
- // The task may be marked dirty at the same time as the scope that owns the task is dropped.
|
|
|
|
- self.mark_task_dirty(id);
|
|
|
|
- }
|
|
|
|
- };
|
|
|
|
|
|
+ self.wait_for_event().await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /// Wait for the next event to trigger and add it to the queue
|
|
|
|
+ async fn wait_for_event(&mut self) {
|
|
|
|
+ match self.rx.next().await.expect("channel should never close") {
|
|
|
|
+ SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
|
|
|
+ SchedulerMsg::TaskNotified(id) => {
|
|
|
|
+ // Instead of running the task immediately, we insert it into the runtime's task queue.
|
|
|
|
+ // The task may be marked dirty at the same time as the scope that owns the task is dropped.
|
|
|
|
+ self.mark_task_dirty(id);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
/// Queue any pending events
|
|
/// Queue any pending events
|
|
fn queue_events(&mut self) {
|
|
fn queue_events(&mut self) {
|
|
// Prevent a task from deadlocking the runtime by repeatedly queueing itself
|
|
// Prevent a task from deadlocking the runtime by repeatedly queueing itself
|
|
@@ -494,29 +493,31 @@ impl VirtualDom {
|
|
/// Process all events in the queue until there are no more left
|
|
/// Process all events in the queue until there are no more left
|
|
#[instrument(skip(self), level = "trace", name = "VirtualDom::process_events")]
|
|
#[instrument(skip(self), level = "trace", name = "VirtualDom::process_events")]
|
|
pub fn process_events(&mut self) {
|
|
pub fn process_events(&mut self) {
|
|
- let _runtime = RuntimeGuard::new(self.runtime.clone());
|
|
|
|
self.queue_events();
|
|
self.queue_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
|
|
// 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.has_dirty_scopes() {
|
|
|
|
|
|
+ if self.has_dirty_scopes() {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ self.poll_tasks()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// Poll any queued tasks
|
|
|
|
+ #[instrument(skip(self), level = "trace", name = "VirtualDom::poll_tasks")]
|
|
|
|
+ fn poll_tasks(&mut self) {
|
|
|
|
+ // Make sure we set the runtime since we're running user code
|
|
|
|
+ let _runtime = RuntimeGuard::new(self.runtime.clone());
|
|
// Next, run any queued tasks
|
|
// Next, run any queued tasks
|
|
// We choose not to poll the deadline since we complete pretty quickly anyways
|
|
// We choose not to poll the deadline since we complete pretty quickly anyways
|
|
- while let Some(task) = self.dirty_scopes.pop_task() {
|
|
|
|
- // If the scope doesn't exist for whatever reason, then we should skip it
|
|
|
|
- if !self.scopes.contains(task.order.id.0) {
|
|
|
|
- continue;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
|
|
+ while let Some(task) = self.pop_task() {
|
|
// Then poll any tasks that might be pending
|
|
// Then poll any tasks that might be pending
|
|
let tasks = task.tasks_queued.into_inner();
|
|
let tasks = task.tasks_queued.into_inner();
|
|
for task in tasks {
|
|
for task in tasks {
|
|
let _ = self.runtime.handle_task_wakeup(task);
|
|
let _ = self.runtime.handle_task_wakeup(task);
|
|
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
|
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
|
self.queue_events();
|
|
self.queue_events();
|
|
- if self.dirty_scopes.has_dirty_scopes() {
|
|
|
|
|
|
+ if self.has_dirty_scopes() {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -608,16 +609,10 @@ impl VirtualDom {
|
|
|
|
|
|
// Next, diff any dirty scopes
|
|
// Next, diff any dirty scopes
|
|
// We choose not to poll the deadline since we complete pretty quickly anyways
|
|
// We choose not to poll the deadline since we complete pretty quickly anyways
|
|
- while let Some(work) = self.dirty_scopes.pop_work() {
|
|
|
|
- // If the scope doesn't exist for whatever reason, then we should skip it
|
|
|
|
- if !self.scopes.contains(work.scope.id.0) {
|
|
|
|
- continue;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
|
|
+ while let Some(work) = self.pop_work() {
|
|
{
|
|
{
|
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
|
// Then, poll any tasks that might be pending in the scope
|
|
// Then, poll any tasks that might be pending in the scope
|
|
- // This will run effects, so this **must** be done after the scope is diffed
|
|
|
|
for task in work.tasks {
|
|
for task in work.tasks {
|
|
let _ = self.runtime.handle_task_wakeup(task);
|
|
let _ = self.runtime.handle_task_wakeup(task);
|
|
}
|
|
}
|
|
@@ -649,15 +644,62 @@ impl VirtualDom {
|
|
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
|
|
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
|
|
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.runtime.suspended_tasks.borrow().is_empty() {
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Wait for a work to be ready (IE new suspense leaves to pop up)
|
|
// Wait for a work to be ready (IE new suspense leaves to pop up)
|
|
- self.poll_tasks().await;
|
|
|
|
|
|
+ 'wait_for_work: loop {
|
|
|
|
+ // Process all events - Scopes are marked dirty, etc
|
|
|
|
+ // Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
|
|
|
|
+ self.queue_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.has_dirty_scopes() {
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ {
|
|
|
|
+ // Make sure we set the runtime since we're running user code
|
|
|
|
+ let _runtime = RuntimeGuard::new(self.runtime.clone());
|
|
|
|
+ // Next, run any queued tasks
|
|
|
|
+ // We choose not to poll the deadline since we complete pretty quickly anyways
|
|
|
|
+ while let Some(task) = self.pop_task() {
|
|
|
|
+ // Then poll any tasks that might be pending
|
|
|
|
+ let tasks = task.tasks_queued.into_inner();
|
|
|
|
+ for task in tasks {
|
|
|
|
+ if self.runtime.suspended_tasks.borrow().contains(&task) {
|
|
|
|
+ let _ = self.runtime.handle_task_wakeup(task);
|
|
|
|
+ // Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
|
|
|
+ self.queue_events();
|
|
|
|
+ if self.has_dirty_scopes() {
|
|
|
|
+ break 'wait_for_work;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ self.wait_for_event().await;
|
|
|
|
+ }
|
|
|
|
|
|
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
|
|
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
|
|
- self.render_immediate(&mut NoOpMutations);
|
|
|
|
|
|
+ let _runtime = RuntimeGuard::new(self.runtime.clone());
|
|
|
|
+ while let Some(work) = self.pop_work() {
|
|
|
|
+ // Then, poll any tasks that might be pending in the scope
|
|
|
|
+ for task in work.tasks {
|
|
|
|
+ // During suspense, we only want to run tasks that are suspended
|
|
|
|
+ if self.runtime.suspended_tasks.borrow().contains(&task) {
|
|
|
|
+ let _ = self.runtime.handle_task_wakeup(task);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ // If the scope is dirty, run the scope and get the mutations
|
|
|
|
+ if work.rerun_scope {
|
|
|
|
+ let new_nodes = self.run_scope(work.scope.id);
|
|
|
|
+
|
|
|
|
+ self.diff_scope(&mut NoOpMutations, work.scope.id, new_nodes);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|