diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 9651ac483..c1c8d44b7 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -41,6 +41,7 @@ serde = { version = "1", features = ["derive"], optional = true } tokio = { version = "1", features = ["full"] } dioxus = { path = "../dioxus" } pretty_assertions = "1.3.0" +rand = "0.8.5" [features] default = [] diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index a11fc69ba..a1a211527 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,3 +1,5 @@ +use std::ptr::NonNull; + use crate::{ nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode, ScopeId, @@ -17,7 +19,7 @@ pub(crate) struct ElementRef { pub path: ElementPath, // The actual template - pub template: *const VNode<'static>, + pub template: Option>>, } #[derive(Clone, Copy)] @@ -27,9 +29,9 @@ pub enum ElementPath { } impl ElementRef { - pub(crate) fn null() -> Self { + pub(crate) fn none() -> Self { Self { - template: std::ptr::null_mut(), + template: None, path: ElementPath::Root(0), } } @@ -44,12 +46,21 @@ impl VirtualDom { self.next_reference(template, ElementPath::Root(path)) } + pub(crate) fn next_null(&mut self) -> ElementId { + let entry = self.elements.vacant_entry(); + let id = entry.key(); + + entry.insert(ElementRef::none()); + ElementId(id) + } + fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); entry.insert(ElementRef { - template: template as *const _ as *mut _, + // We know this is non-null because it comes from a reference + template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }), path, }); ElementId(id) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 7227ee6e7..410eec55c 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -42,7 +42,7 @@ impl<'b> VirtualDom { // Just move over the placeholder (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), - // Becomes async, do nothing while we ait + // Becomes async, do nothing while we wait (Ready(_nodes), Pending(_fut)) => self.diff_ok_to_async(_nodes, scope), // Placeholder becomes something @@ -65,8 +65,26 @@ impl<'b> VirtualDom { // } - fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _p: &'b VPlaceholder) { - todo!() + fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { + let id = self.next_null(); + p.id.set(Some(id)); + self.mutations.push(Mutation::CreatePlaceholder { id }); + + let pre_edits = self.mutations.edits.len(); + + self.remove_node(l, true); + + // We should always have a remove mutation + // Eventually we don't want to generate placeholders, so this might not be true. But it's true today + assert!(self.mutations.edits.len() > pre_edits); + + // We want to optimize the replace case to use one less mutation if possible + // Since mutations are done in reverse, the last node removed will be the first in the stack + // Instead of *just* removing it, we can use the replace mutation + match self.mutations.edits.pop().unwrap() { + Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m: 1 }), + _ => panic!("Expected remove mutation from remove_node"), + }; } fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { @@ -905,12 +923,7 @@ impl<'b> VirtualDom { match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { RenderReturn::Ready(t) => self.remove_node(t, gen_muts), - RenderReturn::Aborted(t) => { - if let Some(id) = t.id.get() { - self.try_reclaim(id); - } - return; - } + RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts), _ => todo!(), }; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index d05dab347..d8272a57c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -282,7 +282,7 @@ impl VirtualDom { root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0)))); // the root element is always given element ID 0 since it's the container for the entire tree - dom.elements.insert(ElementRef::null()); + dom.elements.insert(ElementRef::none()); dom } @@ -387,7 +387,7 @@ impl VirtualDom { // Loop through each dynamic attribute in this template before moving up to the template's parent. while let Some(el_ref) = parent_path { // safety: we maintain references of all vnodes in the element slab - let template = unsafe { &*el_ref.template }; + let template = unsafe { el_ref.template.unwrap().as_ref() }; let node_template = template.template.get(); let target_path = el_ref.path; diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs new file mode 100644 index 000000000..75dd84c33 --- /dev/null +++ b/packages/core/tests/fuzzing.rs @@ -0,0 +1,305 @@ +use dioxus::prelude::Props; +use dioxus_core::*; +use std::cell::Cell; + +fn random_ns() -> Option<&'static str> { + let namespace = rand::random::() % 2; + match namespace { + 0 => None, + 1 => Some(Box::leak( + format!("ns{}", rand::random::()).into_boxed_str(), + )), + _ => unreachable!(), + } +} + +fn create_random_attribute(attr_idx: &mut usize) -> TemplateAttribute<'static> { + match rand::random::() % 2 { + 0 => TemplateAttribute::Static { + name: Box::leak(format!("attr{}", rand::random::()).into_boxed_str()), + value: Box::leak(format!("value{}", rand::random::()).into_boxed_str()), + namespace: random_ns(), + }, + 1 => TemplateAttribute::Dynamic { + id: { + let old_idx = *attr_idx; + *attr_idx += 1; + old_idx + }, + }, + _ => unreachable!(), + } +} + +fn create_random_template_node( + dynamic_node_types: &mut Vec, + template_idx: &mut usize, + attr_idx: &mut usize, + depth: usize, +) -> TemplateNode<'static> { + match rand::random::() % 4 { + 0 => { + let attrs = { + let attrs: Vec<_> = (0..(rand::random::() % 10)) + .map(|_| create_random_attribute(attr_idx)) + .collect(); + Box::leak(attrs.into_boxed_slice()) + }; + TemplateNode::Element { + tag: Box::leak(format!("tag{}", rand::random::()).into_boxed_str()), + namespace: random_ns(), + attrs, + children: { + if depth > 4 { + &[] + } else { + let children: Vec<_> = (0..(rand::random::() % 3)) + .map(|_| { + create_random_template_node( + dynamic_node_types, + template_idx, + attr_idx, + depth + 1, + ) + }) + .collect(); + Box::leak(children.into_boxed_slice()) + } + }, + } + } + 1 => TemplateNode::Text { + text: Box::leak(format!("{}", rand::random::()).into_boxed_str()), + }, + 2 => TemplateNode::DynamicText { + id: { + let old_idx = *template_idx; + *template_idx += 1; + dynamic_node_types.push(DynamicNodeType::Text); + old_idx + }, + }, + 3 => TemplateNode::Dynamic { + id: { + let old_idx = *template_idx; + *template_idx += 1; + dynamic_node_types.push(DynamicNodeType::Other); + old_idx + }, + }, + _ => unreachable!(), + } +} + +fn generate_paths( + node: &TemplateNode<'static>, + current_path: &[u8], + node_paths: &mut Vec>, + attr_paths: &mut Vec>, +) { + match node { + TemplateNode::Element { children, attrs, .. } => { + for attr in *attrs { + match attr { + TemplateAttribute::Static { .. } => {} + TemplateAttribute::Dynamic { .. } => { + attr_paths.push(current_path.to_vec()); + } + } + } + for (i, child) in children.iter().enumerate() { + let mut current_path = current_path.to_vec(); + current_path.push(i as u8); + generate_paths(child, ¤t_path, node_paths, attr_paths); + } + } + TemplateNode::Text { .. } => {} + TemplateNode::DynamicText { .. } => { + node_paths.push(current_path.to_vec()); + } + TemplateNode::Dynamic { .. } => { + node_paths.push(current_path.to_vec()); + } + } +} + +enum DynamicNodeType { + Text, + Other, +} + +fn create_random_template(name: &'static str) -> (Template<'static>, Vec) { + let mut dynamic_node_type = Vec::new(); + let mut template_idx = 0; + let mut attr_idx = 0; + let roots = (0..(1 + rand::random::() % 5)) + .map(|_| { + create_random_template_node(&mut dynamic_node_type, &mut template_idx, &mut attr_idx, 0) + }) + .collect::>(); + assert!(!roots.is_empty()); + let roots = Box::leak(roots.into_boxed_slice()); + let mut node_paths = Vec::new(); + let mut attr_paths = Vec::new(); + for (i, root) in roots.iter().enumerate() { + generate_paths(root, &[i as u8], &mut node_paths, &mut attr_paths); + } + let node_paths = Box::leak( + node_paths + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>() + .into_boxed_slice(), + ); + let attr_paths = Box::leak( + attr_paths + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>() + .into_boxed_slice(), + ); + ( + Template { name, roots, node_paths, attr_paths }, + dynamic_node_type, + ) +} + +fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { + let range = if depth > 3 { 1 } else { 3 }; + match rand::random::() % range { + 0 => DynamicNode::Placeholder(Default::default()), + 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| VNode { + key: None, + parent: Default::default(), + template: Cell::new(Template { + name: concat!(file!(), ":", line!(), ":", column!(), ":0"), + roots: &[TemplateNode::Dynamic { id: 0 }], + node_paths: &[&[0]], + attr_paths: &[], + }), + root_ids: Default::default(), + dynamic_nodes: cx.bump().alloc([cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + )]), + dynamic_attrs: &[], + })), + 2 => cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + ), + _ => unreachable!(), + } +} + +fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { + let value = match rand::random::() % 6 { + 0 => AttributeValue::Text(Box::leak( + format!("{}", rand::random::()).into_boxed_str(), + )), + 1 => AttributeValue::Float(rand::random()), + 2 => AttributeValue::Int(rand::random()), + 3 => AttributeValue::Bool(rand::random()), + 4 => cx.any_value(rand::random::()), + 5 => AttributeValue::None, + // Listener(RefCell>>), + _ => unreachable!(), + }; + Attribute { + name: Box::leak(format!("attr{}", rand::random::()).into_boxed_str()), + value, + namespace: random_ns(), + mounted_element: Default::default(), + volatile: rand::random(), + } +} + +static mut TEMPLATE_COUNT: usize = 0; + +#[derive(PartialEq, Props)] +struct DepthProps { + depth: usize, + root: bool, +} + +fn create_random_element(cx: Scope) -> Element { + cx.needs_update(); + let range = if cx.props.root { 2 } else { 3 }; + let node = match rand::random::() % range { + 0 | 1 => { + let (template, dynamic_node_types) = create_random_template(Box::leak( + format!( + "{}{}", + concat!(file!(), ":", line!(), ":", column!(), ":"), + { + unsafe { + let old = TEMPLATE_COUNT; + TEMPLATE_COUNT += 1; + old + } + } + ) + .into_boxed_str(), + )); + println!("{template:#?}"); + let node = VNode { + key: None, + parent: None, + template: Cell::new(template), + root_ids: Default::default(), + dynamic_nodes: { + let dynamic_nodes: Vec<_> = dynamic_node_types + .iter() + .map(|ty| match ty { + DynamicNodeType::Text => DynamicNode::Text(VText { + value: Box::leak( + format!("{}", rand::random::()).into_boxed_str(), + ), + id: Default::default(), + }), + DynamicNodeType::Other => { + create_random_dynamic_node(cx, cx.props.depth + 1) + } + }) + .collect(); + cx.bump().alloc(dynamic_nodes) + }, + dynamic_attrs: cx.bump().alloc( + (0..template.attr_paths.len()) + .map(|_| create_random_dynamic_attr(cx)) + .collect::>(), + ), + }; + Some(node) + } + _ => None, + }; + println!("{node:#?}"); + node +} + +// test for panics when creating random nodes and templates +#[test] +fn create() { + for _ in 0..100 { + let mut vdom = + VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); + let _ = vdom.rebuild(); + } +} + +// test for panics when diffing random nodes +// This test will change the template every render which is not very realistic, but it helps stress the system +#[test] +fn diff() { + for _ in 0..10 { + let mut vdom = + VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); + let _ = vdom.rebuild(); + for _ in 0..10 { + let _ = vdom.render_immediate(); + } + } +} diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 6cf60c6ea..3fc400ea9 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -294,7 +294,7 @@ builder_constructors! { rel: LinkType DEFAULT, sizes: String DEFAULT, // FIXME title: String DEFAULT, // FIXME - r#type: Mime DEFAULT, + r#type: Mime "type", integrity: String DEFAULT, }; @@ -312,7 +312,7 @@ builder_constructors! { /// [`