Merge pull request #44 from DioxusLabs/jk/memoization

feat: memoization safety
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2021-12-20 23:36:37 -05:00
commit dbb981d500
15 files changed
+943 -704

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ struct Borrowed<'a> {
fn App2<'a>(cx: Scope<'a, Borrowed<'a>>) -> Element {
let g = eat2(&cx);
todo!()
rsx!(cx, "")
}
fn eat2(s: &ScopeState) {}
-4
View File
@@ -4,7 +4,3 @@ use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
fn main() {}
fn App(cx: Scope<()>) -> Element {
todo!()
}
+24 -7
View File
@@ -6,16 +6,15 @@ use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
fn main() {
let _ = VirtualDom::new(Parent);
let _ = VirtualDom::new(parent);
}
fn Parent(cx: Scope<()>) -> Element {
fn parent(cx: Scope<()>) -> Element {
let value = cx.use_hook(|_| String::new(), |f| f);
cx.render(rsx! {
div {
Child { name: value }
Fragment { "asd" }
child( name: value )
}
})
}
@@ -25,11 +24,11 @@ struct ChildProps<'a> {
name: &'a str,
}
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
cx.render(rsx! {
div {
h1 { "it's nested" }
Child2 { name: cx.props.name }
grandchild( name: cx.props.name )
}
})
}
@@ -39,8 +38,26 @@ struct Grandchild<'a> {
name: &'a str,
}
fn Child2<'a>(cx: Scope<'a, Grandchild<'a>>) -> Element {
fn grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> {
cx.render(rsx! {
div { "Hello {cx.props.name}!" }
great_grandchild( name: cx.props.name )
})
}
fn great_grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> {
cx.render(rsx! {
div {
h1 { "it's nested" }
}
})
}
/*
can we implement memoization as a wrapper or something? Like we just intercept the
render function?
*/
+77 -109
View File
@@ -88,6 +88,8 @@
//! More info on how to improve this diffing algorithm:
//! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/
use std::borrow::BorrowMut;
use crate::innerlude::*;
use fxhash::{FxHashMap, FxHashSet};
use smallvec::{smallvec, SmallVec};
@@ -239,15 +241,6 @@ impl<'bump> DiffStack<'bump> {
}
impl<'bump> DiffState<'bump> {
pub fn diff_scope(&mut self, id: ScopeId) {
let (old, new) = (self.scopes.wip_head(id), self.scopes.fin_head(id));
self.stack.push(DiffInstruction::Diff { old, new });
self.stack.scope_stack.push(id);
let scope = self.scopes.get_scope(id).unwrap();
self.stack.element_stack.push(scope.container);
self.work(|| false);
}
/// Progress the diffing for this "fiber"
///
/// This method implements a depth-first iterative tree traversal.
@@ -415,7 +408,7 @@ impl<'bump> DiffState<'bump> {
}
// todo: the settext optimization
//
// if children.len() == 1 {
// if let VNode::Text(vtext) = children[0] {
// self.mutations.set_text(vtext.text, real_id.as_u64());
@@ -436,40 +429,31 @@ impl<'bump> DiffState<'bump> {
let parent_idx = self.stack.current_scope().unwrap();
// Insert a new scope into our component list
let parent_scope = self.scopes.get_scope(parent_idx).unwrap();
let height = parent_scope.height + 1;
let subtree = parent_scope.subtree.get();
let parent_scope = self.scopes.get_scope_raw(parent_idx);
let caller = unsafe { std::mem::transmute(vcomponent.caller as *const _) };
let fc_ptr = vcomponent.user_fc;
let container = *self.stack.element_stack.last().unwrap();
let new_idx =
self.scopes
.new_with_key(fc_ptr, caller, parent_scope, container, height, subtree);
let props: Box<dyn AnyProps + 'bump> = vcomponent.props.borrow_mut().take().unwrap();
let props: Box<dyn AnyProps + 'static> = unsafe { std::mem::transmute(props) };
let new_idx = self.scopes.new_with_key(
vcomponent.user_fc,
props,
Some(parent_idx),
self.stack.element_stack.last().copied().unwrap(),
0,
);
// Actually initialize the caller's slot with the right address
vcomponent.associated_scope.set(Some(new_idx));
vcomponent.scope.set(Some(new_idx));
if !vcomponent.can_memoize {
let cur_scope = self.scopes.get_scope(parent_idx).unwrap();
let extended = unsafe { std::mem::transmute(vcomponent) };
cur_scope.items.borrow_mut().borrowed_props.push(extended);
} else {
// the props are currently bump allocated but we need to move them to the heap
match vcomponent.can_memoize {
true => {
// todo: implement promotion logic. save us from boxing props that we don't need
}
false => {
// track this component internally so we know the right drop order
let cur_scope = self.scopes.get_scope(parent_idx).unwrap();
let extended = unsafe { std::mem::transmute(vcomponent) };
cur_scope.items.borrow_mut().borrowed_props.push(extended);
}
}
// TODO: add noderefs to current noderef list Noderefs
let _new_component = self.scopes.get_scope(new_idx).unwrap();
log::debug!(
"initializing component {:?} with height {:?}",
new_idx,
height + 1
);
// Run the scope for one iteration to initialize it
self.scopes.run_scope(new_idx);
@@ -477,11 +461,6 @@ impl<'bump> DiffState<'bump> {
let nextnode = self.scopes.fin_head(new_idx);
self.stack.create_component(new_idx, nextnode);
// todo: subtrees
// if new_component.is_subtree_root.get() {
// self.stack.push_subtree();
// }
// Finally, insert this scope as a seen node.
self.mutations.dirty_scopes.insert(new_idx);
}
@@ -494,15 +473,23 @@ impl<'bump> DiffState<'bump> {
use VNode::*;
match (old_node, new_node) {
// Check the most common cases first
// these are *actual* elements, not wrappers around lists
(Text(old), Text(new)) => {
self.diff_text_nodes(old, new, old_node, new_node);
}
(Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node),
(Placeholder(old), Placeholder(new)) => {
if let Some(root) = old.dom_id.get() {
self.scopes.update_node(new_node, root);
new.dom_id.set(Some(root))
}
}
// These two sets are pointers to nodes but are not actually nodes themselves
(Component(old), Component(new)) => {
self.diff_component_nodes(old_node, new_node, *old, *new)
}
(Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new),
(Placeholder(old), Placeholder(new)) => self.diff_placeholder_nodes(old, new),
(Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node),
// The normal pathway still works, but generates slightly weird instructions
// This pathway just ensures we get create and replace
@@ -708,21 +695,14 @@ impl<'bump> DiffState<'bump> {
old: &'bump VComponent<'bump>,
new: &'bump VComponent<'bump>,
) {
let scope_addr = old.associated_scope.get().unwrap();
log::debug!(
"Diffing components. old_scope: {:?}, old_addr: {:?}, new_addr: {:?}",
scope_addr,
old.user_fc,
new.user_fc
);
let scope_addr = old.scope.get().unwrap();
// Make sure we're dealing with the same component (by function pointer)
if old.user_fc == new.user_fc {
self.stack.scope_stack.push(scope_addr);
// Make sure the new component vnode is referencing the right scope id
new.associated_scope.set(Some(scope_addr));
new.scope.set(Some(scope_addr));
// make sure the component's caller function is up to date
let scope = self
@@ -730,18 +710,41 @@ impl<'bump> DiffState<'bump> {
.get_scope(scope_addr)
.unwrap_or_else(|| panic!("could not find {:?}", scope_addr));
scope.caller.set(unsafe { std::mem::transmute(new.caller) });
// take the new props out regardless
// when memoizing, push to the existing scope if memoization happens
let new_props = new.props.borrow_mut().take().unwrap();
// React doesn't automatically memoize, but we do.
let props_are_the_same = old.comparator.unwrap();
let should_run = {
if old.can_memoize {
let props_are_the_same = unsafe {
scope
.props
.borrow()
.as_ref()
.unwrap()
.memoize(new_props.as_ref())
};
!props_are_the_same || self.force_diff
} else {
true
}
};
if self.force_diff || !props_are_the_same(new) {
if should_run {
let _old_props = scope
.props
.replace(unsafe { std::mem::transmute(Some(new_props)) });
// this should auto drop the previous props
self.scopes.run_scope(scope_addr);
self.diff_node(
self.scopes.wip_head(scope_addr),
self.scopes.fin_head(scope_addr),
);
}
} else {
// memoization has taken place
drop(new_props);
};
self.stack.scope_stack.pop();
} else {
@@ -764,11 +767,6 @@ impl<'bump> DiffState<'bump> {
self.diff_children(old.children, new.children);
}
// probably will get inlined
fn diff_placeholder_nodes(&mut self, old: &'bump VPlaceholder, new: &'bump VPlaceholder) {
new.dom_id.set(old.dom_id.get())
}
// =============================================
// Utilities for creating new diff instructions
// =============================================
@@ -792,33 +790,8 @@ impl<'bump> DiffState<'bump> {
// Remember, fragments can never be empty (they always have a single child)
match (old, new) {
([], []) => {}
([], _) => {
// we need to push the
self.stack.create_children(new, MountType::Append);
}
(_, []) => {
self.remove_nodes(old, true);
}
// todo: placeholders explicitly replace fragments
//
// ([VNode::Placeholder(old_anchor)], [VNode::Placeholder(new_anchor)]) => {
// old_anchor.dom_id.set(new_anchor.dom_id.get());
// }
// ([VNode::Placeholder(_)], _) => {
// self.stack
// .create_children(new, MountType::Replace { old: &old[0] });
// }
// (_, [VNode::Placeholder(_)]) => {
// let new: &'bump VNode<'bump> = &new[0];
// if let Some(first_old) = old.get(0) {
// self.remove_nodes(&old[1..], true);
// self.stack
// .create_node(new, MountType::Replace { old: first_old });
// } else {
// self.stack.create_node(new, MountType::Append {});
// }
// }
([], _) => self.stack.create_children(new, MountType::Append),
(_, []) => self.remove_nodes(old, true),
_ => {
let new_is_keyed = new[0].key().is_some();
let old_is_keyed = old[0].key().is_some();
@@ -1216,7 +1189,7 @@ impl<'bump> DiffState<'bump> {
search_node = frag.children.last();
}
VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap();
let scope_id = el.scope.get().unwrap();
search_node = Some(self.scopes.root_node(scope_id));
}
}
@@ -1233,7 +1206,7 @@ impl<'bump> DiffState<'bump> {
search_node = Some(&frag.children[0]);
}
VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap();
let scope_id = el.scope.get().unwrap();
search_node = Some(self.scopes.root_node(scope_id));
}
VNode::Text(t) => break t.dom_id.get(),
@@ -1250,8 +1223,6 @@ impl<'bump> DiffState<'bump> {
.try_mounted_id()
.unwrap_or_else(|| panic!("broke on {:?}", old));
log::debug!("element parent is {:?}", el.parent_id.get());
self.mutations.replace_with(id, nodes_created as u32);
self.remove_nodes(el.children, false);
}
@@ -1270,11 +1241,11 @@ impl<'bump> DiffState<'bump> {
}
VNode::Component(c) => {
let node = self.scopes.fin_head(c.associated_scope.get().unwrap());
let node = self.scopes.fin_head(c.scope.get().unwrap());
self.replace_node(node, nodes_created);
let scope_id = c.associated_scope.get().unwrap();
log::debug!("Destroying scope {:?}", scope_id);
let scope_id = c.scope.get().unwrap();
self.scopes.try_remove(scope_id).unwrap();
}
}
@@ -1282,7 +1253,7 @@ impl<'bump> DiffState<'bump> {
/// schedules nodes for garbage collection and pushes "remove" to the mutation stack
/// remove can happen whenever
fn remove_nodes(
pub(crate) fn remove_nodes(
&mut self,
nodes: impl IntoIterator<Item = &'bump VNode<'bump>>,
gen_muts: bool,
@@ -1323,20 +1294,17 @@ impl<'bump> DiffState<'bump> {
}
VNode::Component(c) => {
self.destroy_vomponent(c, gen_muts);
let scope_id = c.scope.get().unwrap();
let root = self.scopes.root_node(scope_id);
self.remove_nodes(Some(root), gen_muts);
log::debug!("Destroying scope {:?}", scope_id);
self.scopes.try_remove(scope_id).unwrap();
}
}
}
}
fn destroy_vomponent(&mut self, vc: &VComponent, gen_muts: bool) {
let scope_id = vc.associated_scope.get().unwrap();
let root = self.scopes.root_node(scope_id);
self.remove_nodes(Some(root), gen_muts);
log::debug!("Destroying scope {:?}", scope_id);
self.scopes.try_remove(scope_id).unwrap();
}
/// Adds a listener closure to a scope during diff.
fn attach_listener_to_scope(&mut self, listener: &'bump Listener<'bump>, scope: &ScopeState) {
let long_listener = unsafe { std::mem::transmute(listener) };
+13 -5
View File
@@ -6,8 +6,7 @@ pub(crate) mod diff;
pub(crate) mod lazynodes;
pub(crate) mod mutations;
pub(crate) mod nodes;
pub(crate) mod scope;
pub(crate) mod scopearena;
pub(crate) mod scopes;
pub(crate) mod virtual_dom;
pub(crate) mod innerlude {
@@ -16,8 +15,8 @@ pub(crate) mod innerlude {
pub use crate::lazynodes::*;
pub use crate::mutations::*;
pub use crate::nodes::*;
pub use crate::scope::*;
pub(crate) use crate::scopearena::*;
pub use crate::scopes::*;
pub(crate) use crate::scopes::*;
pub use crate::virtual_dom::*;
/// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`].
@@ -61,7 +60,7 @@ pub(crate) mod innerlude {
/// // ...
/// };
/// ```
pub type Component<P> = for<'a> fn(Scope<'a, P>) -> Element<'a>;
pub type Component<P> = fn(Scope<P>) -> Element;
}
pub use crate::innerlude::{
@@ -86,3 +85,12 @@ pub mod exports {
pub use bumpalo;
pub use futures_channel;
}
/// Functions that wrap unsafe functionality to prevent us from misusing it at the callsite
pub(crate) mod unsafe_utils {
use crate::VNode;
pub unsafe fn extend_vnode<'a, 'b>(node: &'a VNode<'a>) -> &'b VNode<'b> {
std::mem::transmute(node)
}
}
+5
View File
@@ -45,6 +45,11 @@ pub enum DomEdit<'bump> {
many: u32,
},
// // save a possibly-fragment node as a template
// SaveAsTemplate {
// many: u32,
// },
// "Root" refers to the item directly
// it's a waste of an instruction to push the root directly
ReplaceWith {
+95 -82
View File
@@ -6,6 +6,7 @@
use crate::{
innerlude::{Element, Properties, Scope, ScopeId, ScopeState},
lazynodes::LazyNodes,
Component,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
@@ -84,7 +85,7 @@ pub enum VNode<'src> {
///
/// ```rust, ignore
/// fn Example(cx: Scope<()>) -> Element {
/// todo!()
/// ...
/// }
///
/// let mut vdom = VirtualDom::new();
@@ -186,6 +187,7 @@ impl Debug for VNode<'_> {
write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children)
}
VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc),
// VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc),
}
}
}
@@ -354,26 +356,89 @@ impl Clone for EventHandler<'_> {
/// Only supports the functional syntax
pub struct VComponent<'src> {
pub key: Option<&'src str>,
pub associated_scope: Cell<Option<ScopeId>>,
// Function pointer to the FC that was used to generate this component
pub scope: Cell<Option<ScopeId>>,
pub can_memoize: bool,
pub user_fc: *const (),
pub props: RefCell<Option<Box<dyn AnyProps + 'src>>>,
}
pub(crate) can_memoize: bool,
pub(crate) struct VComponentProps<P> {
// Props start local
// If they are static, then they can get promoted onto the heap
pub render_fn: Component<P>,
pub props: P,
pub memo: unsafe fn(&P, &P) -> bool,
}
pub(crate) _hard_allocation: Cell<Option<*const ()>>,
pub trait AnyProps {
fn as_ptr(&self) -> *const ();
fn release(&self);
fn replace_heap(&self, new: Box<dyn Any>);
// Raw pointer into the bump arena for the props of the component
pub(crate) bump_props: *const (),
// move the props from the bump arena onto the heap
fn promote(&self);
fn render<'a>(&'a self, bump: &'a ScopeState) -> Element<'a>;
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool;
}
// during the "teardown" process we'll take the caller out so it can be dropped properly
pub(crate) caller: &'src dyn Fn(&'src ScopeState) -> Element,
impl<P> AnyProps for VComponentProps<P> {
fn as_ptr(&self) -> *const () {
&self.props as *const _ as *const ()
// // todo: maybe move this into an enum
// let heap_props = self.heap_props.borrow();
// if let Some(b) = heap_props.as_ref() {
// b.as_ref() as *const _ as *const ()
// } else {
// let local_props = self.local_props.borrow();
// local_props.as_ref().unwrap() as *const _ as *const ()
// }
}
pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>,
// this will downcat the other ptr as our swallowed type!
// you *must* make this check *before* calling this method
// if your functions are not the same, then you will downcast a pointer into a different type (UB)
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool {
let real_other: &P = &*(other.as_ptr() as *const _ as *const P);
let real_us: &P = &*(self.as_ptr() as *const _ as *const P);
(self.memo)(real_us, real_other)
}
// todo: re-add this
pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
fn release(&self) {
panic!("don't release props anymore");
// if let Some(heap_props) = self.heap_props.borrow_mut().take() {
// dbg!("releasing heap");
// drop(heap_props)
// } else if let Some(local_props) = self.local_props.borrow_mut().take() {
// dbg!("releasing local");
// drop(local_props)
// } else {
// dbg!("trying to do a double release");
// }
}
fn promote(&self) {
panic!("don't promote props anymore");
// dbg!("promoting props");
// if let Some(props) = self.local_props.borrow_mut().take() {
// let mut heap_slot = self.heap_props.borrow_mut();
// *heap_slot = Some(Box::new(props));
// }
}
fn render<'a>(&'a self, scope: &'a ScopeState) -> Element<'a> {
let props: &P = &self.props;
let inline_props = unsafe { std::mem::transmute::<&P, &P>(props) };
(self.render_fn)(Scope {
scope,
props: inline_props,
})
}
fn replace_heap(&self, new: Box<dyn Any>) {
// let new = new.downcast::<P>().unwrap();
// self.heap_props.borrow_mut().replace(new);
}
}
/// This struct provides an ergonomic API to quickly build VNodes.
@@ -517,75 +582,23 @@ impl<'a> NodeFactory<'a> {
where
P: Properties + 'a,
{
let bump = self.bump;
let props = bump.alloc(props);
let bump_props = props as *mut P as *mut ();
let user_fc = component as *const ();
VNode::Component(self.bump.alloc(VComponent {
key: key.map(|f| self.raw_text(f).0),
scope: Default::default(),
can_memoize: P::IS_STATIC,
user_fc: component as *const (),
props: RefCell::new(Some(Box::new(VComponentProps {
// local_props: RefCell::new(Some(props)),
// heap_props: RefCell::new(None),
props,
memo: P::memoize, // smuggle the memoization function across borders
let comparator: &mut dyn Fn(&VComponent) -> bool = bump.alloc_with(|| {
move |other: &VComponent| {
if user_fc == other.user_fc {
// Safety
// - We guarantee that Component<P> is the same by function pointer
// - Because Component<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
//
// 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
unsafe {
let real_other: &P = &*(other.bump_props as *const _ as *const P);
props.memoize(real_other)
}
} else {
false
}
}
});
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 {
if !has_dropped {
let real_other = bump_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))
};
let key = key.map(|f| self.raw_text(f).0);
let caller: &'a mut dyn Fn(&'a ScopeState) -> Element =
bump.alloc(move |scope: &ScopeState| -> Element {
let props: &'_ P = unsafe { &*(bump_props as *const P) };
component(Scope { scope, props })
});
let can_memoize = P::IS_STATIC;
VNode::Component(bump.alloc(VComponent {
user_fc,
comparator: Some(comparator),
bump_props,
caller,
key,
can_memoize,
drop_props,
associated_scope: Cell::new(None),
_hard_allocation: Cell::new(None),
// i'm sorry but I just need to bludgeon the lifetimes into place here
// this is safe because we're managing all lifetimes to originate from previous calls
// the intricacies of Rust's lifetime system make it difficult to properly express
// the transformation from this specific lifetime to the for<'a> lifetime
render_fn: unsafe { std::mem::transmute(component) },
}))),
}))
}
-355
View File
@@ -1,355 +0,0 @@
use bumpalo::Bump;
use futures_channel::mpsc::UnboundedSender;
use fxhash::{FxHashMap, FxHashSet};
use slab::Slab;
use std::{
borrow::Borrow,
cell::{Cell, RefCell},
};
use crate::innerlude::*;
pub(crate) type FcSlot = *const ();
pub(crate) struct Heuristic {
hook_arena_size: usize,
node_arena_size: usize,
}
// a slab-like arena with stable references even when new scopes are allocated
// uses a bump arena as a backing
//
// has an internal heuristics engine to pre-allocate arenas to the right size
pub(crate) struct ScopeArena {
pub pending_futures: RefCell<FxHashSet<ScopeId>>,
scope_counter: Cell<usize>,
pub(crate) sender: UnboundedSender<SchedulerMsg>,
bump: Bump,
pub scopes: RefCell<FxHashMap<ScopeId, *mut ScopeState>>,
pub heuristics: RefCell<FxHashMap<FcSlot, Heuristic>>,
free_scopes: RefCell<Vec<*mut ScopeState>>,
nodes: RefCell<Slab<*const VNode<'static>>>,
}
impl ScopeArena {
pub(crate) fn new(sender: UnboundedSender<SchedulerMsg>) -> Self {
let bump = Bump::new();
// allocate a container for the root element
// this will *never* show up in the diffing process
let el = bump.alloc(VElement {
tag_name: "root",
namespace: None,
key: None,
dom_id: Cell::new(Some(ElementId(0))),
parent_id: Default::default(),
listeners: &[],
attributes: &[],
children: &[],
});
let node = bump.alloc(VNode::Element(el));
let mut nodes = Slab::new();
let root_id = nodes.insert(unsafe { std::mem::transmute(node as *const _) });
debug_assert_eq!(root_id, 0);
Self {
scope_counter: Cell::new(0),
bump,
pending_futures: RefCell::new(FxHashSet::default()),
scopes: RefCell::new(FxHashMap::default()),
heuristics: RefCell::new(FxHashMap::default()),
free_scopes: RefCell::new(Vec::new()),
nodes: RefCell::new(nodes),
sender,
}
}
/// Safety:
/// - Obtaining a mutable refernece to any Scope is unsafe
/// - Scopes use interior mutability when sharing data into components
pub(crate) fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
unsafe { self.scopes.borrow().get(&id).map(|f| &**f) }
}
pub(crate) fn get_scope_raw(&self, id: ScopeId) -> Option<*mut ScopeState> {
self.scopes.borrow().get(&id).copied()
}
pub(crate) fn new_with_key(
&self,
fc_ptr: *const (),
caller: *const dyn Fn(&ScopeState) -> Element,
parent_scope: Option<*mut ScopeState>,
container: ElementId,
height: u32,
subtree: u32,
) -> ScopeId {
let new_scope_id = ScopeId(self.scope_counter.get());
self.scope_counter.set(self.scope_counter.get() + 1);
/*
This scopearena aggressively reuse old scopes when possible.
We try to minimize the new allocations for props/arenas.
However, this will probably lead to some sort of fragmentation.
I'm not exactly sure how to improve this today.
*/
match self.free_scopes.borrow_mut().pop() {
// No free scope, make a new scope
None => {
let (node_capacity, hook_capacity) = self
.heuristics
.borrow()
.get(&fc_ptr)
.map(|h| (h.node_arena_size, h.hook_arena_size))
.unwrap_or_default();
let frames = [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)];
let scope = self.bump.alloc(ScopeState {
sender: self.sender.clone(),
container,
our_arena_idx: new_scope_id,
parent_scope,
height,
frames,
subtree: Cell::new(subtree),
is_subtree_root: Cell::new(false),
caller: Cell::new(caller),
generation: 0.into(),
shared_contexts: Default::default(),
items: RefCell::new(SelfReferentialItems {
listeners: Default::default(),
borrowed_props: Default::default(),
tasks: Default::default(),
}),
hook_arena: Bump::new(),
hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)),
hook_idx: Default::default(),
});
let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope);
debug_assert!(any_item.is_none());
}
// Reuse a free scope
Some(old_scope) => {
let scope = unsafe { &mut *old_scope };
scope.caller.set(caller);
scope.parent_scope = parent_scope;
scope.height = height;
scope.subtree = Cell::new(subtree);
scope.our_arena_idx = new_scope_id;
scope.container = container;
let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope);
debug_assert!(any_item.is_none());
}
}
new_scope_id
}
// Removes a scope and its descendents from the arena
pub fn try_remove(&self, id: ScopeId) -> Option<()> {
self.ensure_drop_safety(id);
// Safety:
// - ensure_drop_safety ensures that no references to this scope are in use
// - this raw pointer is removed from the map
let scope = unsafe { &mut *self.scopes.borrow_mut().remove(&id).unwrap() };
scope.reset();
self.free_scopes.borrow_mut().push(scope);
Some(())
}
pub fn reserve_node(&self, node: &VNode) -> ElementId {
let mut els = self.nodes.borrow_mut();
let entry = els.vacant_entry();
let key = entry.key();
let id = ElementId(key);
let node: *const VNode = node as *const _;
let node = unsafe { std::mem::transmute::<*const VNode, *const VNode>(node) };
entry.insert(node);
id
}
pub fn update_node(&self, node: &VNode, id: ElementId) {
let node = unsafe { std::mem::transmute::<*const VNode, *const VNode>(node) };
*self.nodes.borrow_mut().get_mut(id.0).unwrap() = node;
}
pub fn collect_garbage(&self, id: ElementId) {
self.nodes.borrow_mut().remove(id.0);
}
/// 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(&self, scope_id: ScopeId) {
if let Some(scope) = self.get_scope(scope_id) {
let mut items = scope.items.borrow_mut();
// 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)
// recursively call ensure_drop_safety on all children
items.borrowed_props.drain(..).for_each(|comp| {
let scope_id = comp
.associated_scope
.get()
.expect("VComponents should be associated with a valid Scope");
self.ensure_drop_safety(scope_id);
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.
items
.listeners
.drain(..)
.for_each(|listener| drop(listener.callback.callback.borrow_mut().take()));
}
}
// pub(crate) fn run_scope(&self, id: ScopeId) -> bool {
pub(crate) fn run_scope(&self, id: ScopeId) {
// 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(id);
// todo: we *know* that this is aliased by the contents of the scope itself
let scope = unsafe { &mut *self.get_scope_raw(id).expect("could not find scope") };
// 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
scope.hook_idx.set(0);
// book keeping to ensure safety around the borrowed data
{
// Safety:
// - We've dropped all references to the wip bump frame with "ensure_drop_safety"
unsafe { scope.reset_wip_frame() };
let mut items = scope.items.borrow_mut();
// just forget about our suspended nodes while we're at it
items.tasks.clear();
// guarantee that we haven't screwed up - there should be no latent references anywhere
debug_assert!(items.listeners.is_empty());
debug_assert!(items.borrowed_props.is_empty());
debug_assert!(items.tasks.is_empty());
}
let render: &dyn Fn(&ScopeState) -> Element = unsafe { &*scope.caller.get() };
/*
If the component returns None, then we fill in a placeholder node. This will wipe what was there.
An alternate approach is to leave the Real Dom the same, but that can lead to safety issues and a lot more checks.
*/
if let Some(node) = render(scope) {
if !scope.items.borrow().tasks.is_empty() {
self.pending_futures.borrow_mut().insert(id);
}
let frame = scope.wip_frame();
let node = frame.bump.alloc(node);
frame.node.set(unsafe { std::mem::transmute(node) });
} else {
let frame = scope.wip_frame();
let node = frame
.bump
.alloc(VNode::Placeholder(frame.bump.alloc(VPlaceholder {
dom_id: Default::default(),
})));
frame.node.set(unsafe { std::mem::transmute(node) });
}
// make the "wip frame" contents the "finished frame"
// any future dipping into completed nodes after "render" will go through "fin head"
scope.cycle_frame();
}
pub fn call_listener_with_bubbling(&self, event: UserEvent, element: ElementId) {
let nodes = self.nodes.borrow();
let mut cur_el = Some(element);
while let Some(id) = cur_el.take() {
if let Some(el) = nodes.get(id.0) {
let real_el = unsafe { &**el };
if let VNode::Element(real_el) = real_el {
//
for listener in real_el.listeners.borrow().iter() {
if listener.event == event.name {
let mut cb = listener.callback.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
(cb)(event.data.clone());
}
}
}
cur_el = real_el.parent_id.get();
}
}
}
}
// The head of the bumpframe is the first linked NodeLink
pub fn wip_head(&self, id: ScopeId) -> &VNode {
let scope = self.get_scope(id).unwrap();
let frame = scope.wip_frame();
let node = unsafe { &*frame.node.get() };
unsafe { std::mem::transmute::<&VNode, &VNode>(node) }
}
// The head of the bumpframe is the first linked NodeLink
pub fn fin_head(&self, id: ScopeId) -> &VNode {
let scope = self.get_scope(id).unwrap();
let frame = scope.fin_frame();
let node = unsafe { &*frame.node.get() };
unsafe { std::mem::transmute::<&VNode, &VNode>(node) }
}
pub fn root_node(&self, id: ScopeId) -> &VNode {
self.fin_head(id)
}
}
// when dropping the virtualdom, we need to make sure and drop everything important
impl Drop for ScopeArena {
fn drop(&mut self) {
for (_, scopeptr) in self.scopes.get_mut().drain() {
let scope = unsafe { bumpalo::boxed::Box::from_raw(scopeptr) };
drop(scope);
}
// these are probably complete invalid unfortunately ?
for scopeptr in self.free_scopes.get_mut().drain(..) {
let scope = unsafe { bumpalo::boxed::Box::from_raw(scopeptr) };
drop(scope);
}
}
}
@@ -1,8 +1,9 @@
use crate::innerlude::*;
use futures_channel::mpsc::UnboundedSender;
use fxhash::{FxHashMap, FxHashSet};
use slab::Slab;
use std::{
any::{Any, TypeId},
borrow::Borrow,
cell::{Cell, RefCell},
collections::HashMap,
future::Future,
@@ -10,8 +11,332 @@ use std::{
rc::Rc,
};
use crate::{innerlude::*, unsafe_utils::extend_vnode};
use bumpalo::{boxed::Box as BumpBox, Bump};
pub(crate) type FcSlot = *const ();
pub(crate) struct Heuristic {
hook_arena_size: usize,
node_arena_size: usize,
}
// a slab-like arena with stable references even when new scopes are allocated
// uses a bump arena as a backing
//
// has an internal heuristics engine to pre-allocate arenas to the right size
pub(crate) struct ScopeArena {
pub pending_futures: RefCell<FxHashSet<ScopeId>>,
pub scope_counter: Cell<usize>,
pub sender: UnboundedSender<SchedulerMsg>,
pub bump: Bump,
pub scopes: RefCell<FxHashMap<ScopeId, *mut ScopeState>>,
pub heuristics: RefCell<FxHashMap<FcSlot, Heuristic>>,
pub free_scopes: RefCell<Vec<*mut ScopeState>>,
pub nodes: RefCell<Slab<*const VNode<'static>>>,
}
impl ScopeArena {
pub(crate) fn new(sender: UnboundedSender<SchedulerMsg>) -> Self {
let bump = Bump::new();
// allocate a container for the root element
// this will *never* show up in the diffing process
// todo: figure out why this is necessary. i forgot. whoops.
let el = bump.alloc(VElement {
tag_name: "root",
namespace: None,
key: None,
dom_id: Cell::new(Some(ElementId(0))),
parent_id: Default::default(),
listeners: &[],
attributes: &[],
children: &[],
});
let node = bump.alloc(VNode::Element(el));
let mut nodes = Slab::new();
let root_id = nodes.insert(unsafe { std::mem::transmute(node as *const _) });
debug_assert_eq!(root_id, 0);
Self {
scope_counter: Cell::new(0),
bump,
pending_futures: RefCell::new(FxHashSet::default()),
scopes: RefCell::new(FxHashMap::default()),
heuristics: RefCell::new(FxHashMap::default()),
free_scopes: RefCell::new(Vec::new()),
nodes: RefCell::new(nodes),
sender,
}
}
/// Safety:
/// - Obtaining a mutable refernece to any Scope is unsafe
/// - Scopes use interior mutability when sharing data into components
pub(crate) fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
unsafe { self.scopes.borrow().get(&id).map(|f| &**f) }
}
pub(crate) fn get_scope_raw(&self, id: ScopeId) -> Option<*mut ScopeState> {
self.scopes.borrow().get(&id).copied()
}
pub(crate) fn new_with_key(
&self,
fc_ptr: *const (),
vcomp: Box<dyn AnyProps>,
parent_scope: Option<ScopeId>,
container: ElementId,
subtree: u32,
) -> ScopeId {
// Increment the ScopeId system. ScopeIDs are never reused
let new_scope_id = ScopeId(self.scope_counter.get());
self.scope_counter.set(self.scope_counter.get() + 1);
// Get the height of the scope
let height = parent_scope
.map(|id| self.get_scope(id).map(|scope| scope.height))
.flatten()
.unwrap_or_default();
let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten();
/*
This scopearena aggressively reuse old scopes when possible.
We try to minimize the new allocations for props/arenas.
However, this will probably lead to some sort of fragmentation.
I'm not exactly sure how to improve this today.
*/
if let Some(old_scope) = self.free_scopes.borrow_mut().pop() {
// reuse the old scope
let scope = unsafe { &mut *old_scope };
scope.props.get_mut().replace(vcomp);
scope.parent_scope = parent_scope;
scope.height = height;
scope.subtree.set(subtree);
scope.our_arena_idx = new_scope_id;
scope.container = container;
let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope);
debug_assert!(any_item.is_none());
} else {
// else create a new scope
let (node_capacity, hook_capacity) = self
.heuristics
.borrow()
.get(&fc_ptr)
.map(|h| (h.node_arena_size, h.hook_arena_size))
.unwrap_or_default();
self.scopes.borrow_mut().insert(
new_scope_id,
self.bump.alloc(ScopeState::new(
height,
container,
new_scope_id,
self.sender.clone(),
parent_scope,
vcomp,
node_capacity,
hook_capacity,
)),
);
}
new_scope_id
}
// Removes a scope and its descendents from the arena
pub fn try_remove(&self, id: ScopeId) -> Option<()> {
self.ensure_drop_safety(id);
// Safety:
// - ensure_drop_safety ensures that no references to this scope are in use
// - this raw pointer is removed from the map
let scope = unsafe { &mut *self.scopes.borrow_mut().remove(&id).unwrap() };
scope.reset();
self.free_scopes.borrow_mut().push(scope);
Some(())
}
pub fn reserve_node<'a>(&self, node: &'a VNode<'a>) -> ElementId {
let mut els = self.nodes.borrow_mut();
let entry = els.vacant_entry();
let key = entry.key();
let id = ElementId(key);
let node = unsafe { extend_vnode(node) };
entry.insert(node as *const _);
id
}
pub fn update_node<'a>(&self, node: &'a VNode<'a>, id: ElementId) {
let node = unsafe { extend_vnode(node) };
*self.nodes.borrow_mut().get_mut(id.0).unwrap() = node;
}
pub fn collect_garbage(&self, id: ElementId) {
self.nodes.borrow_mut().remove(id.0);
}
/// 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(&self, scope_id: ScopeId) {
if let Some(scope) = self.get_scope(scope_id) {
let mut items = scope.items.borrow_mut();
// 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)
// recursively call ensure_drop_safety on all children
items.borrowed_props.drain(..).for_each(|comp| {
let scope_id = comp
.scope
.get()
.expect("VComponents should be associated with a valid Scope");
self.ensure_drop_safety(scope_id);
drop(comp.props.take());
});
// Now that all the references are gone, we can safely drop our own references in our listeners.
items
.listeners
.drain(..)
.for_each(|listener| drop(listener.callback.callback.borrow_mut().take()));
}
}
pub(crate) fn run_scope(&self, id: ScopeId) {
// 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(id);
// todo: we *know* that this is aliased by the contents of the scope itself
let scope = unsafe { &mut *self.get_scope_raw(id).expect("could not find scope") };
// 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
scope.hook_idx.set(0);
// book keeping to ensure safety around the borrowed data
{
// Safety:
// - We've dropped all references to the wip bump frame with "ensure_drop_safety"
unsafe { scope.reset_wip_frame() };
let mut items = scope.items.borrow_mut();
// just forget about our suspended nodes while we're at it
items.tasks.clear();
// guarantee that we haven't screwed up - there should be no latent references anywhere
debug_assert!(items.listeners.is_empty());
debug_assert!(items.borrowed_props.is_empty());
debug_assert!(items.tasks.is_empty());
}
// safety: this is definitely not dropped
/*
If the component returns None, then we fill in a placeholder node. This will wipe what was there.
An alternate approach is to leave the Real Dom the same, but that can lead to safety issues and a lot more checks.
Instead, we just treat the `None` as a shortcut to placeholder.
If the developer wants to prevent a scope from updating, they should control its memoization instead.
Also, the way we implement hooks allows us to cut rendering short before the next hook is recalled.
I'm not sure if React lets you abort the component early, but we let you do that.
*/
let props = scope.props.borrow();
let render = props.as_ref().unwrap();
if let Some(node) = render.render(scope) {
if !scope.items.borrow().tasks.is_empty() {
self.pending_futures.borrow_mut().insert(id);
}
let frame = scope.wip_frame();
let node = frame.bump.alloc(node);
frame.node.set(unsafe { extend_vnode(node) });
} else {
let frame = scope.wip_frame();
let node = frame
.bump
.alloc(VNode::Placeholder(frame.bump.alloc(VPlaceholder {
dom_id: Default::default(),
})));
frame.node.set(unsafe { extend_vnode(node) });
}
// make the "wip frame" contents the "finished frame"
// any future dipping into completed nodes after "render" will go through "fin head"
scope.cycle_frame();
}
pub fn call_listener_with_bubbling(&self, event: UserEvent, element: ElementId) {
let nodes = self.nodes.borrow();
let mut cur_el = Some(element);
while let Some(id) = cur_el.take() {
if let Some(el) = nodes.get(id.0) {
let real_el = unsafe { &**el };
if let VNode::Element(real_el) = real_el {
for listener in real_el.listeners.borrow().iter() {
if listener.event == event.name {
let mut cb = listener.callback.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
// todo: arcs are pretty heavy to clone
// we really want to convert arc to rc
// unfortunately, the SchedulerMsg must be send/sync to be sent across threads
// we could convert arc to rc internally or something
(cb)(event.data.clone());
}
}
}
cur_el = real_el.parent_id.get();
}
}
}
}
// The head of the bumpframe is the first linked NodeLink
pub fn wip_head(&self, id: ScopeId) -> &VNode {
let scope = self.get_scope(id).unwrap();
let frame = scope.wip_frame();
let node = unsafe { &*frame.node.get() };
unsafe { extend_vnode(node) }
}
// The head of the bumpframe is the first linked NodeLink
pub fn fin_head(&self, id: ScopeId) -> &VNode {
let scope = self.get_scope(id).unwrap();
let frame = scope.fin_frame();
let node = unsafe { &*frame.node.get() };
unsafe { extend_vnode(node) }
}
pub fn root_node(&self, id: ScopeId) -> &VNode {
self.fin_head(id)
}
}
/// 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.
@@ -72,34 +397,29 @@ pub struct ScopeId(pub usize);
/// use case they might have.
pub struct ScopeState {
pub(crate) parent_scope: Option<*mut ScopeState>,
pub(crate) container: ElementId,
pub(crate) our_arena_idx: ScopeId,
pub(crate) height: u32,
pub(crate) sender: UnboundedSender<SchedulerMsg>,
// todo: subtrees
pub(crate) is_subtree_root: Cell<bool>,
pub(crate) subtree: Cell<u32>,
pub(crate) is_subtree_root: Cell<bool>,
pub(crate) generation: Cell<u32>,
pub(crate) props: RefCell<Option<Box<dyn AnyProps>>>,
// nodes, items
pub(crate) frames: [BumpFrame; 2],
pub(crate) caller: Cell<*const dyn Fn(&ScopeState) -> Element>,
pub(crate) generation: Cell<u32>,
pub(crate) items: RefCell<SelfReferentialItems<'static>>,
// hooks
pub(crate) hook_arena: Bump,
pub(crate) hook_vals: RefCell<Vec<*mut dyn Any>>,
pub(crate) hook_idx: Cell<usize>,
// shared state -> todo: move this out of scopestate
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
pub(crate) sender: UnboundedSender<SchedulerMsg>,
}
pub struct SelfReferentialItems<'a> {
@@ -110,6 +430,47 @@ pub struct SelfReferentialItems<'a> {
// Public methods exposed to libraries and components
impl ScopeState {
fn new(
height: u32,
container: ElementId,
our_arena_idx: ScopeId,
sender: UnboundedSender<SchedulerMsg>,
parent_scope: Option<*mut ScopeState>,
vcomp: Box<dyn AnyProps>,
node_capacity: usize,
hook_capacity: usize,
) -> Self {
let frames = [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)];
ScopeState {
sender,
container,
our_arena_idx,
parent_scope,
props: RefCell::new(Some(vcomp)),
height,
frames,
// todo: subtrees
subtree: Cell::new(0),
is_subtree_root: Cell::new(false),
generation: 0.into(),
shared_contexts: Default::default(),
items: RefCell::new(SelfReferentialItems {
listeners: Default::default(),
borrowed_props: Default::default(),
tasks: Default::default(),
}),
hook_arena: Bump::new(),
hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)),
hook_idx: Default::default(),
}
}
/// Get the subtree ID that this scope belongs to.
///
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
@@ -143,7 +504,6 @@ impl ScopeState {
///
/// ```rust, ignore
/// fn App(cx: Scope<()>) -> Element {
/// todo!();
/// rsx!(cx, div { "Subtree {id}"})
/// };
/// ```
@@ -251,7 +611,7 @@ impl ScopeState {
/// Get the Root Node of this scope
pub fn root_node(&self) -> &VNode {
let node = unsafe { &*self.wip_frame().node.get() };
let node = unsafe { &*self.fin_frame().node.get() };
unsafe { std::mem::transmute(node) }
}
@@ -465,45 +825,37 @@ impl ScopeState {
&self.wip_frame().bump
}
pub(crate) fn drop_hooks(&mut self) {
self.hook_vals.get_mut().drain(..).for_each(|state| {
let as_mut = unsafe { &mut *state };
let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) };
drop(boxed);
});
}
// todo: disable bookkeeping on drop (unncessary)
pub(crate) fn reset(&mut self) {
// we're just reusing scopes so we need to clear it out
self.drop_hooks();
// first: book keaping
self.hook_idx.set(0);
self.hook_arena.reset();
self.shared_contexts.get_mut().clear();
self.parent_scope = None;
self.generation.set(0);
self.is_subtree_root.set(false);
self.subtree.set(0);
self.frames[0].reset();
self.frames[1].reset();
// next: shared context data
self.shared_contexts.get_mut().clear();
// next: reset the node data
let SelfReferentialItems {
borrowed_props,
listeners,
tasks,
} = self.items.get_mut();
borrowed_props.clear();
listeners.clear();
tasks.clear();
}
}
self.frames[0].reset();
self.frames[1].reset();
impl Drop for ScopeState {
fn drop(&mut self) {
self.drop_hooks();
// Finally, free up the hook values
self.hook_arena.reset();
self.hook_vals.get_mut().drain(..).for_each(|state| {
let as_mut = unsafe { &mut *state };
let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) };
drop(boxed);
});
}
}
+147 -74
View File
@@ -8,7 +8,7 @@ use futures_util::{Future, StreamExt};
use fxhash::FxHashSet;
use indexmap::IndexSet;
use smallvec::SmallVec;
use std::{any::Any, collections::VecDeque, pin::Pin, sync::Arc, task::Poll};
use std::{any::Any, collections::VecDeque, iter::FromIterator, pin::Pin, sync::Arc, task::Poll};
/// A virtual node s ystem that progresses user events and diffs UI trees.
///
@@ -103,10 +103,8 @@ use std::{any::Any, collections::VecDeque, pin::Pin, sync::Arc, task::Poll};
/// }
/// ```
pub struct VirtualDom {
scopes: Box<ScopeArena>,
scopes: ScopeArena,
// should always be ScopeId(0)
base_scope: ScopeId,
pending_messages: VecDeque<SchedulerMsg>,
dirty_scopes: IndexSet<ScopeId>,
@@ -114,8 +112,6 @@ pub struct VirtualDom {
UnboundedSender<SchedulerMsg>,
UnboundedReceiver<SchedulerMsg>,
),
caller: *mut dyn for<'r> Fn(&'r ScopeState) -> Element<'r>,
}
// Methods to create the VirtualDom
@@ -174,7 +170,10 @@ impl VirtualDom {
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild();
/// ```
pub fn new_with_props<P: 'static>(root: Component<P>, root_props: P) -> Self {
pub fn new_with_props<P>(root: Component<P>, root_props: P) -> Self
where
P: 'static,
{
Self::new_with_props_and_scheduler(
root,
root_props,
@@ -188,8 +187,8 @@ impl VirtualDom {
/// VirtualDom to be created just to retrieve its channel receiver.
///
/// ```rust
/// let (sender, receiver) = futures_channel::mpsc::unbounded();
/// let dom = VirtualDom::new_with_scheduler(Example, (), sender, receiver);
/// let channel = futures_channel::mpsc::unbounded();
/// let dom = VirtualDom::new_with_scheduler(Example, (), channel);
/// ```
pub fn new_with_props_and_scheduler<P: 'static>(
root: Component<P>,
@@ -199,36 +198,25 @@ impl VirtualDom {
UnboundedReceiver<SchedulerMsg>,
),
) -> Self {
// move these two things onto the heap so we have stable ptrs even if the VirtualDom itself is moved
let scopes = Box::new(ScopeArena::new(channel.0.clone()));
let root_props = Box::new(root_props);
let scopes = ScopeArena::new(channel.0.clone());
// effectively leak the caller so we can drop it when the VirtualDom is dropped
let caller: *mut dyn for<'r> Fn(&'r ScopeState) -> Element<'r> =
Box::into_raw(Box::new(move |scope: &ScopeState| -> Element {
// Safety: The props at this pointer can never be moved.
// Also, this closure will never be ran when the VirtualDom is destroyed.
// This is where the root lifetime of the VirtualDom originates.
let props = unsafe { std::mem::transmute::<&P, &P>(root_props.as_ref()) };
let scp = Scope { scope, props };
root(scp)
}));
let base_scope = scopes.new_with_key(root as _, caller, None, ElementId(0), 0, 0);
let mut dirty_scopes = IndexSet::new();
dirty_scopes.insert(base_scope);
// todo: add a pending message to the scheduler to start the scheduler?
let pending_messages = VecDeque::new();
scopes.new_with_key(
root as *const _,
Box::new(VComponentProps {
props: root_props,
memo: |_a, _b| unreachable!("memo on root will neve be run"),
render_fn: root,
}),
None,
ElementId(0),
0,
);
Self {
scopes,
base_scope,
caller,
pending_messages,
dirty_scopes,
channel,
dirty_scopes: IndexSet::from_iter([ScopeId(0)]),
pending_messages: VecDeque::new(),
}
}
@@ -237,9 +225,18 @@ impl VirtualDom {
/// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus
/// directly.
///
/// This method is equivalent to calling `get_scope(ScopeId(0))`
///
/// # Example
///
/// ```rust
/// let mut dom = VirtualDom::new(example);
/// dom.rebuild();
///
///
/// ```
pub fn base_scope(&self) -> &ScopeState {
self.get_scope(self.base_scope).unwrap()
self.get_scope(ScopeId(0)).unwrap()
}
/// Get the [`ScopeState`] for a component given its [`ScopeId`]
@@ -248,7 +245,7 @@ impl VirtualDom {
///
///
///
pub fn get_scope<'a>(&'a self, id: ScopeId) -> Option<&'a ScopeState> {
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
self.scopes.get_scope(id)
}
@@ -260,7 +257,7 @@ impl VirtualDom {
/// let dom = VirtualDom::new(App);
/// let sender = dom.get_scheduler_channel();
/// ```
pub fn get_scheduler_channel(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
pub fn get_scheduler_channel(&self) -> UnboundedSender<SchedulerMsg> {
self.channel.0.clone()
}
@@ -273,10 +270,12 @@ impl VirtualDom {
/// # Example
/// ```rust, ignore
/// let dom = VirtualDom::new(App);
/// dom.insert_scheduler_message(SchedulerMsg::Immediate(ScopeId(0)));
/// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
/// ```
pub fn insert_scheduler_message(&self, msg: SchedulerMsg) {
self.channel.0.unbounded_send(msg).unwrap()
pub fn handle_message(&mut self, msg: SchedulerMsg) {
if self.channel.0.unbounded_send(msg).is_ok() {
self.process_all_messages();
}
}
/// Check if the [`VirtualDom`] has any pending updates or work to be done.
@@ -326,26 +325,38 @@ impl VirtualDom {
}
}
while let Ok(Some(msg)) = self.channel.1.try_next() {
self.pending_messages.push_front(msg);
}
// Move all the messages into the queue
self.process_all_messages();
}
}
if let Some(msg) = self.pending_messages.pop_back() {
match msg {
// just keep looping, the task is now saved but we should actually poll it
SchedulerMsg::NewTask(id) => {
self.scopes.pending_futures.borrow_mut().insert(id);
}
SchedulerMsg::UiEvent(event) => {
if let Some(element) = event.element {
self.scopes.call_listener_with_bubbling(event, element);
}
}
SchedulerMsg::Immediate(s) => {
self.dirty_scopes.insert(s);
}
/// Manually kick the VirtualDom to process any
pub fn process_all_messages(&mut self) {
// clear out the scheduler queue
while let Ok(Some(msg)) = self.channel.1.try_next() {
self.pending_messages.push_front(msg);
}
// process all the messages pulled from the queue
while let Some(msg) = self.pending_messages.pop_back() {
self.process_message(msg);
}
}
pub fn process_message(&mut self, msg: SchedulerMsg) {
match msg {
// just keep looping, the task is now saved but we should actually poll it
SchedulerMsg::NewTask(id) => {
self.scopes.pending_futures.borrow_mut().insert(id);
}
SchedulerMsg::UiEvent(event) => {
if let Some(element) = event.element {
self.scopes.call_listener_with_bubbling(event, element);
}
}
SchedulerMsg::Immediate(s) => {
self.dirty_scopes.insert(s);
}
}
}
@@ -466,7 +477,7 @@ impl VirtualDom {
/// apply_edits(edits);
/// ```
pub fn rebuild(&mut self) -> Mutations {
let scope_id = self.base_scope;
let scope_id = ScopeId(0);
let mut diff_state = DiffState::new(&self.scopes);
self.scopes.run_scope(scope_id);
@@ -510,12 +521,24 @@ impl VirtualDom {
///
/// let edits = dom.diff();
/// ```
pub fn hard_diff<'a>(&'a mut self, scope_id: ScopeId) -> Option<Mutations<'a>> {
pub fn hard_diff(&mut self, scope_id: ScopeId) -> Mutations {
let mut diff_machine = DiffState::new(&self.scopes);
self.scopes.run_scope(scope_id);
let (old, new) = (
diff_machine.scopes.wip_head(scope_id),
diff_machine.scopes.fin_head(scope_id),
);
diff_machine.force_diff = true;
diff_machine.diff_scope(scope_id);
Some(diff_machine.mutations)
diff_machine.stack.push(DiffInstruction::Diff { old, new });
diff_machine.stack.scope_stack.push(scope_id);
let scope = diff_machine.scopes.get_scope(scope_id).unwrap();
diff_machine.stack.element_stack.push(scope.container);
diff_machine.work(|| false);
diff_machine.mutations
}
/// Renders an `rsx` call into the Base Scope's allocator.
@@ -531,7 +554,7 @@ impl VirtualDom {
/// let nodes = dom.render_nodes(rsx!("div"));
/// ```
pub fn render_vnodes<'a>(&'a self, lazy_nodes: Option<LazyNodes<'a, '_>>) -> &'a VNode<'a> {
let scope = self.scopes.get_scope(self.base_scope).unwrap();
let scope = self.scopes.get_scope(ScopeId(0)).unwrap();
let frame = scope.wip_frame();
let factory = NodeFactory { bump: &frame.bump };
let node = lazy_nodes.unwrap().call(factory);
@@ -554,7 +577,7 @@ impl VirtualDom {
let mut machine = DiffState::new(&self.scopes);
machine.stack.push(DiffInstruction::Diff { new, old });
machine.stack.element_stack.push(ElementId(0));
machine.stack.scope_stack.push(self.base_scope);
machine.stack.scope_stack.push(ScopeId(0));
machine.work(|| false);
machine.mutations
}
@@ -572,11 +595,12 @@ impl VirtualDom {
/// let dom = VirtualDom::new(Base);
/// let nodes = dom.render_nodes(rsx!("div"));
/// ```
pub fn create_vnodes<'a>(&'a self, left: Option<LazyNodes<'a, '_>>) -> Mutations<'a> {
let nodes = self.render_vnodes(left);
pub fn create_vnodes<'a>(&'a self, nodes: Option<LazyNodes<'a, '_>>) -> Mutations<'a> {
let mut machine = DiffState::new(&self.scopes);
machine.stack.element_stack.push(ElementId(0));
machine.stack.create_node(nodes, MountType::Append);
machine
.stack
.create_node(self.render_vnodes(nodes), MountType::Append);
machine.work(|| false);
machine.mutations
}
@@ -602,29 +626,78 @@ impl VirtualDom {
let (old, new) = (self.render_vnodes(left), self.render_vnodes(right));
let mut create = DiffState::new(&self.scopes);
create.stack.scope_stack.push(self.base_scope);
create.stack.scope_stack.push(ScopeId(0));
create.stack.element_stack.push(ElementId(0));
create.stack.create_node(old, MountType::Append);
create.work(|| false);
let mut edit = DiffState::new(&self.scopes);
edit.stack.scope_stack.push(self.base_scope);
edit.stack.scope_stack.push(ScopeId(0));
edit.stack.element_stack.push(ElementId(0));
edit.stack.push(DiffInstruction::Diff { old, new });
edit.work(&mut || false);
edit.work(|| false);
(create.mutations, edit.mutations)
}
}
/*
Scopes and ScopeArenas are never dropped internally.
An app will always occupy as much memory as its biggest form.
This means we need to handle all specifics of drop *here*. It's easier
to reason about centralizing all the drop logic in one spot rather than scattered in each module.
Broadly speaking, we want to use the remove_nodes method to clean up *everything*
This will drop listeners, borrowed props, and hooks for all components.
We need to do this in the correct order - nodes at the very bottom must be dropped first to release
the borrow chain.
Once the contents of the tree have been cleaned up, we can finally clean up the
memory used by ScopeState itself.
questions:
should we build a vcomponent for the root?
- probably - yes?
- store the vcomponent in the root dom
- 1: Use remove_nodes to use the ensure_drop_safety pathway to safely drop the tree
- 2: Drop the ScopeState itself
*/
impl Drop for VirtualDom {
fn drop(&mut self) {
// ensure the root component's caller is dropped
let caller = unsafe { Box::from_raw(self.caller) };
drop(caller);
// the best way to drop the dom is to replace the root scope with a dud
// the diff infrastructure will then finish the rest
let scope = self.scopes.get_scope(ScopeId(0)).unwrap();
// destroy the root component (which will destroy all of the other scopes)
// load the root node and descend
// todo: move the remove nodes method onto scopearena
// this will clear *all* scopes *except* the root scope
let mut machine = DiffState::new(&self.scopes);
machine.remove_nodes([scope.root_node()], false);
// Now, clean up the root scope
// safety: there are no more references to the root scope
let scope = unsafe { &mut *self.scopes.get_scope_raw(ScopeId(0)).unwrap() };
scope.reset();
// make sure there are no "live" components
for (_, scopeptr) in self.scopes.scopes.get_mut().drain() {
let scope = unsafe { bumpalo::boxed::Box::from_raw(scopeptr) };
drop(scope);
}
for scopeptr in self.scopes.free_scopes.get_mut().drain(..) {
let mut scope = unsafe { bumpalo::boxed::Box::from_raw(scopeptr) };
scope.reset();
drop(scope);
}
// Safety: all refereneces to this caller have been dropped
// the pointer is still valid since we haven't moved it since the virtualdom was created
// ensure the root component's caller is dropped
// drop(unsafe { Box::from_raw(self.caller) });
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ mod test_logging;
fn new_dom() -> VirtualDom {
const IS_LOGGING_ENABLED: bool = false;
test_logging::set_up_logging(IS_LOGGING_ENABLED);
VirtualDom::new(|cx| todo!())
VirtualDom::new(|cx| rsx!(cx, "hi"))
}
use DomEdit::*;
+2 -2
View File
@@ -55,13 +55,13 @@ fn test_early_abort() {
]
);
let edits = dom.hard_diff(ScopeId(0)).unwrap();
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[CreatePlaceholder { root: 3 }, ReplaceWith { root: 1, m: 1 },],
);
let edits = dom.hard_diff(ScopeId(0)).unwrap();
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[
+13 -13
View File
@@ -46,13 +46,13 @@ fn manual_diffing() {
#[test]
fn events_generate() {
static App: Component<()> = |cx| {
let mut count = use_state(&cx, || 0);
let count = cx.use_hook(|_| 0, |f| f);
let inner = match *count {
0 => {
rsx! {
div {
onclick: move |_| count += 1,
onclick: move |_| *count += 1,
div {
"nested"
}
@@ -105,8 +105,8 @@ fn events_generate() {
#[test]
fn components_generate() {
static App: Component<()> = |cx| {
let mut render_phase = use_state(&cx, || 0);
render_phase += 1;
let render_phase = cx.use_hook(|_| 0, |f| f);
*render_phase += 1;
cx.render(match *render_phase {
0 => rsx!("Text0"),
@@ -140,7 +140,7 @@ fn components_generate() {
]
);
let edits = dom.hard_diff(ScopeId(0)).unwrap();
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[
@@ -152,7 +152,7 @@ fn components_generate() {
]
);
let edits = dom.hard_diff(ScopeId(0)).unwrap();
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[
@@ -164,7 +164,7 @@ fn components_generate() {
]
);
let edits = dom.hard_diff(ScopeId(0)).unwrap();
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[
@@ -173,13 +173,13 @@ fn components_generate() {
]
);
let edits = dom.hard_diff(ScopeId(0)).unwrap();
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[CreatePlaceholder { root: 5 }, ReplaceWith { root: 4, m: 1 },]
);
let edits = dom.hard_diff(ScopeId(0)).unwrap();
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[
@@ -191,7 +191,7 @@ fn components_generate() {
]
);
let edits = dom.hard_diff(ScopeId(0)).unwrap();
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[
@@ -207,7 +207,7 @@ fn components_generate() {
]
);
let edits = dom.hard_diff(ScopeId(0)).unwrap();
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[
@@ -222,8 +222,8 @@ fn components_generate() {
fn component_swap() {
// simple_logger::init();
static App: Component<()> = |cx| {
let mut render_phase = use_state(&cx, || 0);
render_phase += 1;
let render_phase = cx.use_hook(|_| 0, |f| f);
*render_phase += 1;
cx.render(match *render_phase {
0 => rsx!(
+173 -11
View File
@@ -9,7 +9,7 @@ Specifically:
- [ ] Async isn't busted
*/
use dioxus::{prelude::*, DomEdit, ScopeId};
use dioxus::{prelude::*, DomEdit, SchedulerMsg, ScopeId};
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
@@ -55,12 +55,6 @@ fn test_memory_leak() {
))
}
fn child(cx: Scope<()>) -> Element {
rsx!(cx, div {
"goodbye world"
})
}
#[derive(Props)]
struct BorrowedProps<'a> {
na: &'a str,
@@ -74,6 +68,10 @@ fn test_memory_leak() {
})
}
fn child(cx: Scope<()>) -> Element {
rsx!(cx, div { "goodbye world" })
}
let mut dom = new_dom(app, ());
dom.rebuild();
@@ -100,7 +98,7 @@ fn memo_works_properly() {
cx.render(rsx!(
div { "Hello, world!" }
child(na: "asd".to_string())
child(na: "asdfg".to_string())
))
}
@@ -110,9 +108,7 @@ fn memo_works_properly() {
}
fn child(cx: Scope<ChildProps>) -> Element {
rsx!(cx, div {
"goodbye world"
})
rsx!(cx, div { "goodbye world" })
}
let mut dom = new_dom(app, ());
@@ -123,4 +119,170 @@ fn memo_works_properly() {
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
dom.hard_diff(ScopeId(0));
}
#[test]
fn free_works_on_root_props() {
fn app(cx: Scope<Custom>) -> Element {
cx.render(rsx! {
child(a: "alpha")
child(a: "beta")
child(a: "gamma")
child(a: "delta")
})
}
#[derive(Props, PartialEq)]
struct ChildProps {
a: &'static str,
}
fn child(cx: Scope<ChildProps>) -> Element {
rsx!(cx, "child {cx.props.a}")
}
struct Custom {
val: String,
}
impl Drop for Custom {
fn drop(&mut self) {
dbg!("dropped!");
}
}
let mut dom = new_dom(
app,
Custom {
val: String::from("asd"),
},
);
dom.rebuild();
}
#[test]
fn free_works_on_borrowed() {
fn app(cx: Scope<()>) -> Element {
cx.render(rsx! {
child(a: "alpha", b: "asd".to_string())
})
}
#[derive(Props)]
struct ChildProps<'a> {
a: &'a str,
b: String,
}
fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
dbg!("rendering child");
rsx!(cx, "child {cx.props.a}, {cx.props.b}")
}
impl Drop for ChildProps<'_> {
fn drop(&mut self) {
dbg!("dropped child!");
}
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
}
#[test]
fn free_works_on_root_hooks() {
/*
On Drop, scopearena drops all the hook contents.
*/
struct Droppable<T>(T);
impl<T> Drop for Droppable<T> {
fn drop(&mut self) {
dbg!("dropping droppable");
}
}
fn app(cx: Scope<()>) -> Element {
let name = cx.use_hook(|_| Droppable(String::from("asd")), |f| f);
rsx!(cx, div { "{name.0}" })
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
}
#[test]
fn old_props_arent_stale() {
fn app(cx: Scope<()>) -> Element {
dbg!("rendering parent");
let cnt = cx.use_hook(|_| 0, |f| f);
*cnt += 1;
if *cnt == 1 {
rsx!(cx, div { child(a: "abcdef".to_string()) })
} else {
rsx!(cx, div { child(a: "abcdef".to_string()) })
}
}
#[derive(Props, PartialEq)]
struct ChildProps {
a: String,
}
fn child(cx: Scope<ChildProps>) -> Element {
dbg!("rendering child", &cx.props.a);
rsx!(cx, div { "child {cx.props.a}" })
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dbg!("forcing update to child");
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
}
#[test]
fn basic() {
fn app(cx: Scope<()>) -> Element {
rsx!(cx, div {
child(a: "abcdef".to_string())
})
}
#[derive(Props, PartialEq)]
struct ChildProps {
a: String,
}
fn child(cx: Scope<ChildProps>) -> Element {
dbg!("rendering child", &cx.props.a);
rsx!(cx, div { "child {cx.props.a}" })
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
}
+1 -1
View File
@@ -244,7 +244,7 @@ impl<'a> TextRenderer<'a, '_> {
}
}
VNode::Component(vcomp) => {
let idx = vcomp.associated_scope.get().unwrap();
let idx = vcomp.scope.get().unwrap();
if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
let new_node = vdom.get_scope(idx).unwrap().root_node();