wip: consolidation, simplification

This commit is contained in:
Jonathan Kelley committed 2021-11-05 16:28:08 -04:00
1 parent b56ea6c9a9
commit 70b983d090
17 files changed
+1228 -836

No files matched your search

-2
View File
@@ -28,7 +28,6 @@ impl ActiveFrame {
head_node: VNode::Fragment(VFragment {
key: None,
children: &[],
is_static: false,
}),
bump: b1,
@@ -39,7 +38,6 @@ impl ActiveFrame {
head_node: VNode::Fragment(VFragment {
key: None,
children: &[],
is_static: false,
}),
bump: b2,
+1 -1
View File
@@ -62,7 +62,7 @@ impl<'a> Iterator for RealChildIterator<'a> {
VNode::Component(sc) => {
let scope = self
.scopes
.get_scope(sc.associated_scope.get().unwrap())
.get_scope(&sc.associated_scope.get().unwrap())
.unwrap();
// Simply swap the current node on the stack with the root of the component
+3 -6
View File
@@ -5,10 +5,7 @@
//! if the type supports PartialEq. The Properties trait is used by the rsx! and html! macros to generate the type-safe builder
//! that ensures compile-time required and optional fields on cx.
use crate::{
innerlude::{Context, Element, VAnchor, VFragment, VNode},
LazyNodes, ScopeChildren,
};
use crate::innerlude::{Context, Element, LazyNodes, ScopeChildren};
/// A component is a wrapper around a Context and some Props that share a lifetime
///
///
@@ -51,7 +48,7 @@ pub struct FragmentBuilder<'a, const BUILT: bool> {
children: Option<ScopeChildren<'a>>,
}
impl<'a> FragmentBuilder<'a, false> {
pub fn children(mut self, children: ScopeChildren<'a>) -> FragmentBuilder<'a, true> {
pub fn children(self, children: ScopeChildren<'a>) -> FragmentBuilder<'a, true> {
FragmentBuilder {
children: Some(children),
}
@@ -75,7 +72,7 @@ impl<'a> Properties for FragmentProps<'a> {
FragmentBuilder { children: None }
}
unsafe fn memoize(&self, other: &Self) -> bool {
unsafe fn memoize(&self, _other: &Self) -> bool {
false
}
}
-17
View File
@@ -1,17 +0,0 @@
//! Coroutines are just a "futures unordered" buffer for tasks that can be submitted through the use_coroutine hook.
//!
//! The idea here is to move *coroutine* support as a layer on top of *tasks*
use futures_util::{stream::FuturesUnordered, Future};
pub struct CoroutineScheduler {
futures: FuturesUnordered<Box<dyn Future<Output = ()>>>,
}
impl CoroutineScheduler {
pub fn new() -> Self {
CoroutineScheduler {
futures: FuturesUnordered::new(),
}
}
}
+61 -32
View File
@@ -169,7 +169,7 @@ impl<'bump> DiffMachine<'bump> {
}
pub fn diff_scope(&mut self, id: ScopeId) {
if let Some(component) = self.vdom.get_scope_mut(id) {
if let Some(component) = self.vdom.get_scope_mut(&id) {
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
self.stack.push(DiffInstruction::Diff { new, old });
self.work(|| false);
@@ -189,7 +189,10 @@ impl<'bump> DiffMachine<'bump> {
DiffInstruction::Diff { old, new } => self.diff_node(old, new),
DiffInstruction::Create { node } => self.create_node(node),
DiffInstruction::Mount { and } => self.mount(and),
DiffInstruction::PrepareMove { node } => self.prepare_move_node(node),
DiffInstruction::PrepareMove { node } => {
let num_on_stack = self.push_all_nodes(node);
self.stack.add_child_count(num_on_stack);
}
DiffInstruction::PopScope => self.stack.pop_off_scope(),
};
@@ -202,10 +205,29 @@ impl<'bump> DiffMachine<'bump> {
true
}
fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) {
for el in RealChildIterator::new(node, self.vdom) {
self.mutations.push_root(el.mounted_id());
self.stack.add_child_count(1);
// recursively push all the nodes of a tree onto the stack and return how many are there
fn push_all_nodes(&mut self, node: &'bump VNode<'bump>) -> usize {
match node {
VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => {
self.mutations.push_root(node.mounted_id());
1
}
VNode::Fragment(_) | VNode::Component(_) => node
.children()
.iter()
.map(|child| self.push_all_nodes(child))
.sum(),
VNode::Element(el) => {
let mut num_on_stack = 0;
for child in el.children.iter() {
num_on_stack += self.push_all_nodes(child);
}
self.mutations.push_root(el.dom_id.get().unwrap());
num_on_stack + 1
}
}
}
@@ -307,7 +329,7 @@ impl<'bump> DiffMachine<'bump> {
self.stack.add_child_count(1);
if let Some(cur_scope_id) = self.stack.current_scope() {
let scope = self.vdom.get_scope(cur_scope_id).unwrap();
let scope = self.vdom.get_scope_mut(&cur_scope_id).unwrap();
listeners.iter().for_each(|listener| {
self.attach_listener_to_scope(listener, scope);
@@ -332,18 +354,18 @@ impl<'bump> DiffMachine<'bump> {
}
fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) {
let caller = vcomponent.caller;
// let caller = vcomponent.caller;
let parent_idx = self.stack.current_scope().unwrap();
let shared = self.vdom.channel.clone();
// Insert a new scope into our component list
let parent_scope = self.vdom.get_scope(parent_idx).unwrap();
let parent_scope = self.vdom.get_scope(&parent_idx).unwrap();
let new_idx = self.vdom.insert_scope_with_key(|new_idx| {
ScopeInner::new(
caller,
vcomponent,
new_idx,
Some(parent_idx),
parent_scope.height + 1,
@@ -356,17 +378,18 @@ impl<'bump> DiffMachine<'bump> {
vcomponent.associated_scope.set(Some(new_idx));
if !vcomponent.can_memoize {
let cur_scope = self.vdom.get_scope_mut(parent_idx).unwrap();
let cur_scope = self.vdom.get_scope_mut(&parent_idx).unwrap();
let extended = vcomponent as *const VComponent;
let extended: *const VComponent<'static> = unsafe { std::mem::transmute(extended) };
cur_scope.borrowed_props.borrow_mut().push(extended);
cur_scope.items.get_mut().borrowed_props.push(extended);
}
// TODO:
// add noderefs to current noderef list Noderefs
// add effects to current effect list Effects
let new_component = self.vdom.get_scope_mut(new_idx).unwrap();
let new_component = self.vdom.get_scope_mut(&new_idx).unwrap();
log::debug!(
"initializing component {:?} with height {:?}",
@@ -494,7 +517,7 @@ impl<'bump> DiffMachine<'bump> {
//
// TODO: take a more efficient path than this
if let Some(cur_scope_id) = self.stack.current_scope() {
let scope = self.vdom.get_scope(cur_scope_id).unwrap();
let scope = self.vdom.get_scope_mut(&cur_scope_id).unwrap();
if old.listeners.len() == new.listeners.len() {
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
@@ -549,19 +572,21 @@ impl<'bump> DiffMachine<'bump> {
new.associated_scope.set(Some(scope_addr));
// make sure the component's caller function is up to date
let scope = self.vdom.get_scope_mut(scope_addr).unwrap();
scope.update_scope_dependencies(new.caller);
let scope = self.vdom.get_scope_mut(&scope_addr).unwrap();
scope.update_vcomp(new);
// React doesn't automatically memoize, but we do.
let props_are_the_same = old.comparator.unwrap();
let props_are_the_same = todo!("reworking component memoization");
// let props_are_the_same = todo!("reworking component memoization");
// let props_are_the_same = old.comparator.unwrap();
if self.cfg.force_diff || !props_are_the_same(new) {
let succeeded = scope.run_scope(self.vdom);
// if self.cfg.force_diff || !props_are_the_same(new) {
// let succeeded = scope.run_scope(self.vdom);
if succeeded {
self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
}
}
// if succeeded {
// self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
// }
// }
self.stack.scope_stack.pop();
} else {
@@ -1038,7 +1063,7 @@ impl<'bump> DiffMachine<'bump> {
}
VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap();
let scope = self.vdom.get_scope(scope_id).unwrap();
let scope = self.vdom.get_scope(&scope_id).unwrap();
search_node = Some(scope.root_node());
}
}
@@ -1056,7 +1081,7 @@ impl<'bump> DiffMachine<'bump> {
}
VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap();
let scope = self.vdom.get_scope(scope_id).unwrap();
let scope = self.vdom.get_scope(&scope_id).unwrap();
search_node = Some(scope.root_node());
}
VNode::Text(t) => break t.dom_id.get(),
@@ -1131,12 +1156,12 @@ impl<'bump> DiffMachine<'bump> {
VNode::Component(c) => {
let scope_id = c.associated_scope.get().unwrap();
let scope = self.vdom.get_scope_mut(scope_id).unwrap();
let scope = self.vdom.get_scope_mut(&scope_id).unwrap();
let root = scope.root_node();
self.remove_nodes(Some(root), gen_muts);
log::debug!("Destroying scope {:?}", scope_id);
let mut s = self.vdom.try_remove(scope_id).unwrap();
let mut s = self.vdom.try_remove(&scope_id).unwrap();
s.hooks.clear_hooks();
}
}
@@ -1161,23 +1186,27 @@ impl<'bump> DiffMachine<'bump> {
}
/// Adds a listener closure to a scope during diff.
fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &ScopeInner) {
let mut queue = scope.listeners.borrow_mut();
fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &mut ScopeInner) {
let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) };
queue.push(long_listener as *const _)
scope
.items
.get_mut()
.listeners
.push(long_listener as *const _)
}
fn attach_suspended_node_to_scope(&mut self, suspended: &'bump VSuspended) {
if let Some(scope) = self
.stack
.current_scope()
.and_then(|id| self.vdom.get_scope_mut(id))
.and_then(|id| self.vdom.get_scope_mut(&id))
{
// safety: this lifetime is managed by the logic on scope
let extended: &VSuspended<'static> = unsafe { std::mem::transmute(suspended) };
scope
.items
.get_mut()
.suspended_nodes
.borrow_mut()
.insert(suspended.task_id, extended as *const _);
}
}
-50
View File
@@ -1,50 +0,0 @@
use std::collections::HashMap;
use fxhash::FxHashMap;
use crate::FC;
/// Provides heuristics to the "SharedResources" object for improving allocation performance.
///
/// This heuristics engine records the memory footprint of bump arenas and hook lists for each component. These records are
/// then used later on to optimize the initial allocation for future components. This helps save large allocations later on
/// that would slow down the diffing and initialization process.
///
///
pub struct HeuristicsEngine {
heuristics: FxHashMap<FcSlot, Heuristic>,
}
pub type FcSlot = *const ();
pub struct Heuristic {
hooks: u32,
bump_size: u64,
}
impl HeuristicsEngine {
pub(crate) fn new() -> Self {
Self {
heuristics: FxHashMap::default(),
}
}
fn recommend<T>(&mut self, fc: FC<T>, heuristic: Heuristic) {
let g = fc as FcSlot;
let e = self.heuristics.entry(g);
}
fn get_recommendation<T>(&mut self, fc: FC<T>) -> &Heuristic {
let id = fc as FcSlot;
self.heuristics.entry(id).or_insert(Heuristic {
bump_size: 100,
hooks: 10,
})
}
}
#[test]
fn types_work() {
let engine = HeuristicsEngine::new();
}
+19 -14
View File
@@ -1,10 +1,9 @@
use std::{
any::Any,
cell::{Cell, RefCell, UnsafeCell},
cell::{Cell, RefCell},
};
type UnsafeInnerHookState = UnsafeCell<Box<dyn Any>>;
type HookCleanup = Box<dyn FnOnce(Box<dyn Any>)>;
use bumpalo::Bump;
/// An abstraction over internally stored data using a hook-based memory layout.
///
@@ -15,7 +14,8 @@ type HookCleanup = Box<dyn FnOnce(Box<dyn Any>)>;
/// Todo: this could use its very own bump arena, but that might be a tad overkill
#[derive(Default)]
pub(crate) struct HookList {
vals: RefCell<Vec<(UnsafeInnerHookState, HookCleanup)>>,
arena: Bump,
vals: RefCell<Vec<*mut dyn Any>>,
idx: Cell<usize>,
}
@@ -23,7 +23,7 @@ impl HookList {
pub(crate) fn next<T: 'static>(&self) -> Option<&mut T> {
self.vals.borrow().get(self.idx.get()).and_then(|inn| {
self.idx.set(self.idx.get() + 1);
let raw_box = unsafe { &mut *inn.0.get() };
let raw_box = unsafe { &mut **inn };
raw_box.downcast_mut::<T>()
})
}
@@ -38,10 +38,9 @@ impl HookList {
self.idx.set(0);
}
pub(crate) fn push_hook<T: 'static>(&self, new: T, cleanup: HookCleanup) {
self.vals
.borrow_mut()
.push((UnsafeCell::new(Box::new(new)), cleanup))
pub(crate) fn push_hook<T: 'static>(&self, new: T) {
let val = self.arena.alloc(new);
self.vals.borrow_mut().push(val)
}
pub(crate) fn len(&self) -> usize {
@@ -57,10 +56,16 @@ impl HookList {
}
pub fn clear_hooks(&mut self) {
log::debug!("clearing hooks...");
self.vals
.borrow_mut()
.drain(..)
.for_each(|(state, cleanup)| cleanup(state.into_inner()));
self.vals.borrow_mut().drain(..).for_each(|state| {
let as_mut = unsafe { &mut *state };
let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) };
drop(boxed);
});
}
/// Get the ammount of memory a hooklist uses
/// Used in heuristics
pub fn get_hook_arena_size(&self) -> usize {
self.arena.allocated_bytes()
}
}
+65 -65
View File
@@ -13,73 +13,73 @@ use crate::innerlude::*;
use futures_util::FutureExt;
use std::{any::Any, cell::RefCell, future::Future, ops::Deref, rc::Rc};
/// Awaits the given task, forcing the component to re-render when the value is ready.
///
/// Returns the handle to the task and the value (if it is ready, else None).
///
/// ```
/// static Example: FC<()> = |(cx, props)| {
/// let (task, value) = use_task(|| async {
/// timer::sleep(Duration::from_secs(1)).await;
/// "Hello World"
/// });
///
/// match contents {
/// Some(contents) => rsx!(cx, div { "{title}" }),
/// None => rsx!(cx, div { "Loading..." }),
/// }
/// };
/// ```
pub fn use_coroutine<'src, Out, Fut, Init>(
cx: Context<'src>,
task_initializer: Init,
) -> (&'src TaskHandle, &'src Option<Out>)
where
Out: 'static,
Fut: Future<Output = Out> + 'static,
Init: FnOnce() -> Fut + 'src,
{
struct TaskHook<T> {
handle: TaskHandle,
task_dump: Rc<RefCell<Option<T>>>,
value: Option<T>,
}
// /// Awaits the given task, forcing the component to re-render when the value is ready.
// ///
// /// Returns the handle to the task and the value (if it is ready, else None).
// ///
// /// ```
// /// static Example: FC<()> = |(cx, props)| {
// /// let (task, value) = use_task(|| async {
// /// timer::sleep(Duration::from_secs(1)).await;
// /// "Hello World"
// /// });
// ///
// /// match contents {
// /// Some(contents) => rsx!(cx, div { "{title}" }),
// /// None => rsx!(cx, div { "Loading..." }),
// /// }
// /// };
// /// ```
// pub fn use_coroutine<'src, Out, Fut, Init>(
// cx: Context<'src>,
// task_initializer: Init,
// ) -> (&'src TaskHandle, &'src Option<Out>)
// where
// Out: 'static,
// Fut: Future<Output = Out> + 'static,
// Init: FnOnce() -> Fut + 'src,
// {
// struct TaskHook<T> {
// handle: TaskHandle,
// task_dump: Rc<RefCell<Option<T>>>,
// value: Option<T>,
// }
todo!()
// todo!()
// // whenever the task is complete, save it into th
// cx.use_hook(
// move |_| {
// let task_fut = task_initializer();
// // // whenever the task is complete, save it into th
// // cx.use_hook(
// // move |_| {
// // let task_fut = task_initializer();
// let task_dump = Rc::new(RefCell::new(None));
// // let task_dump = Rc::new(RefCell::new(None));
// let slot = task_dump.clone();
// // let slot = task_dump.clone();
// let updater = cx.schedule_update_any();
// let originator = cx.scope.our_arena_idx;
// // let updater = cx.schedule_update_any();
// // let originator = cx.scope.our_arena_idx;
// let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move {
// *slot.as_ref().borrow_mut() = Some(output);
// updater(originator);
// originator
// })));
// // let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move {
// // *slot.as_ref().borrow_mut() = Some(output);
// // updater(originator);
// // originator
// // })));
// TaskHook {
// task_dump,
// value: None,
// handle,
// }
// },
// |hook| {
// if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() {
// hook.value = Some(val);
// }
// (&hook.handle, &hook.value)
// },
// |_| {},
// )
}
// // TaskHook {
// // task_dump,
// // value: None,
// // handle,
// // }
// // },
// // |hook| {
// // if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() {
// // hook.value = Some(val);
// // }
// // (&hook.handle, &hook.value)
// // },
// // |_| {},
// // )
// }
/// Asynchronously render new nodes once the given future has completed.
///
@@ -167,10 +167,10 @@ where
// )
}
pub(crate) struct SuspenseHook {
pub handle: TaskHandle,
pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
}
// pub(crate) struct SuspenseHook {
// pub handle: TaskHandle,
// pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
// }
#[derive(Clone, Copy)]
pub struct NodeRef<'src, T: 'static>(&'src RefCell<Option<T>>);
@@ -183,5 +183,5 @@ impl<'a, T> Deref for NodeRef<'a, T> {
}
pub fn use_node_ref<T, P>(cx: Context) -> NodeRef<T> {
cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f }, |_| {})
cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f })
}
+2 -13
View File
@@ -13,22 +13,16 @@ Navigating this crate:
Some utilities
*/
pub(crate) mod bumpframe;
pub(crate) mod childiter;
pub(crate) mod component;
pub(crate) mod coroutines;
pub(crate) mod diff;
pub(crate) mod diff_stack;
pub(crate) mod events;
pub(crate) mod heuristics;
pub(crate) mod hooklist;
pub(crate) mod hooks;
pub(crate) mod lazynodes;
pub(crate) mod mutations;
pub(crate) mod nodes;
pub(crate) mod resources;
pub(crate) mod scheduler;
pub(crate) mod scope;
pub(crate) mod tasks;
pub(crate) mod test_dom;
pub(crate) mod threadsafe;
pub(crate) mod util;
@@ -39,21 +33,16 @@ pub mod debug_dom;
pub(crate) mod innerlude {
pub(crate) use crate::bumpframe::*;
pub(crate) use crate::childiter::*;
pub use crate::component::*;
pub(crate) use crate::diff::*;
pub use crate::diff_stack::*;
pub use crate::events::*;
pub use crate::heuristics::*;
pub(crate) use crate::hooklist::*;
pub use crate::hooks::*;
pub use crate::lazynodes::*;
pub use crate::mutations::*;
pub use crate::nodes::*;
pub(crate) use crate::resources::*;
pub use crate::scheduler::*;
pub use crate::scope::*;
pub use crate::tasks::*;
pub use crate::test_dom::*;
pub use crate::threadsafe::*;
pub use crate::util::*;
@@ -65,8 +54,8 @@ pub(crate) mod innerlude {
pub use crate::innerlude::{
Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, LazyNodes, MountType,
Mutations, NodeFactory, Properties, SchedulerMsg, ScopeChildren, ScopeId, TaskHandle, TestDom,
ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC,
Mutations, NodeFactory, Properties, ScopeChildren, ScopeId, TestDom, ThreadsafeVirtualDom,
UserEvent, VNode, VirtualDom, FC,
};
pub mod prelude {
+110 -83
View File
@@ -154,6 +154,14 @@ impl<'src> VNode<'src> {
}
}
pub fn children(&self) -> &[VNode<'src>] {
match &self {
VNode::Fragment(f) => &f.children,
VNode::Component(c) => &c.children,
_ => &[],
}
}
// Create an "owned" version of the vnode.
pub fn decouple(&self) -> VNode<'src> {
match self {
@@ -165,7 +173,6 @@ impl<'src> VNode<'src> {
VNode::Fragment(f) => VNode::Fragment(VFragment {
children: f.children,
key: f.key,
is_static: f.is_static,
}),
}
}
@@ -209,8 +216,6 @@ pub struct VFragment<'src> {
pub key: Option<&'src str>,
pub children: &'src [VNode<'src>],
pub is_static: bool,
}
/// An element like a "div" with children, listeners, and attributes.
@@ -316,21 +321,21 @@ pub struct VComponent<'src> {
pub associated_scope: Cell<Option<ScopeId>>,
pub is_static: bool,
// Function pointer to the FC that was used to generate this component
pub user_fc: *const (),
pub(crate) caller: BumpBox<'src, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> + 'src>,
pub(crate) comparator: Option<BumpBox<'src, dyn Fn(&VComponent) -> bool + 'src>>,
pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
pub(crate) can_memoize: bool,
// Raw pointer into the bump arena for the props of the component
pub(crate) raw_props: *const (),
// during the "teardown" process we'll take the caller out so it can be dropped properly
pub(crate) caller: Option<VCompCaller<'src>>,
pub(crate) comparator: Option<BumpBox<'src, dyn Fn(&VComponent) -> bool + 'src>>,
}
pub enum VCompCaller<'src> {
Borrowed(BumpBox<'src, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> + 'src>),
Owned(Box<dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>>),
}
pub struct VSuspended<'a> {
@@ -473,7 +478,7 @@ impl<'a> NodeFactory<'a> {
}
}
pub fn component<P>(
pub fn component<P, P1>(
&self,
component: fn(Scope<'a, P>) -> Element<'a>,
props: P,
@@ -482,85 +487,109 @@ impl<'a> NodeFactory<'a> {
where
P: Properties + 'a,
{
/*
our strategy:
- unsafe hackery
- lol
- we don't want to hit the global allocator
- allocate into our bump arena
- if the props aren't static, then we convert them into a box which we pass off between renders
*/
let bump = self.bump();
let props = bump.alloc(props);
let raw_props = props as *mut P as *mut ();
// let p = BumpBox::new_in(x, a)
// the best place to allocate the props are the other component's arena
// the second best place is the global allocator
// // if the props are static
// let boxed = if P::IS_STATIC {
// todo!()
// } else {
// todo!()
// }
// let caller = Box::new(|f: &ScopeInner| -> Element {
// //
// component((f, &props))
// });
let user_fc = component as *const ();
let comparator: &mut dyn Fn(&VComponent) -> bool = bump.alloc_with(|| {
move |other: &VComponent| {
if user_fc == other.user_fc {
// Safety
// - We guarantee that FC<P> is the same by function pointer
// - Because FC<P> is the same, then P must be the same (even with generics)
// - Non-static P are autoderived to memoize as false
// - This comparator is only called on a corresponding set of bumpframes
let props_memoized = unsafe {
let real_other: &P = &*(other.raw_props as *const _ as *const P);
props.memoize(real_other)
};
// let comparator: &mut dyn Fn(&VComponent) -> bool = bump.alloc_with(|| {
// move |other: &VComponent| {
// if user_fc == other.user_fc {
// // Safety
// // - We guarantee that FC<P> is the same by function pointer
// // - Because FC<P> is the same, then P must be the same (even with generics)
// // - Non-static P are autoderived to memoize as false
// // - This comparator is only called on a corresponding set of bumpframes
// let props_memoized = unsafe {
// let real_other: &P = &*(other.raw_props as *const _ as *const P);
// props.memoize(real_other)
// };
// It's only okay to memoize if there are no children and the props can be memoized
// Implementing memoize is unsafe and done automatically with the props trait
props_memoized
} else {
false
}
}
});
let comparator = Some(unsafe { BumpBox::from_raw(comparator) });
let drop_props = {
// create a closure to drop the props
let mut has_dropped = false;
let drop_props: &mut dyn FnMut() = bump.alloc_with(|| {
move || unsafe {
log::debug!("dropping props!");
if !has_dropped {
let real_other = raw_props as *mut _ as *mut P;
let b = BumpBox::from_raw(real_other);
std::mem::drop(b);
has_dropped = true;
} else {
panic!("Drop props called twice - this is an internal failure of Dioxus");
}
}
});
let drop_props = unsafe { BumpBox::from_raw(drop_props) };
RefCell::new(Some(drop_props))
};
// // It's only okay to memoize if there are no children and the props can be memoized
// // Implementing memoize is unsafe and done automatically with the props trait
// props_memoized
// } else {
// false
// }
// }
// });
// let comparator = Some(unsafe { BumpBox::from_raw(comparator) });
let key = key.map(|f| self.raw_text(f).0);
let caller: &'a mut dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> =
bump.alloc(move |scope: &ScopeInner| -> Element {
log::debug!("calling component renderr {:?}", scope.our_arena_idx);
let props: &'_ P = unsafe { &*(raw_props as *const P) };
let caller = match P::IS_STATIC {
true => {
// it just makes sense to box the props
let boxed_props: Box<P> = Box::new(props);
let props_we_know_are_static = todo!();
VCompCaller::Owned(Box::new(|f| {
//
let scp: &'a ScopeInner = unsafe { std::mem::transmute(scope) };
let s: Scope<'a, P> = (scp, props);
let p = todo!();
let res: Element = component(s);
unsafe { std::mem::transmute(res) }
});
todo!()
}))
}
false => VCompCaller::Borrowed({
//
let caller = unsafe { BumpBox::from_raw(caller) };
todo!()
// let caller = bump.alloc()
}),
};
VNode::Component(bump.alloc(VComponent {
user_fc,
comparator,
raw_props,
caller,
is_static: P::IS_STATIC,
key,
can_memoize: P::IS_STATIC,
drop_props,
associated_scope: Cell::new(None),
}))
todo!()
// let caller: &'a mut dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> =
// bump.alloc(move |scope: &ScopeInner| -> Element {
// log::debug!("calling component renderr {:?}", scope.our_arena_idx);
// let props: &'_ P = unsafe { &*(raw_props as *const P) };
// let scp: &'a ScopeInner = unsafe { std::mem::transmute(scope) };
// let s: Scope<'a, P> = (scp, props);
// let res: Element = component(s);
// unsafe { std::mem::transmute(res) }
// });
// let caller = unsafe { BumpBox::from_raw(caller) };
// VNode::Component(bump.alloc(VComponent {
// user_fc,
// comparator,
// raw_props,
// caller,
// is_static: P::IS_STATIC,
// key,
// can_memoize: P::IS_STATIC,
// drop_props,
// associated_scope: Cell::new(None),
// }))
}
pub fn fragment_from_iter(
@@ -604,7 +633,6 @@ impl<'a> NodeFactory<'a> {
VNode::Fragment(VFragment {
children,
key: None,
is_static: false,
})
}
@@ -684,7 +712,6 @@ impl<'a> IntoVNode<'a> for Option<LazyNodes<'a, '_>> {
Some(lazy) => lazy.call(cx),
None => VNode::Fragment(VFragment {
children: &[],
is_static: false,
key: None,
}),
}
+3 -3
View File
@@ -32,18 +32,18 @@ pub(crate) struct ResourcePool {
impl ResourcePool {
/// this is unsafe because the caller needs to track which other scopes it's already using
pub fn get_scope(&self, idx: ScopeId) -> Option<&ScopeInner> {
pub fn get_scope(&self, idx: &ScopeId) -> Option<&ScopeInner> {
let inner = unsafe { &*self.components.get() };
inner.get(idx.0)
}
/// this is unsafe because the caller needs to track which other scopes it's already using
pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut ScopeInner> {
pub fn get_scope_mut(&self, idx: &ScopeId) -> Option<&mut ScopeInner> {
let inner = unsafe { &mut *self.components.get() };
inner.get_mut(idx.0)
}
pub fn try_remove(&self, id: ScopeId) -> Option<ScopeInner> {
pub fn try_remove(&self, id: &ScopeId) -> Option<ScopeInner> {
let inner = unsafe { &mut *self.components.get() };
Some(inner.remove(id.0))
// .try_remove(id.0)
+105 -121
View File
@@ -68,14 +68,17 @@ do anything too arduous from onInput.
For the rest, we defer to the rIC period and work down each queue from high to low.
*/
use crate::heuristics::*;
use crate::innerlude::*;
use bumpalo::Bump;
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures_util::{pin_mut, stream::FuturesUnordered, Future, FutureExt, StreamExt};
use fxhash::FxHashMap;
use fxhash::FxHashSet;
use indexmap::IndexSet;
use slab::Slab;
use std::pin::Pin;
use std::task::Poll;
use std::{
any::{Any, TypeId},
cell::{Cell, UnsafeCell},
@@ -85,11 +88,7 @@ use std::{
#[derive(Clone)]
pub(crate) struct EventChannel {
pub task_counter: Rc<Cell<u64>>,
pub cur_subtree: Rc<Cell<u32>>,
pub sender: UnboundedSender<SchedulerMsg>,
pub schedule_any_immediate: Rc<dyn Fn(ScopeId)>,
pub submit_task: Rc<dyn Fn(FiberTask) -> TaskHandle>,
pub get_shared_context: GetSharedContext,
}
@@ -101,16 +100,6 @@ pub enum SchedulerMsg {
// setstate
Immediate(ScopeId),
// tasks
Task(TaskMsg),
}
pub enum TaskMsg {
ToggleTask(u64),
PauseTask(u64),
ResumeTask(u64),
DropTask(u64),
}
/// The scheduler holds basically everything around "working"
@@ -126,16 +115,26 @@ pub enum TaskMsg {
///
/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task.
///
/// There's a lot of raw pointers here...
///
/// Since we're building self-referential structures for each component, we need to make sure that the referencs stay stable
/// The best way to do that is a bump allocator.
///
///
///
pub(crate) struct Scheduler {
/// All mounted components are arena allocated to make additions, removals, and references easy to work with
/// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena.
///
/// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
/// and rusts's guarantees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
pub pool: ResourcePool,
// /// All mounted components are arena allocated to make additions, removals, and references easy to work with
// /// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena.
// ///
// /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
// /// and rusts's guarantees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
// pub pool: ResourcePool,
//
pub component_arena: Bump,
pub heuristics: HeuristicsEngine,
pub free_components: VecDeque<*mut ScopeInner>,
pub heuristics: FxHashMap<FcSlot, Heuristic>,
pub receiver: UnboundedReceiver<SchedulerMsg>,
@@ -145,104 +144,70 @@ pub(crate) struct Scheduler {
// Every component that has futures that need to be polled
pub pending_futures: FxHashSet<ScopeId>,
// In-flight futures
pub async_tasks: FuturesUnordered<FiberTask>,
// // scheduler stuff
// pub current_priority: EventPriority,
pub ui_events: VecDeque<UserEvent>,
pub pending_immediates: VecDeque<ScopeId>,
pub pending_tasks: VecDeque<UserEvent>,
pub batched_events: VecDeque<UserEvent>,
pub garbage_scopes: HashSet<ScopeId>,
pub dirty_scopes: IndexSet<ScopeId>,
pub saved_state: Option<SavedDiffWork<'static>>,
pub in_progress: bool,
}
pub type FcSlot = *const ();
pub struct Heuristic {
hook_arena_size: usize,
node_arena_size: usize,
}
impl Scheduler {
pub(crate) fn new(
sender: UnboundedSender<SchedulerMsg>,
receiver: UnboundedReceiver<SchedulerMsg>,
component_capacity: usize,
element_capacity: usize,
) -> Self {
/*
Preallocate 2000 elements and 100 scopes to avoid dynamic allocation.
Perhaps this should be configurable from some external config?
*/
let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100)));
let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(2000)));
let heuristics = HeuristicsEngine::new();
let task_counter = Rc::new(Cell::new(0));
let cur_subtree = Rc::new(Cell::new(0));
// let components = Rc::new(UnsafeCell::new(Slab::with_capacity(component_capacity)));
let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(element_capacity)));
let channel = EventChannel {
cur_subtree,
task_counter: task_counter.clone(),
sender: sender.clone(),
schedule_any_immediate: {
let sender = sender.clone();
Rc::new(move |id| {
//
log::debug!("scheduling immediate! {:?}", id);
sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap()
})
},
// todo: we want to get the futures out of the scheduler message
// the scheduler message should be send/sync
submit_task: {
Rc::new(move |fiber_task| {
let task_id = task_counter.get();
task_counter.set(task_id + 1);
todo!();
// sender
// .unbounded_send(SchedulerMsg::Task(TaskMsg::SubmitTask(
// fiber_task, task_id,
// )))
// .unwrap();
TaskHandle {
our_id: task_id,
sender: sender.clone(),
}
})
},
get_shared_context: {
let components = components.clone();
Rc::new(move |id, ty| {
let components = unsafe { &*components.get() };
let mut search: Option<&ScopeInner> = components.get(id.0);
while let Some(inner) = search.take() {
if let Some(shared) = inner.shared_contexts.borrow().get(&ty) {
return Some(shared.clone());
} else {
search = inner.parent_idx.map(|id| components.get(id.0)).flatten();
}
}
None
})
todo!()
// let components = components.clone();
// Rc::new(move |id, ty| {
// let components = unsafe { &*components.get() };
// let mut search: Option<&ScopeInner> = components.get(id.0);
// while let Some(inner) = search.take() {
// if let Some(shared) = inner.shared_contexts.borrow().get(&ty) {
// return Some(shared.clone());
// } else {
// search = inner.parent_idx.map(|id| components.get(id.0)).flatten();
// }
// }
// None
// })
},
};
let pool = ResourcePool {
components,
raw_elements,
channel,
};
let async_tasks = FuturesUnordered::new();
// push a task that would never resolve - prevents us from immediately aborting the scheduler
async_tasks.push(Box::pin(async {
std::future::pending::<()>().await;
ScopeId(0)
}) as FiberTask);
// let pool = ResourcePool {
// components,
// raw_elements,
// channel,
// };
let saved_state = SavedDiffWork {
mutations: Mutations::new(),
@@ -251,22 +216,15 @@ impl Scheduler {
};
Self {
pool,
// pool,
receiver,
async_tasks,
pending_garbage: FxHashSet::default(),
heuristics,
ui_events: VecDeque::new(),
pending_immediates: VecDeque::new(),
pending_tasks: VecDeque::new(),
batched_events: VecDeque::new(),
garbage_scopes: HashSet::new(),
@@ -275,6 +233,8 @@ impl Scheduler {
dirty_scopes: Default::default(),
saved_state: Some(saved_state),
in_progress: false,
heuristics: todo!(),
}
}
@@ -282,7 +242,7 @@ impl Scheduler {
pub fn handle_ui_event(&mut self, event: UserEvent) -> bool {
let (discrete, priority) = event_meta(&event);
if let Some(scope) = self.pool.get_scope_mut(event.scope) {
if let Some(scope) = self.get_scope_mut(&event.scope) {
if let Some(element) = event.mounted_dom_id {
// TODO: bubble properly here
scope.call_listener(event, element);
@@ -308,7 +268,7 @@ impl Scheduler {
fn prepare_work(&mut self) {
// while let Some(trigger) = self.ui_events.pop_back() {
// if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {}
// if let Some(scope) = self.get_scope_mut(&trigger.scope) {}
// }
}
@@ -328,15 +288,9 @@ impl Scheduler {
unsafe fn load_work(&mut self) -> SavedDiffWork<'static> {
self.saved_state.take().unwrap().extend()
}
pub fn handle_task(&mut self, evt: TaskMsg) {
//
}
pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
match msg {
//
SchedulerMsg::Task(msg) => todo!(),
SchedulerMsg::Immediate(_) => todo!(),
SchedulerMsg::UiEvent(event) => {
@@ -344,7 +298,7 @@ impl Scheduler {
let (discrete, priority) = event_meta(&event);
if let Some(scope) = self.pool.get_scope_mut(event.scope) {
if let Some(scope) = self.get_scope_mut(&event.scope) {
if let Some(element) = event.mounted_dom_id {
// TODO: bubble properly here
scope.call_listener(event, element);
@@ -374,19 +328,19 @@ impl Scheduler {
let saved_state = unsafe { self.load_work() };
// We have to split away some parts of ourself - current lane is borrowed mutably
let shared = self.pool.clone();
let shared = self.clone();
let mut machine = unsafe { saved_state.promote(&shared) };
let mut ran_scopes = FxHashSet::default();
if machine.stack.is_empty() {
let shared = self.pool.clone();
let shared = self.clone();
self.dirty_scopes
.retain(|id| shared.get_scope(*id).is_some());
.retain(|id| shared.get_scope(id).is_some());
self.dirty_scopes.sort_by(|a, b| {
let h1 = shared.get_scope(*a).unwrap().height;
let h2 = shared.get_scope(*b).unwrap().height;
let h1 = shared.get_scope(a).unwrap().height;
let h2 = shared.get_scope(b).unwrap().height;
h1.cmp(&h2).reverse()
});
@@ -396,8 +350,8 @@ impl Scheduler {
ran_scopes.insert(scopeid);
log::debug!("about to run scope {:?}", scopeid);
if let Some(component) = self.pool.get_scope_mut(scopeid) {
if component.run_scope(&self.pool) {
if let Some(component) = self.get_scope_mut(&scopeid) {
if component.run_scope(&self) {
let (old, new) =
(component.frames.wip_head(), component.frames.fin_head());
// let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
@@ -494,7 +448,6 @@ impl Scheduler {
while self.has_any_work() {
while let Ok(Some(msg)) = self.receiver.try_next() {
match msg {
SchedulerMsg::Task(t) => todo!(),
SchedulerMsg::Immediate(im) => {
self.dirty_scopes.insert(im);
}
@@ -506,7 +459,7 @@ impl Scheduler {
// switch our priority, pop off any work
while let Some(event) = self.ui_events.pop_front() {
if let Some(scope) = self.pool.get_scope_mut(event.scope) {
if let Some(scope) = self.get_scope_mut(&event.scope) {
if let Some(element) = event.mounted_dom_id {
log::info!("Calling listener {:?}, {:?}", event.scope, element);
@@ -519,7 +472,6 @@ impl Scheduler {
self.dirty_scopes.insert(im);
}
SchedulerMsg::UiEvent(e) => self.ui_events.push_back(e),
SchedulerMsg::Task(_) => todo!(),
}
}
}
@@ -562,19 +514,19 @@ impl Scheduler {
///
/// Typically used to kickstart the VirtualDOM after initialization.
pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations {
let mut shared = self.pool.clone();
let mut shared = self.clone();
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
// TODO: drain any in-flight work
let cur_component = self
.pool
.get_scope_mut(base_scope)
.get_scope_mut(&base_scope)
.expect("The base scope should never be moved");
log::debug!("rebuild {:?}", base_scope);
// We run the component. If it succeeds, then we can diff it and add the changes to the dom.
if cur_component.run_scope(&self.pool) {
if cur_component.run_scope(&self) {
diff_machine
.stack
.create_node(cur_component.frames.fin_head(), MountType::Append);
@@ -596,13 +548,13 @@ impl Scheduler {
pub fn hard_diff(&mut self, base_scope: ScopeId) -> Mutations {
let cur_component = self
.pool
.get_scope_mut(base_scope)
.get_scope_mut(&base_scope)
.expect("The base scope should never be moved");
log::debug!("hard diff {:?}", base_scope);
if cur_component.run_scope(&self.pool) {
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self.pool);
if cur_component.run_scope(&self) {
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self);
diff_machine.cfg.force_diff = true;
diff_machine.diff_scope(base_scope);
diff_machine.mutations
@@ -611,3 +563,35 @@ impl Scheduler {
}
}
}
impl Future for Scheduler {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
let mut all_pending = true;
for fut in self.pending_futures.iter() {
let scope = self
.pool
.get_scope_mut(&fut)
.expect("Scope should never be moved");
let items = scope.items.get_mut();
for task in items.tasks.iter_mut() {
let t = task.as_mut();
let g = unsafe { Pin::new_unchecked(t) };
match g.poll(cx) {
Poll::Ready(r) => {
all_pending = false;
}
Poll::Pending => {}
}
}
}
match all_pending {
true => Poll::Pending,
false => Poll::Ready(()),
}
}
}
+258 -284
View File
@@ -6,36 +6,27 @@ use std::{
cell::{Cell, RefCell},
collections::HashMap,
future::Future,
pin::Pin,
rc::Rc,
};
use crate::{innerlude::*, lazynodes::LazyNodes};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::ops::Deref;
/// Components in Dioxus use the "Context" object to interact with their lifecycle.
///
/// This lets components access props, schedule updates, integrate hooks, and expose shared state.
///
/// Note: all of these methods are *imperative* - they do not act as hooks! They are meant to be used by hooks
/// to provide complex behavior. For instance, calling "add_shared_state" on every render is considered a leak. This method
/// exists for the `use_provide_state` hook to provide a shared state object.
///
/// For the most part, the only method you should be using regularly is `render`.
///
/// ## Example
///
/// ```ignore
/// #[derive(Properties)]
/// struct Props {
/// #[derive(Props)]
/// struct ExampleProps {
/// name: String
/// }
///
/// fn example(cx: Context<Props>) -> VNode {
/// html! {
/// <div> "Hello, {cx.name}" </div>
/// }
/// fn Example((cx, props): Scope<Props>) -> Element {
/// cx.render(rsx!{ div {"Hello, {props.name}"} })
/// }
/// ```
pub type Context<'a> = &'a ScopeInner;
@@ -59,7 +50,8 @@ pub struct ScopeInner {
// Nodes
pub(crate) frames: ActiveFrame,
pub(crate) caller: BumpBox<'static, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>>,
pub(crate) vcomp: *const VComponent<'static>,
/*
we care about:
@@ -67,12 +59,7 @@ pub struct ScopeInner {
- borrowed props (and how to drop them when the parent is dropped)
- suspended nodes (and how to call their callback when their associated tasks are complete)
*/
pub(crate) listeners: RefCell<Vec<*const Listener<'static>>>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
pub(crate) suspended_nodes: RefCell<FxHashMap<u64, *const VSuspended<'static>>>,
pub(crate) tasks: RefCell<Vec<BumpBox<'static, dyn Future<Output = ()>>>>,
pub(crate) pending_effects: RefCell<Vec<BumpBox<'static, dyn FnMut()>>>,
pub(crate) items: RefCell<SelfReferentialItems<'static>>,
// State
pub(crate) hooks: HookList,
@@ -80,15 +67,221 @@ pub struct ScopeInner {
// todo: move this into a centralized place - is more memory efficient
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
// whenever set_state is called, we fire off a message to the scheduler
// this closure _is_ the method called by schedule_update that marks this component as dirty
pub(crate) memoized_updater: Rc<dyn Fn()>,
pub(crate) shared: EventChannel,
}
/// Public interface for Scopes.
pub struct SelfReferentialItems<'a> {
pub(crate) listeners: Vec<*const Listener<'a>>,
pub(crate) borrowed_props: Vec<*const VComponent<'a>>,
pub(crate) suspended_nodes: FxHashMap<u64, *const VSuspended<'a>>,
pub(crate) tasks: Vec<BumpBox<'a, dyn Future<Output = ()>>>,
pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
}
impl ScopeInner {
// we are being created in the scope of an existing component (where the creator_node lifetime comes into play)
// we are going to break this lifetime by force in order to save it on ourselves.
// To make sure that the lifetime isn't truly broken, we receive a Weak RC so we can't keep it around after the parent dies.
// This should never happen, but is a good check to keep around
//
// Scopes cannot be made anywhere else except for this file
// Therefore, their lifetimes are connected exclusively to the virtual dom
pub(crate) fn new(
vcomp: &VComponent,
our_arena_idx: ScopeId,
parent_idx: Option<ScopeId>,
height: u32,
subtree: u32,
shared: EventChannel,
) -> Self {
let vcomp = unsafe { std::mem::transmute(vcomp as *const VComponent) };
Self {
shared,
parent_idx,
our_arena_idx,
height,
subtree: Cell::new(subtree),
is_subtree_root: Cell::new(false),
frames: ActiveFrame::new(),
vcomp,
hooks: Default::default(),
shared_contexts: Default::default(),
items: RefCell::new(SelfReferentialItems {
listeners: Default::default(),
borrowed_props: Default::default(),
suspended_nodes: Default::default(),
tasks: Default::default(),
pending_effects: Default::default(),
}),
}
}
/// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from
/// causing UB in our tree.
///
/// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components
/// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove
/// any possible references to the data in the hook list.
///
/// References to hook data can only be stored in listeners and component props. During diffing, we make sure to log
/// all listeners and borrowed props so we can clear them here.
///
/// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will
/// be dropped.
pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) {
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
// run the hooks (which hold an &mut Reference)
// right now, we don't drop
self.items
.get_mut()
.borrowed_props
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|comp| {
// First drop the component's undropped references
let scope_id = comp
.associated_scope
.get()
.expect("VComponents should be associated with a valid Scope");
if let Some(scope) = pool.get_scope_mut(&scope_id) {
scope.ensure_drop_safety(pool);
todo!("drop the component's props");
// let mut drop_props = comp.drop_props.borrow_mut().take().unwrap();
// drop_props();
}
});
// Now that all the references are gone, we can safely drop our own references in our listeners.
self.items
.get_mut()
.listeners
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|listener| drop(listener.callback.borrow_mut().take()));
}
/// A safe wrapper around calling listeners
pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) {
let listners = &mut self.items.get_mut().listeners;
let raw_listener = listners.iter().find(|lis| {
let search = unsafe { &***lis };
if search.event == event.name {
let search_id = search.mounted_node.get();
search_id.map(|f| f == element).unwrap_or(false)
} else {
false
}
});
if let Some(raw_listener) = raw_listener {
let listener = unsafe { &**raw_listener };
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
(cb)(event.event);
}
} else {
log::warn!("An event was triggered but there was no listener to handle it");
}
}
// General strategy here is to load up the appropriate suspended task and then run it.
// Suspended nodes cannot be called repeatedly.
pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) {
let mut nodes = &mut self.items.get_mut().suspended_nodes;
if let Some(suspended) = nodes.remove(&task_id) {
let sus: &'a VSuspended<'static> = unsafe { &*suspended };
let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) };
let mut boxed = sus.callback.borrow_mut().take().unwrap();
let new_node: Element<'a> = boxed();
}
}
// run the list of effects
pub(crate) fn run_effects(&mut self, pool: &ResourcePool) {
for mut effect in self.items.get_mut().pending_effects.drain(..) {
effect();
}
}
/// Render this component.
///
/// Returns true if the scope completed successfully and false if running failed (IE a None error was propagated).
pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool {
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
self.ensure_drop_safety(pool);
// Safety:
// - We dropped the listeners, so no more &mut T can be used while these are held
// - All children nodes that rely on &mut T are replaced with a new reference
unsafe { self.hooks.reset() };
// Safety:
// - We've dropped all references to the wip bump frame
unsafe { self.frames.reset_wip_frame() };
let items = self.items.get_mut();
// just forget about our suspended nodes while we're at it
items.suspended_nodes.clear();
// guarantee that we haven't screwed up - there should be no latent references anywhere
debug_assert!(items.listeners.is_empty());
debug_assert!(items.suspended_nodes.is_empty());
debug_assert!(items.borrowed_props.is_empty());
log::debug!("Borrowed stuff is successfully cleared");
// temporarily cast the vcomponent to the right lifetime
let vcomp = self.load_vcomp();
let render: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = todo!();
// Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders.
if let Some(builder) = render(self) {
let new_head = builder.into_vnode(NodeFactory {
bump: &self.frames.wip_frame().bump,
});
log::debug!("Render is successful");
// the user's component succeeded. We can safely cycle to the next frame
self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
self.frames.cycle_frame();
true
} else {
false
}
}
pub(crate) fn new_subtree(&self) -> Option<u32> {
todo!()
// if self.is_subtree_root.get() {
// None
// } else {
// let cur = self.shared.cur_subtree.get();
// self.shared.cur_subtree.set(cur + 1);
// Some(cur)
// }
}
pub(crate) fn update_vcomp(&mut self, vcomp: &VComponent) {
let f: *const _ = vcomp;
self.vcomp = unsafe { std::mem::transmute(f) };
}
pub(crate) fn load_vcomp<'a>(&'a mut self) -> &'a VComponent<'a> {
unsafe { std::mem::transmute(&*self.vcomp) }
}
/// Get the root VNode for this Scope.
///
/// This VNode is the "entrypoint" VNode. If the component renders multiple nodes, then this VNode will be a fragment.
@@ -128,16 +321,6 @@ impl ScopeInner {
self.subtree.get()
}
pub(crate) fn new_subtree(&self) -> Option<u32> {
if self.is_subtree_root.get() {
None
} else {
let cur = self.shared.cur_subtree.get();
self.shared.cur_subtree.set(cur + 1);
Some(cur)
}
}
/// Get the height of this Scope - IE the number of scopes above it.
///
/// A Scope with a height of `0` is the root scope - there are no other scopes above it.
@@ -192,233 +375,17 @@ impl ScopeInner {
pub fn scope_id(&self) -> ScopeId {
self.our_arena_idx
}
}
// The type of closure that wraps calling components
/// The type of task that gets sent to the task scheduler
/// Submitting a fiber task returns a handle to that task, which can be used to wake up suspended nodes
pub type FiberTask = Pin<Box<dyn Future<Output = ScopeId>>>;
/// Private interface for Scopes.
impl ScopeInner {
// we are being created in the scope of an existing component (where the creator_node lifetime comes into play)
// we are going to break this lifetime by force in order to save it on ourselves.
// To make sure that the lifetime isn't truly broken, we receive a Weak RC so we can't keep it around after the parent dies.
// This should never happen, but is a good check to keep around
//
// Scopes cannot be made anywhere else except for this file
// Therefore, their lifetimes are connected exclusively to the virtual dom
pub(crate) fn new(
caller: BumpBox<dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>>,
our_arena_idx: ScopeId,
parent_idx: Option<ScopeId>,
height: u32,
subtree: u32,
shared: EventChannel,
) -> Self {
let schedule_any_update = shared.schedule_any_immediate.clone();
let memoized_updater = Rc::new(move || schedule_any_update(our_arena_idx));
// wipe away the associated lifetime - we are going to manually manage the one-way lifetime graph
let caller = unsafe { std::mem::transmute(caller) };
Self {
memoized_updater,
shared,
caller,
parent_idx,
our_arena_idx,
height,
subtree: Cell::new(subtree),
is_subtree_root: Cell::new(false),
tasks: Default::default(),
frames: ActiveFrame::new(),
hooks: Default::default(),
pending_effects: Default::default(),
suspended_nodes: Default::default(),
shared_contexts: Default::default(),
listeners: Default::default(),
borrowed_props: Default::default(),
}
}
pub(crate) fn update_scope_dependencies(
&mut self,
caller: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>,
) {
log::debug!("Updating scope dependencies {:?}", self.our_arena_idx);
let caller = caller as *const _;
self.caller = unsafe { std::mem::transmute(caller) };
}
/// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from
/// causing UB in our tree.
///
/// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components
/// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove
/// any possible references to the data in the hook list.
///
/// References to hook data can only be stored in listeners and component props. During diffing, we make sure to log
/// all listeners and borrowed props so we can clear them here.
///
/// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will
/// be dropped.
pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) {
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
// run the hooks (which hold an &mut Reference)
// right now, we don't drop
self.borrowed_props
.get_mut()
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|comp| {
// First drop the component's undropped references
let scope_id = comp
.associated_scope
.get()
.expect("VComponents should be associated with a valid Scope");
if let Some(scope) = pool.get_scope_mut(scope_id) {
scope.ensure_drop_safety(pool);
let mut drop_props = comp.drop_props.borrow_mut().take().unwrap();
drop_props();
}
});
// Now that all the references are gone, we can safely drop our own references in our listeners.
self.listeners
.get_mut()
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|listener| drop(listener.callback.borrow_mut().take()));
}
/// A safe wrapper around calling listeners
pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) {
let listners = self.listeners.borrow_mut();
let raw_listener = listners.iter().find(|lis| {
let search = unsafe { &***lis };
if search.event == event.name {
let search_id = search.mounted_node.get();
search_id.map(|f| f == element).unwrap_or(false)
} else {
false
}
});
if let Some(raw_listener) = raw_listener {
let listener = unsafe { &**raw_listener };
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
(cb)(event.event);
}
} else {
log::warn!("An event was triggered but there was no listener to handle it");
}
}
/*
General strategy here is to load up the appropriate suspended task and then run it.
Suspended nodes cannot be called repeatedly.
*/
pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) {
let mut nodes = self.suspended_nodes.borrow_mut();
if let Some(suspended) = nodes.remove(&task_id) {
let sus: &'a VSuspended<'static> = unsafe { &*suspended };
let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) };
let mut boxed = sus.callback.borrow_mut().take().unwrap();
let new_node: Element<'a> = boxed();
}
}
// run the list of effects
pub(crate) fn run_effects(&mut self, pool: &ResourcePool) {
todo!()
// let mut effects = self.frames.effects.borrow_mut();
// let mut effects = effects.drain(..).collect::<Vec<_>>();
// for effect in effects {
// let effect = unsafe { &*effect };
// let effect = effect.as_ref();
// let mut effect = effect.borrow_mut();
// let mut effect = effect.as_mut();
// effect.run(pool);
// }
}
/// Render this component.
///
/// Returns true if the scope completed successfully and false if running failed (IE a None error was propagated).
pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool {
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
self.ensure_drop_safety(pool);
// Safety:
// - We dropped the listeners, so no more &mut T can be used while these are held
// - All children nodes that rely on &mut T are replaced with a new reference
unsafe { self.hooks.reset() };
// Safety:
// - We've dropped all references to the wip bump frame
unsafe { self.frames.reset_wip_frame() };
// just forget about our suspended nodes while we're at it
self.suspended_nodes.get_mut().clear();
// guarantee that we haven't screwed up - there should be no latent references anywhere
debug_assert!(self.listeners.borrow().is_empty());
debug_assert!(self.suspended_nodes.borrow().is_empty());
debug_assert!(self.borrowed_props.borrow().is_empty());
log::debug!("Borrowed stuff is successfully cleared");
// Cast the caller ptr from static to one with our own reference
let render: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = unsafe { &*self.caller };
// Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders.
if let Some(builder) = render(self) {
let new_head = builder.into_vnode(NodeFactory {
bump: &self.frames.wip_frame().bump,
});
log::debug!("Render is successful");
// the user's component succeeded. We can safely cycle to the next frame
self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
self.frames.cycle_frame();
true
} else {
false
}
}
/// Create a subscription that schedules a future render for the reference component
///
/// ## Notice: you should prefer using prepare_update and get_scope_id
pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
self.memoized_updater.clone()
}
/// Get the [`ScopeId`] of a mounted component.
///
/// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted.
pub fn needs_update(&self) {
(self.memoized_updater)()
}
/// Get the [`ScopeId`] of a mounted component.
///
/// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted.
pub fn needs_update_any(&self, id: ScopeId) {
(self.shared.schedule_any_immediate)(id)
// pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
let chan = self.shared.sender.clone();
let id = self.scope_id();
Rc::new(move || {
chan.unbounded_send(SchedulerMsg::Immediate(id));
})
}
/// Schedule an update for any component given its ScopeId.
@@ -427,7 +394,27 @@ impl ScopeInner {
///
/// This method should be used when you want to schedule an update for a component
pub fn schedule_update_any(&self) -> Rc<dyn Fn(ScopeId)> {
self.shared.schedule_any_immediate.clone()
let chan = self.shared.sender.clone();
Rc::new(move |id| {
chan.unbounded_send(SchedulerMsg::Immediate(id));
})
}
/// Get the [`ScopeId`] of a mounted component.
///
/// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted.
pub fn needs_update(&self) {
self.needs_update_any(self.scope_id())
}
/// Get the [`ScopeId`] of a mounted component.
///
/// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted.
pub fn needs_update_any(&self, id: ScopeId) {
self.shared
.sender
.unbounded_send(SchedulerMsg::Immediate(id))
.unwrap();
}
/// Get the [`ScopeId`] of a mounted component.
@@ -475,8 +462,9 @@ impl ScopeInner {
// erase the 'src lifetime for self-referential storage
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
self.pending_effects.borrow_mut().push(self_ref_fut);
self.pending_effects.borrow().len() - 1
let mut items = self.items.borrow_mut();
items.pending_effects.push(self_ref_fut);
items.pending_effects.len() - 1
}
/// Pushes the future onto the poll queue to be polled
@@ -491,8 +479,9 @@ impl ScopeInner {
// erase the 'src lifetime for self-referential storage
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
self.tasks.borrow_mut().push(self_ref_fut);
self.tasks.borrow().len() - 1
let mut items = self.items.borrow_mut();
items.tasks.push(self_ref_fut);
items.tasks.len() - 1
}
/// This method enables the ability to expose state to children further down the VirtualDOM Tree.
@@ -580,8 +569,9 @@ impl ScopeInner {
///
/// - Initializer: closure used to create the initial hook state
/// - Runner: closure used to output a value every time the hook is used
/// - Cleanup: closure used to teardown the hook once the dom is cleaned up
///
/// To "cleanup" the hook, implement `Drop` on the stored hook value. Whenever the component is dropped, the hook
/// will be dropped as well.
///
/// # Example
///
@@ -591,32 +581,16 @@ impl ScopeInner {
/// use_hook(
/// || Rc::new(RefCell::new(initial_value())),
/// |state| state,
/// |_| {},
/// )
/// }
/// ```
pub fn use_hook<'src, State, Output, Init, Run, Cleanup>(
pub fn use_hook<'src, State: 'static, Output: 'src>(
&'src self,
initializer: Init,
runner: Run,
cleanup: Cleanup,
) -> Output
where
State: 'static,
Output: 'src,
Init: FnOnce(usize) -> State,
Run: FnOnce(&'src mut State) -> Output,
Cleanup: FnOnce(Box<State>) + 'static,
{
// If the idx is the same as the hook length, then we need to add the current hook
initializer: impl FnOnce(usize) -> State,
runner: impl FnOnce(&'src mut State) -> Output,
) -> Output {
if self.hooks.at_end() {
self.hooks.push_hook(
initializer(self.hooks.len()),
Box::new(|raw| {
let s = raw.downcast::<State>().unwrap();
cleanup(s);
}),
);
self.hooks.push_hook(initializer(self.hooks.len()));
}
runner(self.hooks.next::<State>().expect(HOOK_ERR_MSG))
-39
View File
@@ -1,39 +0,0 @@
use crate::innerlude::*;
use futures_channel::mpsc::UnboundedSender;
pub struct TaskHandle {
pub(crate) sender: UnboundedSender<SchedulerMsg>,
pub(crate) our_id: u64,
}
impl TaskHandle {
/// Toggles this coroutine off/on.
///
/// This method is not synchronous - your task will not stop immediately.
pub fn toggle(&self) {
self.sender
.unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id)))
.unwrap()
}
/// This method is not synchronous - your task will not stop immediately.
pub fn resume(&self) {
self.sender
.unbounded_send(SchedulerMsg::Task(TaskMsg::ResumeTask(self.our_id)))
.unwrap()
}
/// This method is not synchronous - your task will not stop immediately.
pub fn stop(&self) {
self.sender
.unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id)))
.unwrap()
}
/// This method is not synchronous - your task will not stop immediately.
pub fn restart(&self) {
self.sender
.unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id)))
.unwrap()
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ impl TestDom {
pub fn new() -> TestDom {
let bump = Bump::new();
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
let scheduler = Scheduler::new(sender, receiver);
let scheduler = Scheduler::new(sender, receiver, 10, 100);
TestDom { bump, scheduler }
}
-53
View File
@@ -11,59 +11,6 @@ pub fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// use crate::task::{Context, Poll};
/// Cooperatively gives up a timeslice to the task scheduler.
///
/// Calling this function will move the currently executing future to the back
/// of the execution queue, making room for other futures to execute. This is
/// especially useful after running CPU-intensive operations inside a future.
///
/// See also [`task::spawn_blocking`].
///
/// [`task::spawn_blocking`]: fn.spawn_blocking.html
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// # async_std::task::block_on(async {
/// #
/// use async_std::task;
///
/// task::yield_now().await;
/// #
/// # })
/// ```
#[inline]
pub async fn yield_now() {
YieldNow(false).await
}
struct YieldNow(bool);
impl Future for YieldNow {
type Output = ();
// The futures executor is implemented as a FIFO queue, so all this future
// does is re-schedule the future back to the end of the queue, giving room
// for other futures to progress.
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if !self.0 {
self.0 = true;
cx.waker().wake_by_ref();
Poll::Pending
} else {
Poll::Ready(())
}
}
}
/// A component's unique identifier.
///
/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
+600 -52
View File
@@ -19,10 +19,24 @@
//! This module includes just the barebones for a complete VirtualDOM API.
//! Additional functionality is defined in the respective files.
use crate::innerlude::*;
use bumpalo::Bump;
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures_util::{pin_mut, stream::FuturesUnordered, Future, FutureExt, StreamExt};
use fxhash::FxHashMap;
use fxhash::FxHashSet;
use indexmap::IndexSet;
use slab::Slab;
use std::pin::Pin;
use std::task::Poll;
use std::{
any::{Any, TypeId},
cell::{Cell, UnsafeCell},
collections::{HashSet, VecDeque},
rc::Rc,
};
use crate::innerlude::*;
use std::{any::Any, rc::Rc};
/// An integrated virtual node system that progresses events and diffs UI trees.
///
@@ -55,8 +69,6 @@ use std::{any::Any, rc::Rc};
/// }
/// ```
pub struct VirtualDom {
scheduler: Scheduler,
base_scope: ScopeId,
root_fc: Box<dyn Any>,
@@ -65,6 +77,49 @@ pub struct VirtualDom {
// we need to keep the allocation around, but we don't necessarily use it
_root_caller: Box<dyn Any>,
// /// All mounted components are arena allocated to make additions, removals, and references easy to work with
// /// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena.
// ///
// /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
// /// and rusts's guarantees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
// pub pool: ResourcePool,
//
pub component_arena: Bump,
pub free_components: VecDeque<*mut ScopeInner>,
pub heuristics: FxHashMap<FcSlot, Heuristic>,
pub receiver: UnboundedReceiver<SchedulerMsg>,
// Garbage stored
pub pending_garbage: FxHashSet<ScopeId>,
// Every component that has futures that need to be polled
pub pending_futures: FxHashSet<ScopeId>,
pub ui_events: VecDeque<UserEvent>,
pub pending_immediates: VecDeque<ScopeId>,
pub batched_events: VecDeque<UserEvent>,
pub garbage_scopes: HashSet<ScopeId>,
pub dirty_scopes: IndexSet<ScopeId>,
pub saved_state: Option<SavedDiffWork<'static>>,
pub in_progress: bool,
}
pub enum SchedulerMsg {
// events from the host
UiEvent(UserEvent),
// setstate
Immediate(ScopeId),
}
impl VirtualDom {
@@ -127,7 +182,7 @@ impl VirtualDom {
Self::new_with_props_and_scheduler(root, root_props, sender, receiver)
}
/// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler.
/// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the
///
/// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the
/// VirtualDom to be created just to retrieve its channel receiver.
@@ -137,33 +192,71 @@ impl VirtualDom {
sender: UnboundedSender<SchedulerMsg>,
receiver: UnboundedReceiver<SchedulerMsg>,
) -> Self {
let root_fc = Box::new(root);
let mut component_arena = Bump::new();
let root_props: Rc<dyn Any> = Rc::new(root_props);
// let root_fc = Box::new(root);
let props = root_props.clone();
// let root_props: Rc<dyn Any> = Rc::new(root_props);
let root_caller: Box<dyn Fn(&ScopeInner) -> Element> =
Box::new(move |scope: &ScopeInner| {
let props = props.downcast_ref::<P>().unwrap();
let node = root((scope, props));
// cast into the right lifetime
unsafe { std::mem::transmute(node) }
});
let caller = unsafe { bumpalo::boxed::Box::from_raw(root_caller.as_mut() as *mut _) };
// let props = root_props.clone();
let scheduler = Scheduler::new(sender, receiver);
// let mut root_caller: Box<dyn Fn(&ScopeInner) -> Element> =
// Box::new(move |scope: &ScopeInner| {
// let props = props.downcast_ref::<P>().unwrap();
// let node = root((scope, props));
// // cast into the right lifetime
// unsafe { std::mem::transmute(node) }
// });
let base_scope = scheduler.pool.insert_scope_with_key(|myidx| {
ScopeInner::new(caller, myidx, None, 0, 0, scheduler.pool.channel.clone())
});
// let caller = unsafe { bumpalo::boxed::Box::from_raw(root_caller.as_mut() as *mut _) };
// // todo make the memory footprint congifurable
// let scheduler = Scheduler::new(sender, receiver, 100, 2000);
// let vcomp = VComponent {
// key: todo!(),
// associated_scope: todo!(),
// user_fc: root as *const _,
// can_memoize: todo!(),
// raw_props: todo!(),
// // drop_props: todo!(),
// // caller,
// comparator: todo!(),
// caller: todo!(),
// };
// let boxed_comp = Box::new(vcomp);
// let base_scope = pool.insert_scope_with_key(|myidx| {
// ScopeInner::new(
// boxed_comp.as_ref(),
// myidx,
// None,
// 0,
// 0,
// pool.channel.clone(),
// )
// });
Self {
_root_caller: Box::new(root_caller),
root_fc,
base_scope,
scheduler,
root_props,
scheduler: todo!(),
base_scope: todo!(),
root_fc: todo!(),
root_props: todo!(),
_root_caller: todo!(),
component_arena: todo!(),
free_components: todo!(),
heuristics: todo!(),
receiver,
pending_garbage: todo!(),
pending_futures: todo!(),
ui_events: todo!(),
pending_immediates: todo!(),
batched_events: todo!(),
garbage_scopes: todo!(),
dirty_scopes: todo!(),
saved_state: todo!(),
in_progress: todo!(),
}
}
@@ -172,12 +265,12 @@ impl VirtualDom {
/// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus
/// directly.
pub fn base_scope(&self) -> &ScopeInner {
self.scheduler.pool.get_scope(self.base_scope).unwrap()
self.pool.get_scope(&self.base_scope).unwrap()
}
/// Get the [`Scope`] for a component given its [`ScopeId`]
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeInner> {
self.scheduler.pool.get_scope(id)
self.pool.get_scope(&id)
}
/// Update the root props of this VirtualDOM.
@@ -202,10 +295,10 @@ impl VirtualDom {
where
P: 'static,
{
let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap();
let root_scope = self.pool.get_scope_mut(&self.base_scope).unwrap();
// Pre-emptively drop any downstream references of the old props
root_scope.ensure_drop_safety(&self.scheduler.pool);
root_scope.ensure_drop_safety(&self.pool);
let mut root_props: Rc<dyn Any> = Rc::new(root_props);
@@ -221,8 +314,6 @@ impl VirtualDom {
std::mem::transmute(root((scope, props)))
});
root_scope.update_scope_dependencies(&root_caller);
drop(root_props);
Some(self.rebuild())
@@ -249,7 +340,7 @@ impl VirtualDom {
/// apply_edits(edits);
/// ```
pub fn rebuild(&mut self) -> Mutations {
self.scheduler.rebuild(self.base_scope)
self.rebuild(self.base_scope)
}
/// Compute a manual diff of the VirtualDOM between states.
@@ -287,7 +378,7 @@ impl VirtualDom {
/// let edits = dom.diff();
/// ```
pub fn diff(&mut self) -> Mutations {
self.scheduler.hard_diff(self.base_scope)
self.hard_diff(self.base_scope)
}
/// Runs the virtualdom immediately, not waiting for any suspended nodes to complete.
@@ -295,8 +386,8 @@ impl VirtualDom {
/// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will
/// return "None"
pub fn run_immediate(&mut self) -> Option<Vec<Mutations>> {
if self.scheduler.has_any_work() {
Some(self.scheduler.work_sync())
if self.has_any_work() {
Some(self.work_sync())
} else {
None
}
@@ -346,18 +437,18 @@ impl VirtualDom {
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
pub fn run_with_deadline(&mut self, deadline: impl FnMut() -> bool) -> Vec<Mutations<'_>> {
self.scheduler.work_with_deadline(deadline)
self.work_with_deadline(deadline)
}
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
self.scheduler.pool.channel.sender.clone()
self.pool.channel.sender.clone()
}
/// Waits for the scheduler to have work
/// This lets us poll async tasks during idle periods without blocking the main thread.
pub async fn wait_for_work(&mut self) {
// todo: poll the events once even if there is work to do to prevent starvation
if self.scheduler.has_any_work() {
if self.has_any_work() {
return;
}
@@ -365,28 +456,485 @@ impl VirtualDom {
// Wait for any new events if we have nothing to do
let tasks_fut = self.scheduler.async_tasks.next();
let scheduler_fut = self.scheduler.receiver.next();
// let tasks_fut = self.async_tasks.next();
// let scheduler_fut = self.receiver.next();
use futures_util::future::{select, Either};
match select(tasks_fut, scheduler_fut).await {
// poll the internal futures
Either::Left((_id, _)) => {
// use futures_util::future::{select, Either};
// match select(tasks_fut, scheduler_fut).await {
// // poll the internal futures
// Either::Left((_id, _)) => {
// //
// }
// // wait for an external event
// Either::Right((msg, _)) => match msg.unwrap() {
// SchedulerMsg::Task(t) => {
// self.handle_task(t);
// }
// SchedulerMsg::Immediate(im) => {
// self.dirty_scopes.insert(im);
// }
// SchedulerMsg::UiEvent(evt) => {
// self.ui_events.push_back(evt);
// }
// },
// }
}
}
pub type FcSlot = *const ();
pub struct Heuristic {
hook_arena_size: usize,
node_arena_size: usize,
}
/*
Welcome to Dioxus's cooperative, priority-based scheduler.
I hope you enjoy your stay.
Some essential reading:
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440
- https://github.com/WICG/is-input-pending
- https://web.dev/rail/
- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react
# What's going on?
Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI
snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no
point in being "fast" if you can't also be "responsive."
As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned
priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms).
The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many
different types of work simultaneously.
# How does it work?
Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked.
React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of
React, and the UI still stays snappy!
While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do
this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser
has time to paint and run animations. When the browser is finished, we can step in and continue our work.
React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not
necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially
"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause
our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields
so Rust's async machinery can take over, allowing us to customize when exactly to pause it.
React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For
now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes
before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes
are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine
future is paused and saved (self-referentially).
# Priority System
So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that
doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where
priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as
a "lower" priority. That way, we can interrupt long-running low-priority work with short-running high-priority work.
React's priority system is quite complex.
There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only
uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on.
For Dioxus, we're going with a 4 tier priority system:
- Sync: Things that need to be done by the next frame, like TextInput on controlled elements
- High: for events that block all others - clicks, keyboard, and hovers
- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events)
- Low: set_state called asynchronously, and anything generated by suspense
In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed
work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave
as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to
do anything too arduous from onInput.
For the rest, we defer to the rIC period and work down each queue from high to low.
*/
/// The scheduler holds basically everything around "working"
///
/// Each scope has the ability to lightly interact with the scheduler (IE, schedule an update) but ultimately the scheduler calls the components.
///
/// In Dioxus, the scheduler provides 4 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs
/// out.
///
/// Saved DiffMachine state can be self-referential, so we need to be careful about how we save it. All self-referential data is a link between
/// pending DiffInstructions, Mutations, and their underlying Scope. It's okay for us to be self-referential with this data, provided we don't priority
/// task shift to a higher priority task that needs mutable access to the same scopes.
///
/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task.
///
/// There's a lot of raw pointers here...
///
/// Since we're building self-referential structures for each component, we need to make sure that the referencs stay stable
/// The best way to do that is a bump allocator.
///
///
///
impl VirtualDom {
// returns true if the event is discrete
pub fn handle_ui_event(&mut self, event: UserEvent) -> bool {
let (discrete, priority) = event_meta(&event);
if let Some(scope) = self.get_scope_mut(&event.scope) {
if let Some(element) = event.mounted_dom_id {
// TODO: bubble properly here
scope.call_listener(event, element);
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
//
// self.add_dirty_scope(dirty_scope, trigger.priority)
}
}
}
// use EventPriority::*;
// match priority {
// Immediate => todo!(),
// High => todo!(),
// Medium => todo!(),
// Low => todo!(),
// }
discrete
}
fn prepare_work(&mut self) {
// while let Some(trigger) = self.ui_events.pop_back() {
// if let Some(scope) = self.get_scope_mut(&trigger.scope) {}
// }
}
// nothing to do, no events on channels, no work
pub fn has_any_work(&self) -> bool {
!(self.dirty_scopes.is_empty() && self.ui_events.is_empty())
}
/// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work
fn balance_lanes(&mut self) {}
fn save_work(&mut self, lane: SavedDiffWork) {
let saved: SavedDiffWork<'static> = unsafe { std::mem::transmute(lane) };
self.saved_state = Some(saved);
}
unsafe fn load_work(&mut self) -> SavedDiffWork<'static> {
self.saved_state.take().unwrap().extend()
}
pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
match msg {
SchedulerMsg::Immediate(_) => todo!(),
SchedulerMsg::UiEvent(event) => {
//
let (discrete, priority) = event_meta(&event);
if let Some(scope) = self.get_scope_mut(&event.scope) {
if let Some(element) = event.mounted_dom_id {
// TODO: bubble properly here
scope.call_listener(event, element);
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
//
// self.add_dirty_scope(dirty_scope, trigger.priority)
}
}
}
discrete;
}
}
}
/// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
///
/// Returns true if the lane is finished before the deadline could be met.
pub fn work_on_current_lane(
&mut self,
deadline_reached: impl FnMut() -> bool,
mutations: &mut Vec<Mutations>,
) -> bool {
// Work through the current subtree, and commit the results when it finishes
// When the deadline expires, give back the work
let saved_state = unsafe { self.load_work() };
// We have to split away some parts of ourself - current lane is borrowed mutably
let shared = self.clone();
let mut machine = unsafe { saved_state.promote(&shared) };
let mut ran_scopes = FxHashSet::default();
if machine.stack.is_empty() {
let shared = self.clone();
self.dirty_scopes
.retain(|id| shared.get_scope(id).is_some());
self.dirty_scopes.sort_by(|a, b| {
let h1 = shared.get_scope(a).unwrap().height;
let h2 = shared.get_scope(b).unwrap().height;
h1.cmp(&h2).reverse()
});
if let Some(scopeid) = self.dirty_scopes.pop() {
log::info!("handling dirty scope {:?}", scopeid);
if !ran_scopes.contains(&scopeid) {
ran_scopes.insert(scopeid);
log::debug!("about to run scope {:?}", scopeid);
if let Some(component) = self.get_scope_mut(&scopeid) {
if component.run_scope(&self) {
let (old, new) =
(component.frames.wip_head(), component.frames.fin_head());
// let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
machine.stack.scope_stack.push(scopeid);
machine.stack.push(DiffInstruction::Diff { new, old });
}
}
}
}
}
let work_completed = machine.work(deadline_reached);
// log::debug!("raw edits {:?}", machine.mutations.edits);
let mut machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
// let mut saved = machine.save();
if work_completed {
for node in machine.seen_scopes.drain() {
// self.dirty_scopes.clear();
// self.ui_events.clear();
self.dirty_scopes.remove(&node);
// self.dirty_scopes.remove(&node);
}
// wait for an external event
Either::Right((msg, _)) => match msg.unwrap() {
SchedulerMsg::Task(t) => {
self.scheduler.handle_task(t);
let mut new_mutations = Mutations::new();
for edit in machine.mutations.edits.drain(..) {
new_mutations.edits.push(edit);
}
// for edit in saved.edits.drain(..) {
// new_mutations.edits.push(edit);
// }
// std::mem::swap(&mut new_mutations, &mut saved.mutations);
mutations.push(new_mutations);
// log::debug!("saved edits {:?}", mutations);
let mut saved = machine.save();
self.save_work(saved);
true
// self.save_work(saved);
// false
} else {
false
}
}
/// The primary workhorse of the VirtualDOM.
///
/// Uses some fairly complex logic to schedule what work should be produced.
///
/// Returns a list of successful mutations.
pub fn work_with_deadline<'a>(
&'a mut self,
mut deadline: impl FnMut() -> bool,
) -> Vec<Mutations<'a>> {
/*
Strategy:
- When called, check for any UI events that might've been received since the last frame.
- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue.
- If there are any pending discrete events, then elevate our priority level. If our priority level is already "high,"
then we need to finish the high priority work first. If the current work is "low" then analyze what scopes
will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need
to bump the other work out of the way, or choose to process it so we don't have any conflicts.
'static components have a leg up here since their work can be re-used among multiple scopes.
"High priority" is only for blocking! Should only be used on "clicks"
- If there are no pending discrete events, then check for continuous events. These can be completely batched
- we batch completely until we run into a discrete event
- all continuous events are batched together
- so D C C C C C would be two separate events - D and C. IE onclick and onscroll
- D C C C C C C D C C C D would be D C D C D in 5 distinct phases.
- !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future
- we need to keep track of element parents to be able to traverse properly
Open questions:
- what if we get two clicks from the component during the same slice?
- should we batch?
- react says no - they are continuous
- but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
*/
let mut committed_mutations = Vec::<Mutations<'static>>::new();
while self.has_any_work() {
while let Ok(Some(msg)) = self.receiver.try_next() {
match msg {
SchedulerMsg::Immediate(im) => {
self.dirty_scopes.insert(im);
}
SchedulerMsg::UiEvent(evt) => {
self.ui_events.push_back(evt);
}
}
SchedulerMsg::Immediate(im) => {
self.scheduler.dirty_scopes.insert(im);
}
// switch our priority, pop off any work
while let Some(event) = self.ui_events.pop_front() {
if let Some(scope) = self.get_scope_mut(&event.scope) {
if let Some(element) = event.mounted_dom_id {
log::info!("Calling listener {:?}, {:?}", event.scope, element);
// TODO: bubble properly here
scope.call_listener(event, element);
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
match dirty_scope {
SchedulerMsg::Immediate(im) => {
self.dirty_scopes.insert(im);
}
SchedulerMsg::UiEvent(e) => self.ui_events.push_back(e),
}
}
}
}
SchedulerMsg::UiEvent(evt) => {
self.scheduler.ui_events.push_back(evt);
}
let work_complete = self.work_on_current_lane(&mut deadline, &mut committed_mutations);
if !work_complete {
return committed_mutations;
}
}
committed_mutations
}
/// Work the scheduler down, not polling any ongoing tasks.
///
/// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
pub fn work_sync<'a>(&'a mut self) -> Vec<Mutations<'a>> {
let mut committed_mutations = Vec::new();
while let Ok(Some(msg)) = self.receiver.try_next() {
self.handle_channel_msg(msg);
}
if !self.has_any_work() {
return committed_mutations;
}
while self.has_any_work() {
self.prepare_work();
self.work_on_current_lane(|| false, &mut committed_mutations);
}
committed_mutations
}
/// Restart the entire VirtualDOM from scratch, wiping away any old state and components.
///
/// Typically used to kickstart the VirtualDOM after initialization.
pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations {
let mut shared = self.clone();
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
// TODO: drain any in-flight work
let cur_component = self
.pool
.get_scope_mut(&base_scope)
.expect("The base scope should never be moved");
log::debug!("rebuild {:?}", base_scope);
// We run the component. If it succeeds, then we can diff it and add the changes to the dom.
if cur_component.run_scope(&self) {
diff_machine
.stack
.create_node(cur_component.frames.fin_head(), MountType::Append);
diff_machine.stack.scope_stack.push(base_scope);
diff_machine.work(|| false);
} else {
// todo: should this be a hard error?
log::warn!(
"Component failed to run successfully during rebuild.
This does not result in a failed rebuild, but indicates a logic failure within your app."
);
}
unsafe { std::mem::transmute(diff_machine.mutations) }
}
pub fn hard_diff(&mut self, base_scope: ScopeId) -> Mutations {
let cur_component = self
.pool
.get_scope_mut(&base_scope)
.expect("The base scope should never be moved");
log::debug!("hard diff {:?}", base_scope);
if cur_component.run_scope(&self) {
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self);
diff_machine.cfg.force_diff = true;
diff_machine.diff_scope(base_scope);
diff_machine.mutations
} else {
Mutations::new()
}
}
}
impl Future for VirtualDom {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
let mut all_pending = true;
for fut in self.pending_futures.iter() {
let scope = self
.pool
.get_scope_mut(&fut)
.expect("Scope should never be moved");
let items = scope.items.get_mut();
for task in items.tasks.iter_mut() {
let t = task.as_mut();
let g = unsafe { Pin::new_unchecked(t) };
match g.poll(cx) {
Poll::Ready(r) => {
all_pending = false;
}
Poll::Pending => {}
}
},
}
}
match all_pending {
true => Poll::Pending,
false => Poll::Ready(()),
}
}
}