Subtree memorization / reactive templates (#488)
This commit adds subtree memoization to Dioxus. Subtree memoization is basically a compile-time step that drastically reduces the amount of work the diffing engine needs to do at runtime by extracting non-changing nodes out into a static "template." Templates are then understood by the various renderers in the ecosystem as a faster way of rendering the same items. For example, in the web, templates are simply a set of DOM Nodes created once and then cloned later. This is the same pattern frameworks like Lithtml and SolidJS use to achieve near-perfect performance. Subtree memoization adds an additional level of complexity to Dioxus. The RSX macro needs to be much smarter to identify changing/nonchanging nodes and generate a mapping between the Template and its runtime counterparts. This commit represents a working starter point for this work, adding support for templates for the web, desktop, liveview, ssr, and native-core renderers. In the future we will try to shrink code generation, generally improve performance, and simplify our implementation.
This commit is contained in:
1 parent
b32fd2d2cd
commit
047ed1e553
80 files changed
+6774
-2552
No files matched your search
@@ -2,6 +2,7 @@ pub mod layout_attributes;
|
||||
pub mod node_ref;
|
||||
pub mod real_dom;
|
||||
pub mod state;
|
||||
pub mod template;
|
||||
#[doc(hidden)]
|
||||
pub mod traversable;
|
||||
pub mod utils;
|
||||
@@ -1,37 +1,39 @@
|
||||
use dioxus_core::*;
|
||||
|
||||
use crate::state::union_ordered_iter;
|
||||
use crate::{
|
||||
real_dom::{NodeData, NodeType, OwnedAttributeView},
|
||||
state::union_ordered_iter,
|
||||
};
|
||||
|
||||
/// A view into a [VNode] with limited access.
|
||||
#[derive(Debug)]
|
||||
pub struct NodeView<'a> {
|
||||
inner: &'a VNode<'a>,
|
||||
inner: &'a NodeData,
|
||||
mask: NodeMask,
|
||||
}
|
||||
|
||||
impl<'a> NodeView<'a> {
|
||||
/// Create a new NodeView from a VNode, and mask.
|
||||
pub fn new(mut vnode: &'a VNode<'a>, view: NodeMask, vdom: &'a VirtualDom) -> Self {
|
||||
if let VNode::Component(sc) = vnode {
|
||||
let scope = vdom.get_scope(sc.scope.get().unwrap()).unwrap();
|
||||
vnode = scope.root_node();
|
||||
}
|
||||
pub fn new(node: &'a NodeData, view: NodeMask) -> Self {
|
||||
Self {
|
||||
inner: vnode,
|
||||
inner: node,
|
||||
mask: view,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the id of the node
|
||||
pub fn id(&self) -> ElementId {
|
||||
self.inner.mounted_id()
|
||||
pub fn id(&self) -> GlobalNodeId {
|
||||
self.inner.id
|
||||
}
|
||||
|
||||
/// Get the tag of the node if the tag is enabled in the mask
|
||||
pub fn tag(&self) -> Option<&'a str> {
|
||||
self.mask
|
||||
.tag
|
||||
.then(|| self.try_element().map(|el| el.tag))
|
||||
.then(|| match &self.inner.node_type {
|
||||
NodeType::Element { tag, .. } => Some(&**tag),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
@@ -39,47 +41,47 @@ impl<'a> NodeView<'a> {
|
||||
pub fn namespace(&self) -> Option<&'a str> {
|
||||
self.mask
|
||||
.namespace
|
||||
.then(|| self.try_element().and_then(|el| el.namespace))
|
||||
.then(|| match &self.inner.node_type {
|
||||
NodeType::Element { namespace, .. } => namespace.map(|s| &*s),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Get any attributes that are enabled in the mask
|
||||
pub fn attributes(&self) -> impl Iterator<Item = &Attribute<'a>> {
|
||||
self.try_element()
|
||||
.map(|el| el.attributes)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter(|a| self.mask.attritutes.contains_attribute(a.name))
|
||||
pub fn attributes<'b>(&'b self) -> Option<impl Iterator<Item = OwnedAttributeView<'a>> + 'b> {
|
||||
match &self.inner.node_type {
|
||||
NodeType::Element { attributes, .. } => Some(
|
||||
attributes
|
||||
.iter()
|
||||
.filter(move |(attr, _)| self.mask.attritutes.contains_attribute(&attr.name))
|
||||
.map(|(attr, val)| OwnedAttributeView {
|
||||
attribute: attr,
|
||||
value: val,
|
||||
}),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the text if it is enabled in the mask
|
||||
pub fn text(&self) -> Option<&str> {
|
||||
self.mask
|
||||
.text
|
||||
.then(|| self.try_text().map(|txt| txt.text))
|
||||
.then(|| match &self.inner.node_type {
|
||||
NodeType::Text { text } => Some(&**text),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Get the listeners if it is enabled in the mask
|
||||
pub fn listeners(&self) -> &'a [Listener<'a>] {
|
||||
self.try_element()
|
||||
.map(|el| el.listeners)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Try to get the underlying element.
|
||||
fn try_element(&self) -> Option<&'a VElement<'a>> {
|
||||
if let VNode::Element(el) = &self.inner {
|
||||
Some(el)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to get the underlying text node.
|
||||
fn try_text(&self) -> Option<&'a VText<'a>> {
|
||||
if let VNode::Text(txt) = &self.inner {
|
||||
Some(txt)
|
||||
pub fn listeners(&self) -> Option<impl Iterator<Item = &'a str> + '_> {
|
||||
if self.mask.listeners {
|
||||
match &self.inner.node_type {
|
||||
NodeType::Element { listeners, .. } => Some(listeners.iter().map(|l| &**l)),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -100,7 +102,7 @@ impl AttributeMask {
|
||||
/// A empty attribute mask
|
||||
pub const NONE: Self = Self::Static(&[]);
|
||||
|
||||
fn contains_attribute(&self, attr: &'static str) -> bool {
|
||||
fn contains_attribute(&self, attr: &str) -> bool {
|
||||
match self {
|
||||
AttributeMask::All => true,
|
||||
AttributeMask::Dynamic(l) => l.binary_search(&attr).is_ok(),
|
||||
@@ -215,7 +217,8 @@ impl NodeMask {
|
||||
/// A node mask with every part visible.
|
||||
pub const ALL: Self = Self::new_with_attrs(AttributeMask::All)
|
||||
.with_text()
|
||||
.with_element();
|
||||
.with_element()
|
||||
.with_listeners();
|
||||
|
||||
/// Check if two masks overlap
|
||||
pub fn overlaps(&self, other: &Self) -> bool {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,10 +1,11 @@
|
||||
use std::{cmp::Ordering, fmt::Debug};
|
||||
|
||||
use anymap::AnyMap;
|
||||
use dioxus_core::ElementId;
|
||||
use dioxus_core::GlobalNodeId;
|
||||
use fxhash::FxHashSet;
|
||||
|
||||
use crate::node_ref::{NodeMask, NodeView};
|
||||
use crate::real_dom::NodeData;
|
||||
use crate::traversable::Traversable;
|
||||
|
||||
/// Join two sorted iterators
|
||||
@@ -206,12 +207,16 @@ pub trait NodeDepState<DepState = ()> {
|
||||
/// Do not implement this trait. It is only meant to be derived and used through [crate::real_dom::RealDom].
|
||||
pub trait State: Default + Clone {
|
||||
#[doc(hidden)]
|
||||
fn update<'a, T: Traversable<Node = Self, Id = ElementId>>(
|
||||
dirty: &[(ElementId, NodeMask)],
|
||||
fn update<
|
||||
'a,
|
||||
T: Traversable<Node = Self, Id = GlobalNodeId>,
|
||||
T2: Traversable<Node = NodeData, Id = GlobalNodeId>,
|
||||
>(
|
||||
dirty: &[(GlobalNodeId, NodeMask)],
|
||||
state_tree: &'a mut T,
|
||||
vdom: &'a dioxus_core::VirtualDom,
|
||||
rdom: &'a T2,
|
||||
ctx: &AnyMap,
|
||||
) -> FxHashSet<ElementId>;
|
||||
) -> FxHashSet<GlobalNodeId>;
|
||||
}
|
||||
|
||||
impl ChildDepState for () {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use dioxus_core::{GlobalNodeId, TemplateNodeId};
|
||||
|
||||
use crate::{real_dom::Node, state::State};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NativeTemplate<S: State> {
|
||||
pub(crate) nodes: Vec<Option<Box<Node<S>>>>,
|
||||
pub(crate) roots: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<S: State> NativeTemplate<S> {
|
||||
pub fn insert(&mut self, node: Node<S>) {
|
||||
let id = node.node_data.id;
|
||||
match id {
|
||||
GlobalNodeId::TemplateId {
|
||||
template_node_id: TemplateNodeId(id),
|
||||
..
|
||||
} => {
|
||||
self.nodes.resize(id + 1, None);
|
||||
self.nodes[id] = Some(Box::new(node));
|
||||
}
|
||||
GlobalNodeId::VNodeId(_) => panic!("Cannot insert a VNode into a template"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum TemplateRefOrNode<S: State> {
|
||||
Ref {
|
||||
nodes: Vec<Option<Box<Node<S>>>>,
|
||||
roots: Vec<GlobalNodeId>,
|
||||
parent: Option<GlobalNodeId>,
|
||||
},
|
||||
Node(Node<S>),
|
||||
}
|
||||
|
||||
impl<S: State> TemplateRefOrNode<S> {
|
||||
pub fn parent(&self) -> Option<GlobalNodeId> {
|
||||
match self {
|
||||
TemplateRefOrNode::Ref { parent, .. } => *parent,
|
||||
TemplateRefOrNode::Node(node) => node.node_data.parent,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@ use crate::{
|
||||
real_dom::{NodeType, RealDom},
|
||||
state::State,
|
||||
};
|
||||
use dioxus_core::{DomEdit, ElementId, Mutations};
|
||||
use dioxus_core::{DomEdit, ElementId, GlobalNodeId, Mutations};
|
||||
|
||||
pub enum ElementProduced {
|
||||
/// The iterator produced an element by progressing to the next node in a depth first order.
|
||||
Progressed(ElementId),
|
||||
Progressed(GlobalNodeId),
|
||||
/// The iterator reached the end of the tree and looped back to the root
|
||||
Looped(ElementId),
|
||||
Looped(GlobalNodeId),
|
||||
}
|
||||
impl ElementProduced {
|
||||
pub fn id(&self) -> ElementId {
|
||||
pub fn id(&self) -> GlobalNodeId {
|
||||
match self {
|
||||
ElementProduced::Progressed(id) => *id,
|
||||
ElementProduced::Looped(id) => *id,
|
||||
@@ -50,13 +50,16 @@ impl NodePosition {
|
||||
/// The iterator loops around when it reaches the end or the beginning.
|
||||
pub struct PersistantElementIter {
|
||||
// stack of elements and fragments
|
||||
stack: smallvec::SmallVec<[(ElementId, NodePosition); 5]>,
|
||||
stack: smallvec::SmallVec<[(GlobalNodeId, NodePosition); 5]>,
|
||||
}
|
||||
|
||||
impl Default for PersistantElementIter {
|
||||
fn default() -> Self {
|
||||
PersistantElementIter {
|
||||
stack: smallvec::smallvec![(ElementId(0), NodePosition::AtNode)],
|
||||
stack: smallvec::smallvec![(
|
||||
GlobalNodeId::VNodeId(dioxus_core::ElementId(0)),
|
||||
NodePosition::AtNode
|
||||
)],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,8 +77,10 @@ impl PersistantElementIter {
|
||||
.edits
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
// nodes within templates will never be removed
|
||||
if let DomEdit::Remove { root } = e {
|
||||
Some(*root)
|
||||
let id = rdom.decode_id(*root);
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -85,7 +90,7 @@ impl PersistantElementIter {
|
||||
if let Some(r) = self
|
||||
.stack
|
||||
.iter()
|
||||
.position(|(el_id, _)| ids_removed.iter().any(|id| el_id.as_u64() == *id))
|
||||
.position(|(el_id, _)| ids_removed.iter().any(|id| el_id == id))
|
||||
{
|
||||
self.stack.truncate(r);
|
||||
changed = true;
|
||||
@@ -93,33 +98,24 @@ impl PersistantElementIter {
|
||||
// if a child is removed or inserted before or at the current element, update the child index
|
||||
for (el_id, child_idx) in self.stack.iter_mut() {
|
||||
if let NodePosition::InChild(child_idx) = child_idx {
|
||||
if let NodeType::Element { children, .. } = &rdom[*el_id].node_type {
|
||||
if let NodeType::Element { children, .. } = &rdom[*el_id].node_data.node_type {
|
||||
for m in &mutations.edits {
|
||||
match m {
|
||||
DomEdit::Remove { root } => {
|
||||
if children
|
||||
.iter()
|
||||
.take(*child_idx + 1)
|
||||
.any(|c| c.as_u64() == *root)
|
||||
{
|
||||
let id = rdom.decode_id(*root);
|
||||
if children.iter().take(*child_idx + 1).any(|c| *c == id) {
|
||||
*child_idx -= 1;
|
||||
}
|
||||
}
|
||||
DomEdit::InsertBefore { root, n } => {
|
||||
if children
|
||||
.iter()
|
||||
.take(*child_idx + 1)
|
||||
.any(|c| c.as_u64() == *root)
|
||||
{
|
||||
let id = rdom.decode_id(*root);
|
||||
if children.iter().take(*child_idx + 1).any(|c| *c == id) {
|
||||
*child_idx += *n as usize;
|
||||
}
|
||||
}
|
||||
DomEdit::InsertAfter { root, n } => {
|
||||
if children
|
||||
.iter()
|
||||
.take(*child_idx)
|
||||
.any(|c| c.as_u64() == *root)
|
||||
{
|
||||
let id = rdom.decode_id(*root);
|
||||
if children.iter().take(*child_idx).any(|c| *c == id) {
|
||||
*child_idx += *n as usize;
|
||||
}
|
||||
}
|
||||
@@ -135,14 +131,14 @@ impl PersistantElementIter {
|
||||
/// get the next element
|
||||
pub fn next<S: State>(&mut self, rdom: &RealDom<S>) -> ElementProduced {
|
||||
if self.stack.is_empty() {
|
||||
let id = ElementId(0);
|
||||
let id = GlobalNodeId::VNodeId(ElementId(0));
|
||||
let new = (id, NodePosition::AtNode);
|
||||
self.stack.push(new);
|
||||
ElementProduced::Looped(id)
|
||||
} else {
|
||||
let (last, o_child_idx) = self.stack.last_mut().unwrap();
|
||||
let node = &rdom[*last];
|
||||
match &node.node_type {
|
||||
match &node.node_data.node_type {
|
||||
NodeType::Element { children, .. } => {
|
||||
*o_child_idx = o_child_idx.map(|i| i + 1);
|
||||
// if we have children, go to the next child
|
||||
@@ -152,7 +148,7 @@ impl PersistantElementIter {
|
||||
self.next(rdom)
|
||||
} else {
|
||||
let id = children[child_idx];
|
||||
if let NodeType::Element { .. } = &rdom[id].node_type {
|
||||
if let NodeType::Element { .. } = &rdom[id].node_data.node_type {
|
||||
self.stack.push((id, NodePosition::AtNode));
|
||||
}
|
||||
ElementProduced::Progressed(id)
|
||||
@@ -171,11 +167,11 @@ impl PersistantElementIter {
|
||||
pub fn prev<S: State>(&mut self, rdom: &RealDom<S>) -> ElementProduced {
|
||||
// recursively add the last child element to the stack
|
||||
fn push_back<S: State>(
|
||||
stack: &mut smallvec::SmallVec<[(ElementId, NodePosition); 5]>,
|
||||
new_node: ElementId,
|
||||
stack: &mut smallvec::SmallVec<[(GlobalNodeId, NodePosition); 5]>,
|
||||
new_node: GlobalNodeId,
|
||||
rdom: &RealDom<S>,
|
||||
) -> ElementId {
|
||||
match &rdom[new_node].node_type {
|
||||
) -> GlobalNodeId {
|
||||
match &rdom[new_node].node_data.node_type {
|
||||
NodeType::Element { children, .. } => {
|
||||
if children.is_empty() {
|
||||
new_node
|
||||
@@ -188,12 +184,12 @@ impl PersistantElementIter {
|
||||
}
|
||||
}
|
||||
if self.stack.is_empty() {
|
||||
let new_node = ElementId(0);
|
||||
let new_node = GlobalNodeId::VNodeId(ElementId(0));
|
||||
ElementProduced::Looped(push_back(&mut self.stack, new_node, rdom))
|
||||
} else {
|
||||
let (last, o_child_idx) = self.stack.last_mut().unwrap();
|
||||
let node = &rdom[*last];
|
||||
match &node.node_type {
|
||||
match &node.node_data.node_type {
|
||||
NodeType::Element { children, .. } => {
|
||||
// if we have children, go to the next child
|
||||
if let NodePosition::InChild(0) = o_child_idx {
|
||||
@@ -227,7 +223,7 @@ impl PersistantElementIter {
|
||||
}
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> ElementId {
|
||||
fn pop(&mut self) -> GlobalNodeId {
|
||||
self.stack.pop().unwrap().0
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user