Feat: major overhaul to diffing

This commit is contained in:
Jonathan Kelley committed 2021-05-15 12:03:08 -04:00
1 parent c809095124
commit 9810feebf5
37 files changed
+1470 -1129

No files matched your search

+9 -3
View File
@@ -1,4 +1,4 @@
use crate::{nodebuilder::IntoDomTree, prelude::*, scope::Scope};
use crate::{innerlude::*, nodebuilder::IntoDomTree};
use crate::{nodebuilder::LazyNodes, nodes::VNode};
use bumpalo::Bump;
use hooks::Hook;
@@ -97,9 +97,12 @@ impl<'a> Context<'a> {
&self,
lazy_nodes: LazyNodes<'a, F>,
) -> DomTree {
// let idx = self.idx.borrow();
let ctx = NodeCtx {
bump: &self.scope.cur_frame().bump,
scope: self.scope.myidx,
// hmmmmmmmm not sure if this is right
idx: 0.into(),
listeners: &self.scope.listeners,
};
@@ -151,11 +154,14 @@ pub mod hooks {
let new_state = initializer();
hooks.push(Hook::new(new_state));
}
*self.idx.borrow_mut() = 1;
*self.idx.borrow_mut() += 1;
let stable_ref = hooks.get_mut(idx).unwrap().0.as_mut();
let v = unsafe { Pin::get_unchecked_mut(stable_ref) };
let internal_state = v.downcast_mut::<InternalHookState>().unwrap();
let internal_state = v
.downcast_mut::<InternalHookState>()
.expect("couldn't find the hook state");
// we extend the lifetime from borrowed in this scope to borrowed from self.
// This is okay because the hook is pinned
+20 -13
View File
@@ -32,7 +32,7 @@
//!
//! 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 crate::{innerlude::*, scope::Scope};
use crate::innerlude::*;
use bumpalo::Bump;
use fxhash::{FxHashMap, FxHashSet};
use generational_arena::Arena;
@@ -66,26 +66,29 @@ pub struct DiffMachine<'a> {
pub enum LifeCycleEvent<'a> {
Mount {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
scope: Weak<VCompAssociatedScope>,
id: u32,
stable_scope_addr: Weak<VCompAssociatedScope>,
root_id: u32,
},
PropsChanged {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
scope: Weak<VCompAssociatedScope>,
id: u32,
stable_scope_addr: Weak<VCompAssociatedScope>,
root_id: u32,
},
SameProps {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
scope: Weak<VCompAssociatedScope>,
id: u32,
stable_scope_addr: Weak<VCompAssociatedScope>,
root_id: u32,
},
Replace {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
old_scope: Weak<VCompAssociatedScope>,
new_scope: Weak<VCompAssociatedScope>,
id: u32,
root_id: u32,
},
Remove {
stable_scope_addr: Weak<VCompAssociatedScope>,
root_id: u32,
},
Remove,
}
static COUNTER: AtomicU32 = AtomicU32::new(1);
@@ -159,7 +162,11 @@ impl<'a> DiffMachine<'a> {
let scope = Rc::downgrade(&cold.ass_scope);
self.lifecycle_events
.push_back(LifeCycleEvent::PropsChanged { caller, id, scope });
.push_back(LifeCycleEvent::PropsChanged {
caller,
root_id: id,
stable_scope_addr: scope,
});
} else {
let caller = Rc::downgrade(&cnew.caller);
let id = cold.stable_addr.borrow().unwrap();
@@ -168,7 +175,7 @@ impl<'a> DiffMachine<'a> {
self.lifecycle_events.push_back(LifeCycleEvent::Replace {
caller,
id,
root_id: id,
old_scope,
new_scope,
});
@@ -262,8 +269,8 @@ impl<'a> DiffMachine<'a> {
let scope = Rc::downgrade(&component.ass_scope);
self.lifecycle_events.push_back(LifeCycleEvent::Mount {
caller: Rc::downgrade(&component.caller),
id,
scope,
root_id: id,
stable_scope_addr: scope,
});
}
VNode::Suspended => {
+4 -4
View File
@@ -21,7 +21,7 @@ mod use_state_def {
struct UseState<T: 'static> {
new_val: Rc<RefCell<Option<T>>>,
current_val: T,
caller: Box<dyn Fn(T) + 'static>,
caller: Rc<dyn Fn(T) + 'static>,
}
/// Store state between component renders!
@@ -49,12 +49,12 @@ mod use_state_def {
pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T>(
ctx: &'c Context<'a>,
initial_state_fn: F,
) -> (&'a T, &'a impl Fn(T)) {
) -> (&'a T, &'a Rc<dyn Fn(T)>) {
ctx.use_hook(
move || UseState {
new_val: Rc::new(RefCell::new(None)),
current_val: initial_state_fn(),
caller: Box::new(|_| println!("setter called!")),
caller: Rc::new(|_| println!("setter called!")),
},
move |hook| {
log::debug!("Use_state set called");
@@ -69,7 +69,7 @@ mod use_state_def {
}
// todo: swap out the caller with a subscription call and an internal update
hook.caller = Box::new(move |new_val| {
hook.caller = Rc::new(move |new_val| {
// update the setter with the new value
let mut new_inner = inner.as_ref().borrow_mut();
*new_inner = Some(new_val);
+13 -35
View File
@@ -68,18 +68,14 @@
pub mod component; // Logic for extending FC
pub mod context; // Logic for providing hook + context functionality to user components
pub mod debug_renderer;
pub mod patch; // An "edit phase" described by transitions and edit operations // Test harness for validating that lifecycles and diffs work appropriately
// pub mod diff;
// pub mod patch; // The diffing algorithm that builds the ChangeList
pub mod diff;
// the diffing algorithm that builds the ChangeList
pub mod patch; // An "edit phase" described by transitions and edit operations // Test harness for validating that lifecycles and diffs work appropriately
// the diffing algorithm that builds the ChangeList
pub mod error; // Error type we expose to the renderers
pub mod events; // Manages the synthetic event API
pub mod hooks; // Built-in hooks
pub mod nodebuilder; // Logic for building VNodes with a direct syntax
pub mod nodes; // Logic for the VNodes
pub mod scope; // Logic for single components
// pub mod validation; // Logic for validating trees
pub mod virtual_dom; // Most fun logic starts here, manages the lifecycle and suspense
pub mod builder {
@@ -88,37 +84,20 @@ pub mod builder {
// types used internally that are important
pub(crate) mod innerlude {
// pub(crate) use crate::component::Properties;
pub(crate) use crate::component::Properties;
pub(crate) use crate::context::Context;
pub use crate::diff::LifeCycleEvent;
pub(crate) use crate::error::Result;
pub use crate::events::{EventTrigger, VirtualEvent};
use crate::nodes;
pub use crate::component::ScopeIdx;
pub use crate::context::hooks::Hook;
pub use crate::nodes::VNode;
pub(crate) use nodes::*;
pub use crate::context::NodeCtx;
pub use crate::diff::DiffMachine;
pub use crate::patch::{EditList, EditMachine};
// pub use crate::patchdx;
// pub use crate::patchtList;
// pub use nodes::iterables::IterableNodes;
/// This type alias is an internal way of abstracting over the static functions that represent components.
pub use crate::component::*;
pub use crate::context::*;
pub use crate::debug_renderer::*;
pub use crate::diff::*;
pub use crate::error::*;
pub use crate::events::*;
pub use crate::hooks::*;
pub use crate::nodebuilder::*;
pub use crate::nodes::*;
pub use crate::patch::*;
pub use crate::virtual_dom::*;
pub type FC<P> = for<'scope> fn(Context<'scope>, &'scope P) -> DomTree;
mod fc2 {}
// pub type FC<'a, P: 'a> = for<'scope> fn(Context<'scope>, &'scope P) -> DomTree;
// pub type FC<P> = for<'scope, 'r> fn(Context<'scope>, &'scope P) -> DomTree;
// pub type FC<P> = for<'scope, 'r> fn(Context<'scope>, &'r P) -> VNode<'scope>;
// pub type FC<P> = for<'scope, 'r> fn(Context<'scope>, &'r P) -> VNode<'scope>;
// pub type FC<P> = for<'a> fn(Context<'a, P>) -> VNode<'a>;
// TODO @Jon, fix this
// hack the VNode type until VirtualNode is fixed in the macro crate
pub type VirtualNode<'a> = VNode<'a>;
@@ -127,7 +106,6 @@ pub(crate) mod innerlude {
pub use crate as dioxus;
pub use crate::nodebuilder as builder;
pub use dioxus_core_macro::{html, rsx};
// pub use dioxus_core_macro::{fc, html, rsx};
}
/// Re-export common types for ease of development use.
+7 -3
View File
@@ -21,9 +21,14 @@
//! ----
//! - stack machine approach does not work when 3rd party extensions inject elements (breaking our view of the dom)
use std::cell::Ref;
use crate::innerlude::ScopeIdx;
pub type EditList<'src> = Vec<Edit<'src>>;
// pub struct EditList<'src> {
// edits: Vec<Edit<'src>>,
// }
/// The `Edit` represents a single modifcation of the renderer tree.
/// todo @jon, go through and make certain fields static. tag names should be known at compile time
@@ -146,7 +151,7 @@ pub struct EditMachine<'lock> {
pub traversal: Traversal,
next_temporary: u32,
forcing_new_listeners: bool,
// // if the current node is a "known" node
// // any actions that modify this node should update the mapping
// current_known: Option<u32>,
@@ -338,7 +343,7 @@ impl<'a> EditMachine<'a> {
}
pub fn create_element(&mut self, tag_name: &'a str) {
debug_assert!(self.traversal_is_committed());;
debug_assert!(self.traversal_is_committed());
self.emitter.push(Edit::CreateElement { tag_name });
}
@@ -389,7 +394,6 @@ impl<'a> EditMachine<'a> {
// self.current_known = Some(id);
self.emitter.push(Edit::TraverseToKnown { node: id })
}
}
// Keeps track of where we are moving in a DOM tree, and shortens traversal
-382
View File
@@ -1,382 +0,0 @@
use crate::{innerlude::*, virtual_dom::OpaqueComponent};
use bumpalo::Bump;
use std::{
cell::RefCell,
collections::HashSet,
marker::PhantomData,
rc::{Rc, Weak},
};
/// Every component in Dioxus is represented by a `Scope`.
///
/// Scopes contain the state for hooks, the component's props, and other lifecycle information.
///
/// Scopes are allocated in a generational arena. As components are mounted/unmounted, they will replace slots of dead components.
/// The actual contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently.
pub struct Scope {
// Map to the parent
pub parent: Option<ScopeIdx>,
// our own index
pub myidx: ScopeIdx,
//
pub children: HashSet<ScopeIdx>,
pub caller: Weak<OpaqueComponent<'static>>,
// ==========================
// slightly unsafe stuff
// ==========================
// an internal, highly efficient storage of vnodes
pub frames: ActiveFrame,
// These hooks are actually references into the hook arena
// These two could be combined with "OwningRef" to remove unsafe usage
// or we could dedicate a tiny bump arena just for them
// could also use ourborous
pub hooks: RefCell<Vec<Hook>>,
pub hook_arena: Vec<Hook>,
// Unsafety:
// - is self-refenrential and therefore needs to point into the bump
// Stores references into the listeners attached to the vnodes
// NEEDS TO BE PRIVATE
pub(crate) listeners: RefCell<Vec<*const dyn Fn(VirtualEvent)>>,
}
impl Scope {
// 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
pub fn new<'creator_node>(
caller: Weak<OpaqueComponent<'creator_node>>,
myidx: ScopeIdx,
parent: Option<ScopeIdx>,
) -> Self {
// Caller has been broken free
// However, it's still weak, so if the original Rc gets killed, we can't touch it
let broken_caller: Weak<OpaqueComponent<'static>> = unsafe { std::mem::transmute(caller) };
Self {
caller: broken_caller,
hook_arena: Vec::new(),
hooks: RefCell::new(Vec::new()),
frames: ActiveFrame::new(),
children: HashSet::new(),
listeners: Default::default(),
parent,
myidx,
}
}
pub fn update_caller<'creator_node>(&mut self, caller: Weak<OpaqueComponent<'creator_node>>) {
let broken_caller: Weak<OpaqueComponent<'static>> = unsafe { std::mem::transmute(caller) };
self.caller = broken_caller;
}
/// Create a new context and run the component with references from the Virtual Dom
/// This function downcasts the function pointer based on the stored props_type
///
/// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized)
pub fn run_scope<'b>(&'b mut self) -> Result<()> {
// cycle to the next frame and then reset it
// this breaks any latent references
self.frames.next().bump.reset();
let ctx = Context {
idx: 0.into(),
_p: PhantomData {},
scope: self,
};
let caller = self.caller.upgrade().expect("Failed to get caller");
/*
SAFETY ALERT
DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS.
KEEPING THIS STATIC REFERENCE CAN LEAD TO UB.
Some things to note:
- The VNode itself is bound to the lifetime, but it itself is owned by scope.
- The VNode has a private API and can only be used from accessors.
- Public API cannot drop or destructure VNode
*/
let new_head = unsafe {
// use the same type, just manipulate the lifetime
type ComComp<'c> = Rc<OpaqueComponent<'c>>;
let caller = std::mem::transmute::<ComComp<'static>, ComComp<'b>>(caller);
(caller.as_ref())(ctx)
};
self.frames.cur_frame_mut().head_node = new_head.root;
Ok(())
}
// A safe wrapper around calling listeners
// calling listeners will invalidate the list of listeners
// The listener list will be completely drained because the next frame will write over previous listeners
pub fn call_listener(&mut self, trigger: EventTrigger) {
let EventTrigger {
listener_id,
event: source,
..
} = trigger;
unsafe {
let listener = self
.listeners
.borrow()
.get(listener_id as usize)
.expect("Listener should exist if it was triggered")
.as_ref()
.unwrap();
// Run the callback with the user event
log::debug!("Running listener");
listener(source);
log::debug!("Listener finished");
// drain all the event listeners
// if we don't, then they'll stick around and become invalid
// big big big big safety issue
self.listeners.borrow_mut().drain(..);
}
}
pub fn new_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
self.frames.current_head_node()
}
pub fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
self.frames.prev_head_node()
}
pub fn cur_frame(&self) -> &BumpFrame {
self.frames.cur_frame()
}
}
// ==========================
// Active-frame related code
// ==========================
// todo, do better with the active frame stuff
// somehow build this vnode with a lifetime tied to self
// This root node has "static" lifetime, but it's really not static.
// It's goverened by the oldest of the two frames and is switched every time a new render occurs
// Use this node as if it were static is unsafe, and needs to be fixed with ourborous or owning ref
// ! do not copy this reference are things WILL break !
pub struct ActiveFrame {
pub idx: RefCell<usize>,
pub frames: [BumpFrame; 2],
}
pub struct BumpFrame {
pub bump: Bump,
pub head_node: VNode<'static>,
}
impl ActiveFrame {
pub fn new() -> Self {
Self::from_frames(
BumpFrame {
bump: Bump::new(),
head_node: VNode::text(""),
},
BumpFrame {
bump: Bump::new(),
head_node: VNode::text(""),
},
)
}
fn from_frames(a: BumpFrame, b: BumpFrame) -> Self {
Self {
idx: 0.into(),
frames: [a, b],
}
}
fn cur_frame(&self) -> &BumpFrame {
match *self.idx.borrow() & 1 == 0 {
true => &self.frames[0],
false => &self.frames[1],
}
}
fn cur_frame_mut(&mut self) -> &mut BumpFrame {
match *self.idx.borrow() & 1 == 0 {
true => &mut self.frames[0],
false => &mut self.frames[1],
}
}
pub fn current_head_node<'b>(&'b self) -> &'b VNode<'b> {
let raw_node = match *self.idx.borrow() & 1 == 0 {
true => &self.frames[0],
false => &self.frames[1],
};
// Give out our self-referential item with our own borrowed lifetime
unsafe {
let unsafe_head = &raw_node.head_node;
let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head);
safe_node
}
}
pub fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> {
let raw_node = match *self.idx.borrow() & 1 != 0 {
true => &self.frames[0],
false => &self.frames[1],
};
// Give out our self-referential item with our own borrowed lifetime
unsafe {
let unsafe_head = &raw_node.head_node;
let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head);
safe_node
}
}
fn next(&mut self) -> &mut BumpFrame {
*self.idx.borrow_mut() += 1;
if *self.idx.borrow() % 2 == 0 {
&mut self.frames[0]
} else {
&mut self.frames[1]
}
}
}
#[cfg(old)]
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
// static ListenerTest: FC<()> = |ctx, props| {
// ctx.render(html! {
// <div onclick={|_| println!("Hell owlrld")}>
// "hello"
// </div>
// })
// };
#[test]
fn test_scope() {
#[derive(PartialEq)]
struct Example {}
impl FC for Example {
fn render(ctx: Context<'_>, _: &Self) -> DomTree {
use crate::builder::*;
ctx.render(|ctx| {
builder::ElementBuilder::new(ctx, "div")
.child(text("a"))
.finish()
})
}
}
let mut nodes = generational_arena::Arena::new();
nodes.insert_with(|myidx| {
let scope = create_scoped(Example {}, myidx, None);
});
}
use crate::{builder::*, hooks::use_ref};
#[derive(Debug, PartialEq)]
struct EmptyProps<'src> {
name: &'src String,
}
impl FC for EmptyProps<'_> {
fn render(ctx: Context, props: &Self) -> DomTree {
let (content, _): (&String, _) = crate::hooks::use_state(&ctx, || "abcd".to_string());
let childprops: ExampleProps<'_> = ExampleProps { name: content };
todo!()
// ctx.render(move |c| {
// builder::ElementBuilder::new(c, "div")
// .child(text(props.name))
// .child(virtual_child(c, childprops))
// .finish()
// })
}
}
#[derive(Debug, PartialEq)]
struct ExampleProps<'src> {
name: &'src String,
}
impl FC for ExampleProps<'_> {
fn render(ctx: Context, props: &Self) -> DomTree {
ctx.render(move |ctx| {
builder::ElementBuilder::new(ctx, "div")
.child(text(props.name))
.finish()
})
}
}
#[test]
fn test_borrowed_scope() {
#[derive(Debug, PartialEq)]
struct Example {
name: String,
}
impl FC for Example {
fn render(ctx: Context, props: &Self) -> DomTree {
todo!()
// ctx.render(move |c| {
// builder::ElementBuilder::new(c, "div")
// .child(virtual_child(c, ExampleProps { name: &props.name }))
// .finish()
// })
}
}
let source_text = "abcd123".to_string();
let props = ExampleProps { name: &source_text };
}
}
#[cfg(asd)]
mod old {
/// The ComponentCaller struct is an opaque object that encapsultes the memoization and running functionality for FC
///
/// It's opaque because during the diffing mechanism, the type of props is sealed away in a closure. This makes it so
/// scope doesn't need to be generic
pub struct ComponentCaller {
// used as a memoization strategy
comparator: Box<dyn Fn(&Box<dyn Any>) -> bool>,
// used to actually run the component
// encapsulates props
runner: Box<dyn Fn(Context) -> DomTree>,
props_type: TypeId,
// the actual FC<T>
raw: *const (),
}
impl ComponentCaller {
fn new<P>(props: P) -> Self {
let comparator = Box::new(|f| false);
todo!();
// Self { comparator }
}
fn update_props<P>(props: P) {}
}
}
+546 -149
View File
@@ -1,13 +1,12 @@
// use crate::{changelist::EditList, nodes::VNode};
use crate::{error::Error, innerlude::*};
use crate::{patch::Edit, scope::Scope};
use crate::{innerlude::hooks::Hook, patch::Edit};
use bumpalo::Bump;
use generational_arena::Arena;
use std::{
any::TypeId,
borrow::{Borrow, BorrowMut},
cell::RefCell,
collections::{BTreeMap, BTreeSet},
cell::{Ref, RefCell, UnsafeCell},
collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet},
rc::{Rc, Weak},
};
use thiserror::private::AsDynError;
@@ -21,23 +20,30 @@ pub(crate) type OpaqueComponent<'a> = dyn for<'b> Fn(Context<'b>) -> DomTree + '
pub struct VirtualDom {
/// 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.
pub components: Arena<Scope>,
///
/// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
/// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
components: UnsafeCell<Arena<Scope>>,
/// The index of the root component.
/// Will not be ready if the dom is fresh
/// The index of the root component.\
/// Should always be the first
pub base_scope: ScopeIdx,
/// All components dump their updates into a queue to be processed
pub(crate) update_schedule: UpdateFunnel,
// a strong allocation to the "caller" for the original props
#[doc(hidden)]
_root_caller: Rc<OpaqueComponent<'static>>,
root_caller: Rc<OpaqueComponent<'static>>,
// Type of the original props. This is done so VirtualDom does not need to be generic.
#[doc(hidden)]
_root_prop_type: std::any::TypeId,
}
// ======================================
// Public Methods for the VirtualDOM
// ======================================
impl VirtualDom {
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
///
@@ -55,109 +61,50 @@ impl VirtualDom {
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
let mut components = Arena::new();
// let prr = Rc::new(root_props);
// The user passes in a "root" component (IE the function)
// When components are used in the rsx! syntax, the parent assumes ownership
// Here, the virtual dom needs to own the function, wrapping it with a `context caller`
// The RC holds this component with a hard allocation
let root_caller: Rc<OpaqueComponent> = Rc::new(move |ctx| root(ctx, &root_props));
// the root is kept around with a "hard" allocation
let root_caller: Rc<OpaqueComponent> = Rc::new(move |ctx| {
//
// let p2 = &root_props;
// let p2 = prr.clone();
root(ctx, &root_props)
});
// we then expose this to the component with a weak allocation
let weak_caller: Weak<OpaqueComponent> = Rc::downgrade(&root_caller);
let base_scope = components.insert_with(move |myidx| Scope::new(weak_caller, myidx, None));
// To make it easier to pass the root around, we just leak it
// When the virtualdom is dropped, we unleak it, so that unsafe isn't here, but it's important to remember
let leaked_caller = Rc::downgrade(&root_caller);
Self {
components,
_root_caller: root_caller,
base_scope,
root_caller,
base_scope: components
.insert_with(move |myidx| Scope::new(leaked_caller, myidx, None, 0)),
components: UnsafeCell::new(components),
update_schedule: UpdateFunnel::default(),
_root_prop_type: TypeId::of::<P>(),
}
}
// consume the top of the diff machine event cycle and dump edits into the edit list
pub fn step(&mut self, event: LifeCycleEvent) -> Result<()> {
Ok(())
}
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom.
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom. from scratch
pub fn rebuild<'s>(&'s mut self) -> Result<EditList<'s>> {
// Diff from the top
let mut diff_machine = DiffMachine::new();
let very_unsafe_components = &mut self.components as *mut generational_arena::Arena<Scope>;
let component = self
.components
.get_mut(self.base_scope)
.ok_or_else(|| Error::FatalInternal("Acquring base component should never fail"))?;
component.run_scope()?;
diff_machine.diff_node(component.old_frame(), component.new_frame());
// log::debug!("New events generated: {:#?}", diff_machine.lifecycle_events);
// chew down the the lifecycle events until all dirty nodes are computed
while let Some(event) = diff_machine.lifecycle_events.pop_front() {
match event {
// A new component has been computed from the diffing algorithm
// create a new component in the arena, run it, move the diffing machine to this new spot, and then diff it
// this will flood the lifecycle queue with new updates
LifeCycleEvent::Mount { caller, id, scope } => {
log::debug!("Mounting a new component");
// We're modifying the component arena while holding onto references into the assoiated bump arenas of its children
// those references are stable, even if the component arena moves around in memory, thanks to the bump arenas.
// However, there is no way to convey this to rust, so we need to use unsafe to pierce through the lifetime.
unsafe {
let p = &mut *(very_unsafe_components);
// todo, hook up the parent/child indexes properly
let idx = p.insert_with(|f| Scope::new(caller, f, None));
let c = p.get_mut(idx).unwrap();
let real_scope = scope.upgrade().unwrap();
*real_scope.as_ref().borrow_mut() = Some(idx);
c.run_scope()?;
diff_machine.change_list.load_known_root(id);
diff_machine.diff_node(c.old_frame(), c.new_frame());
}
}
LifeCycleEvent::PropsChanged { caller, id, scope } => {
log::debug!("PROPS CHANGED");
let idx = scope.upgrade().unwrap().as_ref().borrow().unwrap();
unsafe {
let p = &mut *(very_unsafe_components);
let c = p.get_mut(idx).unwrap();
c.update_caller(caller);
c.run_scope()?;
diff_machine.change_list.load_known_root(id);
diff_machine.diff_node(c.old_frame(), c.new_frame());
}
// break 'render;
}
LifeCycleEvent::SameProps { caller, id, scope } => {
log::debug!("SAME PROPS RECEIVED");
//
// break 'render;
}
LifeCycleEvent::Remove => {
//
// break 'render;
}
LifeCycleEvent::Replace { caller, id, .. } => {}
}
// } else {
// break 'render;
// }
}
let edits: Vec<Edit<'s>> = diff_machine.consume();
Ok(edits)
todo!()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct HeightMarker {
idx: ScopeIdx,
height: u32,
}
impl Ord for HeightMarker {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.height.cmp(&other.height)
}
}
impl PartialOrd for HeightMarker {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl VirtualDom {
/// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered.
///
/// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual
@@ -165,7 +112,16 @@ impl VirtualDom {
/// change list.
///
/// If implementing an external renderer, this is the perfect method to combine with an async event loop that waits on
/// listeners.
/// listeners, something like this:
///
/// ```ignore
/// while let Ok(event) = receiver.recv().await {
/// let edits = self.internal_dom.progress_with_event(event)?;
/// for edit in &edits {
/// patch_machine.handle_edit(edit);
/// }
/// }
/// ```
///
/// Note: this method is not async and does not provide suspense-like functionality. It is up to the renderer to provide the
/// executor and handlers for suspense as show in the example.
@@ -182,67 +138,508 @@ impl VirtualDom {
/// }
///
/// ```
pub fn progress_with_event(&mut self, event: EventTrigger) -> Result<EditList<'_>> {
let component = self
.components
.get_mut(event.component_id)
.expect("Borrowing should not fail");
//
// Developer notes:
// ----
// This method has some pretty complex safety guarantees to uphold.
// We interact with bump arenas, raw pointers, and use UnsafeCell to get a partial borrow of the arena.
// The final EditList has edits that pull directly from the Bump Arenas which add significant complexity
// in crafting a 100% safe solution with traditional lifetimes. Consider this method to be internally unsafe
// but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework.
pub fn progress_with_event(&mut self, event: EventTrigger) -> Result<EditList> {
let id = event.component_id.clone();
component.call_listener(event);
unsafe {
(&mut *self.components.get())
.get_mut(id)
.expect("Borrowing should not fail")
.call_listener(event)?;
}
/*
-> call listener
-> sort through accumulated queue by the top
-> run each component, running its children
-> add new updates to the tree of updates (these are dirty)
->
*/
// component.run_scope()?;
// Add this component to the list of components that need to be difed
let mut diff_machine = DiffMachine::new();
let mut cur_height = 0;
// let mut diff_machine = DiffMachine::new();
// let mut diff_machine = DiffMachine::new(&self.diff_bump);
// Now, there are events in the queue
let mut seen_nodes = HashSet::<ScopeIdx>::new();
let mut updates = self.update_schedule.0.as_ref().borrow_mut();
// diff_machine.diff_node(component.old_frame(), component.new_frame());
// Order the nodes by their height, we want the biggest nodes on the top
// This prevents us from running the same component multiple times
updates.sort_unstable();
// Ok(diff_machine.consume())
self.rebuild()
// Iterate through the triggered nodes (sorted by height) and begin to diff them
for update in updates.drain(..) {
// Make sure this isn't a node we've already seen, we don't want to double-render anything
// If we double-renderer something, this would cause memory safety issues
if seen_nodes.contains(&update.idx) {
continue;
}
// Now, all the "seen nodes" are nodes that got notified by running this listener
seen_nodes.insert(update.idx.clone());
unsafe {
// Start a new mutable borrow to components
// We are guaranteeed that this scope is unique because we are tracking which nodes have modified
let component = (&mut *self.components.get())
.get_mut(update.idx)
.expect("msg");
component.run_scope()?;
diff_machine.diff_node(component.old_frame(), component.next_frame());
cur_height = component.height + 1;
}
// Now, the entire subtree has been invalidated. We need to descend depth-first and process
// any updates that the diff machine has proprogated into the component lifecycle queue
while let Some(event) = diff_machine.lifecycle_events.pop_front() {
match event {
// A new component has been computed from the diffing algorithm
// create a new component in the arena, run it, move the diffing machine to this new spot, and then diff it
// this will flood the lifecycle queue with new updates to build up the subtree
LifeCycleEvent::Mount {
caller,
root_id: id,
stable_scope_addr,
} => {
log::debug!("Mounting a new component");
// We're modifying the component arena while holding onto references into the assoiated bump arenas of its children
// those references are stable, even if the component arena moves around in memory, thanks to the bump arenas.
// However, there is no way to convey this to rust, so we need to use unsafe to pierce through the lifetime.
let components: &mut _ = unsafe { &mut *self.components.get() };
// Insert a new scope into our component list
let idx =
components.insert_with(|f| Scope::new(caller, f, None, cur_height));
// Grab out that component
let component = components.get_mut(idx).unwrap();
// Actually initialize the caller's slot with the right address
*stable_scope_addr.upgrade().unwrap().as_ref().borrow_mut() = Some(idx);
// Run the scope for one iteration to initialize it
component.run_scope()?;
// Navigate the diff machine to the right point in the output dom
diff_machine.change_list.load_known_root(id);
// And then run the diff algorithm
diff_machine.diff_node(component.old_frame(), component.next_frame());
// Finally, insert this node as a seen node.
seen_nodes.insert(idx);
}
// A component has remained in the same location but its properties have changed
// We need to process this component and then dump the output lifecycle events into the queue
LifeCycleEvent::PropsChanged {
caller,
root_id,
stable_scope_addr,
} => {
log::debug!("Updating a component after its props have changed");
let components: &mut _ = unsafe { &mut *self.components.get() };
// Get the stable index to the target component
// This *should* exist due to guarantees in the diff algorithm
let idx = stable_scope_addr
.upgrade()
.unwrap()
.as_ref()
.borrow()
.unwrap();
// Grab out that component
let component = components.get_mut(idx).unwrap();
// We have to move the caller over or running the scope will fail
component.update_caller(caller);
// Run the scope
component.run_scope()?;
// Navigate the diff machine to the right point in the output dom
diff_machine.change_list.load_known_root(root_id);
// And then run the diff algorithm
diff_machine.diff_node(component.old_frame(), component.next_frame());
// Finally, insert this node as a seen node.
seen_nodes.insert(idx);
}
// A component's parent has updated, but its properties did not change.
// This means the caller ptr is invalidated and needs to be updated, but the component itself does not need to be re-ran
LifeCycleEvent::SameProps {
caller,
root_id,
stable_scope_addr,
} => {
// In this case, the parent made a new DomTree that resulted in the same props for us
// However, since our caller is located in a Bump frame, we need to update the caller pointer (which is now invalid)
log::debug!("Received the same props");
let components: &mut _ = unsafe { &mut *self.components.get() };
// Get the stable index to the target component
// This *should* exist due to guarantees in the diff algorithm
let idx = stable_scope_addr
.upgrade()
.unwrap()
.as_ref()
.borrow()
.unwrap();
// Grab out that component
let component = components.get_mut(idx).unwrap();
// We have to move the caller over or running the scope will fail
component.update_caller(caller);
// This time, we will not add it to our seen nodes since we did not actually run it
}
LifeCycleEvent::Remove {
root_id,
stable_scope_addr,
} => {
unimplemented!("This feature (Remove) is unimplemented")
}
LifeCycleEvent::Replace {
caller,
root_id: id,
..
} => {
unimplemented!("This feature (Replace) is unimplemented")
}
}
}
}
Ok(diff_machine.consume())
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HierarchyMarker {
source: ScopeIdx,
}
impl Drop for VirtualDom {
fn drop(&mut self) {
// Drop all the components first
// self.components.drain();
#[derive(Debug, Default, PartialEq, Clone)]
pub struct UpdateFunnel(Rc<RefCell<Vec<HierarchyMarker>>>);
// Finally, drop the root caller
unsafe {
// let root: Box<OpaqueComponent<'static>> =
// Box::from_raw(self.root_caller as *const OpaqueComponent<'static> as *mut _);
impl UpdateFunnel {
fn schedule_update(&self, source: ScopeIdx) -> impl Fn() {
let inner = self.clone();
move || {
inner
.0
.as_ref()
.borrow_mut()
.push(HierarchyMarker { source })
// std::mem::drop(root);
}
}
}
macro_rules! UpdateFunnel {
(root: $root:expr) => {
VirtualDom::new($root)
};
#[derive(Debug, Default, Clone)]
pub struct UpdateFunnel(Rc<RefCell<Vec<HeightMarker>>>);
impl UpdateFunnel {
fn schedule_update(&self, source: &Scope) -> impl Fn() {
let inner = self.clone();
let marker = HeightMarker {
height: source.height,
idx: source.myidx,
};
move || inner.0.as_ref().borrow_mut().push(marker)
}
}
/// Every component in Dioxus is represented by a `Scope`.
///
/// Scopes contain the state for hooks, the component's props, and other lifecycle information.
///
/// Scopes are allocated in a generational arena. As components are mounted/unmounted, they will replace slots of dead components.
/// The actual contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently.
pub struct Scope {
// The parent's scope ID
pub parent: Option<ScopeIdx>,
// Our own ID accessible from the component map
pub myidx: ScopeIdx,
pub height: u32,
// A list of children
// TODO, repalce the hashset with a faster hash function
pub children: HashSet<ScopeIdx>,
// caller: &'static OpaqueComponent<'static>,
pub caller: Weak<OpaqueComponent<'static>>,
// ==========================
// slightly unsafe stuff
// ==========================
// an internal, highly efficient storage of vnodes
pub(crate) frames: ActiveFrame,
// These hooks are actually references into the hook arena
// These two could be combined with "OwningRef" to remove unsafe usage
// or we could dedicate a tiny bump arena just for them
// could also use ourborous
pub(crate) hooks: RefCell<Vec<Hook>>,
// Unsafety:
// - is self-refenrential and therefore needs to point into the bump
// Stores references into the listeners attached to the vnodes
// NEEDS TO BE PRIVATE
pub(crate) listeners: RefCell<Vec<*const dyn Fn(VirtualEvent)>>,
}
// #[test]
// fn test_new_vdom() {
// let dom = UpdateFunnel! {
// root: |ctx, props| {
// ctx.render(rsx!{
impl Scope {
// 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
fn new<'creator_node>(
caller: Weak<OpaqueComponent<'creator_node>>,
myidx: ScopeIdx,
parent: Option<ScopeIdx>,
height: u32,
) -> Self {
log::debug!(
"New scope created, height is {}, idx is {:?}",
height,
myidx
);
// Caller has been broken free
// However, it's still weak, so if the original Rc gets killed, we can't touch it
let broken_caller: Weak<OpaqueComponent<'static>> = unsafe { std::mem::transmute(caller) };
// })
// }
// };
// }
Self {
caller: broken_caller,
hooks: RefCell::new(Vec::new()),
frames: ActiveFrame::new(),
children: HashSet::new(),
listeners: Default::default(),
parent,
myidx,
height,
}
}
pub fn update_caller<'creator_node>(&mut self, caller: Weak<OpaqueComponent<'creator_node>>) {
let broken_caller: Weak<OpaqueComponent<'static>> = unsafe { std::mem::transmute(caller) };
self.caller = broken_caller;
}
/// Create a new context and run the component with references from the Virtual Dom
/// This function downcasts the function pointer based on the stored props_type
///
/// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized)
pub fn run_scope<'b>(&'b mut self) -> Result<()> {
// cycle to the next frame and then reset it
// this breaks any latent references
self.frames.next().bump.reset();
let ctx = Context {
idx: 0.into(),
_p: std::marker::PhantomData {},
scope: self,
};
let caller = self.caller.upgrade().expect("Failed to get caller");
/*
SAFETY ALERT
DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS.
KEEPING THIS STATIC REFERENCE CAN LEAD TO UB.
Some things to note:
- The VNode itself is bound to the lifetime, but it itself is owned by scope.
- The VNode has a private API and can only be used from accessors.
- Public API cannot drop or destructure VNode
*/
let new_head = unsafe {
// use the same type, just manipulate the lifetime
type ComComp<'c> = Rc<OpaqueComponent<'c>>;
let caller = std::mem::transmute::<ComComp<'static>, ComComp<'b>>(caller);
(caller.as_ref())(ctx)
};
self.frames.cur_frame_mut().head_node = new_head.root;
Ok(())
}
// A safe wrapper around calling listeners
// calling listeners will invalidate the list of listeners
// The listener list will be completely drained because the next frame will write over previous listeners
pub fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> {
let EventTrigger {
listener_id,
event: source,
..
} = trigger;
unsafe {
// Convert the raw ptr into an actual object
// This operation is assumed to be safe
log::debug!("Running listener");
self.listeners
.borrow()
.get(listener_id as usize)
.ok_or(Error::FatalInternal("Event should exist if it was triggered"))?
.as_ref()
.ok_or(Error::FatalInternal("Raw event ptr is invalid"))?
// Run the callback with the user event
(source);
log::debug!("Listener finished");
// drain all the event listeners
// if we don't, then they'll stick around and become invalid
// big big big big safety issue
self.listeners.borrow_mut().drain(..);
}
Ok(())
}
pub fn next_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
self.frames.current_head_node()
}
pub fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
self.frames.prev_head_node()
}
pub fn cur_frame(&self) -> &BumpFrame {
self.frames.cur_frame()
}
}
// ==========================
// Active-frame related code
// ==========================
// todo, do better with the active frame stuff
// somehow build this vnode with a lifetime tied to self
// This root node has "static" lifetime, but it's really not static.
// It's goverened by the oldest of the two frames and is switched every time a new render occurs
// Use this node as if it were static is unsafe, and needs to be fixed with ourborous or owning ref
// ! do not copy this reference are things WILL break !
pub struct ActiveFrame {
pub idx: RefCell<usize>,
pub frames: [BumpFrame; 2],
// We use a "generation" for users of contents in the bump frames to ensure their data isn't broken
pub generation: u32,
}
pub struct BumpFrame {
pub bump: Bump,
pub head_node: VNode<'static>,
}
impl ActiveFrame {
pub fn new() -> Self {
Self::from_frames(
BumpFrame {
bump: Bump::new(),
head_node: VNode::text(""),
},
BumpFrame {
bump: Bump::new(),
head_node: VNode::text(""),
},
)
}
fn from_frames(a: BumpFrame, b: BumpFrame) -> Self {
Self {
idx: 0.into(),
frames: [a, b],
generation: 0,
}
}
fn cur_frame(&self) -> &BumpFrame {
match *self.idx.borrow() & 1 == 0 {
true => &self.frames[0],
false => &self.frames[1],
}
}
fn cur_frame_mut(&mut self) -> &mut BumpFrame {
match *self.idx.borrow() & 1 == 0 {
true => &mut self.frames[0],
false => &mut self.frames[1],
}
}
pub fn current_head_node<'b>(&'b self) -> &'b VNode<'b> {
let raw_node = match *self.idx.borrow() & 1 == 0 {
true => &self.frames[0],
false => &self.frames[1],
};
// Give out our self-referential item with our own borrowed lifetime
unsafe {
let unsafe_head = &raw_node.head_node;
let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head);
safe_node
}
}
pub fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> {
let raw_node = match *self.idx.borrow() & 1 != 0 {
true => &self.frames[0],
false => &self.frames[1],
};
// Give out our self-referential item with our own borrowed lifetime
unsafe {
let unsafe_head = &raw_node.head_node;
let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head);
safe_node
}
}
fn next(&mut self) -> &mut BumpFrame {
*self.idx.borrow_mut() += 1;
if *self.idx.borrow() % 2 == 0 {
&mut self.frames[0]
} else {
&mut self.frames[1]
}
}
}
mod test {
use super::*;
#[test]
fn simulate() {
let dom = VirtualDom::new(|ctx, props| {
//
ctx.render(rsx! {
div {
}
})
});
// let root = dom.components.get(dom.base_scope).unwrap();
}
// // This marker is designed to ensure resources shared from one bump to another are handled properly
// // The underlying T may be already freed if there is an issue with our crate
// pub(crate) struct BumpResource<T: 'static> {
// resource: T,
// scope: ScopeIdx,
// gen: u32,
// }
}