wip: move to slab

This commit is contained in:
Jonathan Kelley 2021-07-23 17:03:51 -04:00
parent f644d7c441
commit 6084fbcd11
7 changed files with 159 additions and 170 deletions

View file

@ -31,15 +31,15 @@ log = "0.4"
# # Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
# Backs scopes and unique keys
slotmap = "1.0.3"
appendlist = "1.4.0"
futures-util = "0.3.15"
smallvec = "1.6.1"
slab = "0.4.3"
[features]
default = ["serialize"]
serialize = ["slotmap/serde", "serde"]
serialize = ["serde"]

View file

@ -21,6 +21,8 @@ Dioxus-core leverages some really cool techniques and hits a very high level of
- suspended nodes (task/fiber endpoints) for asynchronous vnodes
- custom memory allocator for vnodes and all text content
- support for fragments w/ lazy normalization
- slab allocator for scopes
- mirrored-slab approach for remote vdoms
There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, it is likely that zero allocations will need to be performed once the app has been mounted. Only when new components are added to the dom will allocations occur - and only en mass. The space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components.

View file

@ -4,24 +4,24 @@ use std::{cell::UnsafeCell, rc::Rc};
use crate::heuristics::*;
use crate::innerlude::*;
use futures_util::stream::FuturesUnordered;
use slotmap::SlotMap;
use slab::Slab;
slotmap::new_key_type! {
// A dedicated key type for the all the scopes
pub struct ScopeId;
}
// slotmap::new_key_type! {
// // A dedicated key type for the all the scopes
// pub struct ScopeId;
// }
// #[cfg(feature = "serialize", serde::Serialize)]
// #[cfg(feature = "serialize", serde::Serialize)]
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ScopeId(usize);
slotmap::new_key_type! {
// A dedicated key type for every real element that the virtualdom creates.
//
// This is a slotmap key because we expect the "mirror" realdom to also maintain a slotmap mapping
// of virtual element to real element.
pub struct ElementId;
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ElementId(usize);
impl ElementId {
pub fn as_u64(self) -> u64 {
self.0.as_ffi()
todo!()
// self.0.as_ffi()
}
}
@ -30,7 +30,7 @@ type Shared<T> = Rc<RefCell<T>>;
/// These are resources shared among all the components and the virtualdom itself
#[derive(Clone)]
pub struct SharedResources {
pub components: Rc<UnsafeCell<SlotMap<ScopeId, Scope>>>,
pub components: Rc<UnsafeCell<Slab<Scope>>>,
pub event_queue: Shared<Vec<HeightMarker>>,
@ -42,34 +42,38 @@ pub struct SharedResources {
/// We use a SlotSet to keep track of the keys that are currently being used.
/// However, we don't store any specific data since the "mirror"
pub raw_elements: Shared<SlotMap<ElementId, ()>>,
pub raw_elements: Shared<Slab<()>>,
pub task_setter: Rc<dyn Fn(ScopeId)>,
}
impl SharedResources {
pub fn new() -> Self {
// preallocate 1000 elements and 20 scopes to avoid dynamic allocation
let components = Rc::new(UnsafeCell::new(
SlotMap::<ScopeId, Scope>::with_capacity_and_key(20),
));
let raw_elements = SlotMap::<ElementId, ()>::with_capacity_and_key(1000);
// preallocate 2000 elements and 20 scopes to avoid dynamic allocation
let components: Rc<UnsafeCell<Slab<Scope>>> =
Rc::new(UnsafeCell::new(Slab::with_capacity(100)));
// elements are super cheap - the value takes no space
let raw_elements = Slab::with_capacity(2000);
let event_queue = Rc::new(RefCell::new(Vec::new()));
let tasks = Vec::new();
let heuristics = HeuristicsEngine::new();
let queue = event_queue.clone();
let _components = components.clone();
let task_setter = Rc::new(move |idx| {
let comps = unsafe { &*_components.get() };
if let Some(scope) = comps.get(idx) {
queue.borrow_mut().push(HeightMarker {
height: scope.height,
idx,
})
}
});
let task_setter = {
let queue = event_queue.clone();
let components = components.clone();
Rc::new(move |idx: ScopeId| {
let comps = unsafe { &*components.get() };
if let Some(scope) = comps.get(idx.0) {
queue.borrow_mut().push(HeightMarker {
height: scope.height,
idx,
})
}
})
};
Self {
event_queue,
@ -85,13 +89,13 @@ impl SharedResources {
/// this is unsafe because the caller needs to track which other scopes it's already using
pub unsafe fn get_scope(&self, idx: ScopeId) -> Option<&Scope> {
let inner = &*self.components.get();
inner.get(idx)
inner.get(idx.0)
}
/// this is unsafe because the caller needs to track which other scopes it's already using
pub unsafe fn get_sope_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
pub unsafe fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
let inner = &mut *self.components.get();
inner.get_mut(idx)
inner.get_mut(idx.0)
}
pub fn with_scope<'b, O: 'static>(
@ -114,13 +118,13 @@ impl SharedResources {
pub fn try_remove(&self, id: ScopeId) -> Result<Scope> {
let inner = unsafe { &mut *self.components.get() };
inner
.remove(id)
.ok_or_else(|| Error::FatalInternal("Scope not found"))
Ok(inner.remove(id.0))
// .try_remove(id.0)
// .ok_or_else(|| Error::FatalInternal("Scope not found"))
}
pub fn reserve_node(&self) -> ElementId {
self.raw_elements.borrow_mut().insert(())
ElementId(self.raw_elements.borrow_mut().insert(()))
}
/// return the id, freeing the space of the original node
@ -132,7 +136,10 @@ impl SharedResources {
pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId {
let g = unsafe { &mut *self.components.get() };
g.insert_with_key(f)
let entry = g.vacant_entry();
let id = ScopeId(entry.key());
entry.insert(f(id));
id
}
pub fn schedule_update(&self) -> Rc<dyn Fn(ScopeId)> {

View file

@ -36,10 +36,9 @@
//!
//! ## Garbage Collection
//! ---------------------
//! We roughly place the role of garbage collection onto the reconciler. Dioxus needs to manage the lifecycle of components
//! but will not spend any time cleaning up old elements. It's the Reconciler's duty to understand which elements need to
//! be cleaned up *after* the diffing is completed. The reconciler should schedule this garbage collection as the absolute
//! lowest priority task, after all edits have been applied.
//! Dioxus uses a passive garbage collection system to clean up old nodes once the work has been completed. This garabge
//! collection is done internally once the main diffing work is complete. After the "garbage" is collected, Dioxus will then
//! start to re-use old keys for new nodes. This results in a passive memory management system that is very efficient.
//!
//!
//! Further Reading and Thoughts
@ -54,8 +53,6 @@ use smallvec::{smallvec, SmallVec};
use std::{any::Any, borrow::Borrow};
/// The accompanying "real dom" exposes an imperative API for controlling the UI layout
///
/// Instead of having handles directly over nodes, Dioxus uses simple u64s as node IDs.
/// The expectation is that the underlying renderer will mainain their Nodes in something like slotmap or an ECS memory
/// where indexing is very fast. For reference, the slotmap in the WebSys renderer takes about 3ns to randomly access any
@ -69,39 +66,36 @@ pub trait RealDom<'a> {
fn raw_node_as_any(&self) -> &mut dyn Any;
}
pub struct DiffMachine<'real, 'bump, Dom: RealDom<'bump>> {
pub real_dom: &'real mut Dom,
pub struct DiffMachine<'real, 'bump> {
pub real_dom: &'real dyn RealDom<'bump>,
pub vdom: &'bump SharedResources,
pub edits: DomEditor<'real, 'bump>,
pub scheduled_garbage: Vec<&'bump VNode<'bump>>,
pub cur_idxs: SmallVec<[ScopeId; 5]>,
pub diffed: FxHashSet<ScopeId>,
pub seen_nodes: FxHashSet<ScopeId>,
}
impl<'r, 'b, D: RealDom<'b>> DiffMachine<'r, 'b, D> {
impl<'r, 'b> DiffMachine<'r, 'b> {
pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'b mut Scope> {
// ensure we haven't seen this scope before
// if we have, then we're trying to alias it, which is not allowed
debug_assert!(!self.seen_nodes.contains(id));
let compon = unsafe { &mut *self.vdom.components.get() };
compon.get_mut(*id)
unsafe { self.vdom.get_scope_mut(*id) }
}
pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'b Scope> {
// ensure we haven't seen this scope before
// if we have, then we're trying to alias it, which is not allowed
let compon = unsafe { &*self.vdom.components.get() };
compon.get(*id)
unsafe { self.vdom.get_scope(*id) }
}
}
impl<'real, 'bump, Dom> DiffMachine<'real, 'bump, Dom>
where
Dom: RealDom<'bump>,
{
impl<'real, 'bump> DiffMachine<'real, 'bump> {
pub fn new(
edits: &'real mut Vec<DomEdit<'bump>>,
dom: &'real mut Dom,
dom: &'real dyn RealDom<'bump>,
cur_idx: ScopeId,
shared: &'bump SharedResources,
) -> Self {
@ -110,6 +104,7 @@ where
edits: DomEditor::new(edits),
cur_idxs: smallvec![cur_idx],
vdom: shared,
scheduled_garbage: vec![],
diffed: FxHashSet::default(),
seen_nodes: FxHashSet::default(),
}
@ -151,6 +146,7 @@ where
self.edits.push_root(root);
let meta = self.create(new_node);
self.edits.replace_with(meta.added_to_stack);
self.scheduled_garbage.push(old_node);
self.edits.pop();
return;
}
@ -208,14 +204,15 @@ where
// remove any leftovers
for to_remove in old_iter {
self.edits.push_root(to_remove);
self.edits.push_root(to_remove.element_id().unwrap());
self.edits.remove();
}
// seems like we could combine this into a single instruction....
self.edits.push_root(first);
self.edits.push_root(first.element_id().unwrap());
let meta = self.create(new_node);
self.edits.replace_with(meta.added_to_stack);
self.scheduled_garbage.push(old_node);
self.edits.pop();
// Wipe the old one and plant the new one
@ -272,11 +269,11 @@ where
// remove any leftovers
for to_remove in old_iter {
self.edits.push_root(to_remove);
self.edits.push_root(to_remove.element_id().unwrap());
self.edits.remove();
}
back_node
back_node.element_id().unwrap()
}
};
@ -314,10 +311,7 @@ impl CreateMeta {
}
}
impl<'real, 'bump, Dom> DiffMachine<'real, 'bump, Dom>
where
Dom: RealDom<'bump>,
{
impl<'real, 'bump> DiffMachine<'real, 'bump> {
// Emit instructions to create the given virtual node.
//
// The change list stack may have any shape upon entering this function:
@ -448,8 +442,7 @@ where
// .insert(idx);
// TODO: abstract this unsafe into the arena abstraction
let inner: &'bump mut _ = unsafe { &mut *self.vdom.components.get() };
let new_component = inner.get_mut(new_idx).unwrap();
let new_component = self.get_scope_mut(&new_idx).unwrap();
// Actually initialize the caller's slot with the right address
vcomponent.ass_scope.set(Some(new_idx));
@ -498,7 +491,7 @@ where
}
}
impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
impl<'a, 'bump> DiffMachine<'a, 'bump> {
/// Destroy a scope and all of its descendents.
///
/// Calling this will run the destuctors on all hooks in the tree.
@ -727,7 +720,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
// [... parent]
//
// Upon exiting, the change list stack is in the same state.
fn diff_keyed_children(&self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) {
fn diff_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) {
// todo!();
if cfg!(debug_assertions) {
let mut keys = fxhash::FxHashSet::default();
@ -761,11 +754,6 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
KeyedPrefixResult::MoreWorkToDo(count) => count,
};
match self.diff_keyed_prefix(old, new) {
KeyedPrefixResult::Finished => return,
KeyedPrefixResult::MoreWorkToDo(count) => count,
};
// Next, we find out how many of the nodes at the end of the children have
// the same key. We do _not_ diff them yet, since we want to emit the change
// list instructions such that they can be applied in a single pass over the
@ -812,46 +800,47 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
//
// Upon exit, the change list stack is the same.
fn diff_keyed_prefix(
&self,
_old: &'bump [VNode<'bump>],
_new: &'bump [VNode<'bump>],
&mut self,
old: &'bump [VNode<'bump>],
new: &'bump [VNode<'bump>],
) -> KeyedPrefixResult {
todo!()
// self.edits.go_down();
// let mut shared_prefix_count = 0;
// for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() {
// if old.key() != new.key() {
// break;
// }
let mut shared_prefix_count = 0;
// self.edits.go_to_sibling(i);
for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() {
// abort early if we finally run into nodes with different keys
if old.key() != new.key() {
break;
}
// self.diff_node(old, new);
// self.edits.go_to_sibling(i);
// shared_prefix_count += 1;
// }
self.diff_node(old, new);
// // If that was all of the old children, then create and append the remaining
// // new children and we're finished.
// if shared_prefix_count == old.len() {
// self.edits.go_up();
// // self.edits.commit_traversal();
// self.create_and_append_children(&new[shared_prefix_count..]);
// return KeyedPrefixResult::Finished;
// }
shared_prefix_count += 1;
}
// // And if that was all of the new children, then remove all of the remaining
// // old children and we're finished.
// if shared_prefix_count == new.len() {
// self.edits.go_to_sibling(shared_prefix_count);
// // self.edits.commit_traversal();
// self.remove_self_and_next_siblings(&old[shared_prefix_count..]);
// return KeyedPrefixResult::Finished;
// }
// If that was all of the old children, then create and append the remaining
// new children and we're finished.
if shared_prefix_count == old.len() {
// self.edits.go_up();
// self.edits.commit_traversal();
self.create_and_append_children(&new[shared_prefix_count..]);
return KeyedPrefixResult::Finished;
}
// And if that was all of the new children, then remove all of the remaining
// old children and we're finished.
if shared_prefix_count == new.len() {
// self.edits.go_to_sibling(shared_prefix_count);
// self.edits.commit_traversal();
self.remove_self_and_next_siblings(&old[shared_prefix_count..]);
return KeyedPrefixResult::Finished;
}
//
// self.edits.go_up();
// KeyedPrefixResult::MoreWorkToDo(shared_prefix_count)
KeyedPrefixResult::MoreWorkToDo(shared_prefix_count)
}
// Remove all of a node's children.
@ -902,13 +891,12 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
// Upon exit from this function, it will be restored to that same state.
fn diff_keyed_middle(
&self,
_old: &[VNode<'bump>],
_new: &[VNode<'bump>],
_shared_prefix_count: usize,
_shared_suffix_count: usize,
_old_shared_suffix_start: usize,
old: &[VNode<'bump>],
new: &[VNode<'bump>],
shared_prefix_count: usize,
shared_suffix_count: usize,
old_shared_suffix_start: usize,
) {
todo!()
// // Should have already diffed the shared-key prefixes and suffixes.
// debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key()));
// debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key()));
@ -921,24 +909,30 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
// debug_assert!(new.len() < u32::MAX as usize);
// // Map from each `old` node's key to its index within `old`.
// let mut old_key_to_old_index = FxHashMap::default();
// old_key_to_old_index.reserve(old.len());
// old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i)));
// // IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3).
// let mut old_key_to_old_index = old
// .iter()
// .enumerate()
// .map(|(i, o)| (o.key(), i))
// .collect::<FxHashMap<_, _>>();
// // The set of shared keys between `new` and `old`.
// let mut shared_keys = FxHashSet::default();
// // Map from each index in `new` to the index of the node in `old` that
// // has the same key.
// let mut new_index_to_old_index = Vec::with_capacity(new.len());
// new_index_to_old_index.extend(new.iter().map(|n| {
// let key = n.key();
// if let Some(&i) = old_key_to_old_index.get(&key) {
// shared_keys.insert(key);
// i
// } else {
// u32::MAX as usize
// }
// }));
// let mut new_index_to_old_index = new
// .iter()
// .map(|n| {
// let key = n.key();
// if let Some(&i) = old_key_to_old_index.get(&key) {
// shared_keys.insert(key);
// i
// } else {
// u32::MAX as usize
// }
// })
// .collect::<Vec<_>>();
// // If none of the old keys are reused by the new children, then we
// // remove all the remaining old children and create the new children
@ -948,7 +942,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
// // self.edits.commit_traversal();
// self.remove_all_children(old);
// } else {
// self.edits.go_down_to_child(shared_prefix_count);
// // self.edits.go_down_to_child(shared_prefix_count);
// // self.edits.commit_traversal();
// self.remove_self_and_next_siblings(&old[shared_prefix_count..]);
// }
@ -998,6 +992,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
// // registry.remove_subtree(old_child);
// // todo
// // self.edits.commit_traversal();
// self.edits.remove(old_child.dom_id.get());
// self.edits.remove_child(i + shared_prefix_count);
// removed_count += 1;
// }
@ -1115,10 +1110,6 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
// self.diff_node(&old[old_index], new_child);
// }
// }
// // [... parent child]
// self.edits.go_up();
// [... parent]
}
// Diff the suffix of keyed children that share the same keys in the same order.
@ -1129,26 +1120,17 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
//
// When this function exits, the change list stack remains the same.
fn diff_keyed_suffix(
&self,
_old: &[VNode<'bump>],
_new: &[VNode<'bump>],
_new_shared_suffix_start: usize,
&mut self,
old: &'bump [VNode<'bump>],
new: &'bump [VNode<'bump>],
new_shared_suffix_start: usize,
) {
todo!()
// debug_assert_eq!(old.len(), new.len());
// debug_assert!(!old.is_empty());
debug_assert_eq!(old.len(), new.len());
debug_assert!(!old.is_empty());
// // [... parent]
// self.edits.go_down();
// // [... parent new_child]
// for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() {
// self.edits.go_to_sibling(new_shared_suffix_start + i);
// self.diff_node(old_child, new_child);
// }
// // [... parent]
// self.edits.go_up();
for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() {
self.diff_node(old_child, new_child);
}
}
// Diff children that are not keyed.
@ -1299,11 +1281,11 @@ impl<'a> RealChildIterator<'a> {
}
impl<'a> Iterator for RealChildIterator<'a> {
type Item = ElementId;
type Item = &'a VNode<'a>;
fn next(&mut self) -> Option<ElementId> {
fn next(&mut self) -> Option<&'a VNode<'a>> {
let mut should_pop = false;
let mut returned_node = None;
let mut returned_node: Option<&'a VNode<'a>> = None;
let mut should_push = None;
while returned_node.is_none() {
@ -1315,7 +1297,7 @@ impl<'a> Iterator for RealChildIterator<'a> {
// We've recursed INTO an element/text
// We need to recurse *out* of it and move forward to the next
should_pop = true;
returned_node = node.dom_id.get();
returned_node = Some(&*node);
}
// If we get a fragment we push the next child
@ -1324,7 +1306,7 @@ impl<'a> Iterator for RealChildIterator<'a> {
if frag.children.len() == 0 {
should_pop = true;
returned_node = node.dom_id.get();
returned_node = Some(&*node);
}
if subcount >= frag.children.len() {

View file

@ -17,17 +17,18 @@ use std::{
pub struct VNode<'src> {
pub kind: VNodeKind<'src>,
///
/// ElementId supports NonZero32 and Cell is zero cost, so the size of this field is unaffected
///
///
pub(crate) dom_id: Cell<Option<ElementId>>,
pub(crate) key: Option<&'src str>,
/// ElementId supports NonZero32 and Cell is zero cost, so the size of this field is unaffected
pub(crate) dom_id: Cell<Option<ElementId>>,
}
impl VNode<'_> {
fn key(&self) -> Option<&str> {
pub fn key(&self) -> Option<&str> {
self.key
}
pub fn element_id(&self) -> Option<ElementId> {
self.dom_id.get()
}
}
/// Tools for the base unit of the virtual dom - the VNode

View file

@ -21,8 +21,6 @@
use crate::{arena::SharedResources, innerlude::*};
use slotmap::DefaultKey;
use slotmap::SlotMap;
use std::any::Any;
use std::any::TypeId;
@ -181,10 +179,10 @@ impl VirtualDom {
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch
///
/// The diff machine expects the RealDom's stack to be the root of the application
pub fn rebuild<'s, Dom: RealDom<'s>>(
pub fn rebuild<'s>(
&'s mut self,
realdom: &mut Dom,
edits: &mut Vec<DomEdit<'s>>,
realdom: &'s mut dyn RealDom<'s>,
edits: &'s mut Vec<DomEdit<'s>>,
) -> Result<()> {
let mut diff_machine = DiffMachine::new(edits, realdom, self.base_scope, &self.shared);
@ -260,9 +258,9 @@ impl VirtualDom {
// but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework.
//
// A good project would be to remove all unsafe from this crate and move the unsafety into safer abstractions.
pub async fn progress_with_event<'s, Dom: RealDom<'s>>(
pub async fn progress_with_event<'s>(
&'s mut self,
realdom: &'_ mut Dom,
realdom: &'s mut dyn RealDom<'s>,
edits: &mut Vec<DomEdit<'s>>,
) -> Result<()> {
let trigger = self.triggers.borrow_mut().pop().expect("failed");

View file

@ -2,7 +2,7 @@ use std::{collections::HashMap, rc::Rc, sync::Arc};
use dioxus_core::{
events::{EventTrigger, VirtualEvent},
DomEdit, RealDomNode, ScopeId,
DomEdit, ElementId, ScopeId,
};
use fxhash::FxHashMap;
use slotmap::{DefaultKey, Key, KeyData};
@ -588,7 +588,6 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
// let evt: web_sys::ToggleEvent = event.clone().dyn_into().unwrap();
todo!()
}
_ => VirtualEvent::OtherEvent,
}
}