diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 4575b848a..93a9af585 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -4,6 +4,7 @@ use std::cmp::Ordering; use std::collections::HashSet; use std::hash::Hash; use std::ops::Deref; +use std::rc::Rc; use web_sys::Element; @@ -22,6 +23,22 @@ pub(super) struct BList { key: Option, } +impl VList { + // Splits a VList for creating / reconciling to a BList. + fn split_for_blist(self) -> (Option, bool, Vec) { + let fully_keyed = self.fully_keyed(); + + let children = self + .children + .map(Rc::try_unwrap) + .unwrap_or_else(|| Ok(Vec::new())) + // Rc::unwrap_or_clone is not stable yet. + .unwrap_or_else(|m| m.to_vec()); + + (self.key, fully_keyed, children) + } +} + impl Deref for BList { type Target = Vec; @@ -397,7 +414,7 @@ impl Reconcilable for VList { } fn reconcile( - mut self, + self, root: &BSubtree, parent_scope: &AnyScope, parent: &Element, @@ -412,16 +429,16 @@ impl Reconcilable for VList { // The left items are known since we want to insert them // (self.children). For the right ones, we will look at the bundle, // i.e. the current DOM list element that we want to replace with self. + let (key, mut fully_keyed, mut lefts) = self.split_for_blist(); - if self.children.is_empty() { + if lefts.is_empty() { // Without a placeholder the next element becomes first // and corrupts the order of rendering // We use empty text element to stake out a place - self.add_child(VText::new("").into()); + lefts.push(VText::new("").into()); + fully_keyed = false; } - let fully_keyed = self.fully_keyed(); - let lefts = self.children; let rights = &mut blist.rev_children; test_log!("lefts: {:?}", lefts); test_log!("rights: {:?}", rights); @@ -435,7 +452,7 @@ impl Reconcilable for VList { BList::apply_unkeyed(root, parent_scope, parent, slot, lefts, rights) }; blist.fully_keyed = fully_keyed; - blist.key = self.key; + blist.key = key; test_log!("result: {:?}", rights); first } @@ -454,8 +471,8 @@ mod feat_hydration { parent: &Element, fragment: &mut Fragment, ) -> Self::Bundle { - let fully_keyed = self.fully_keyed(); - let vchildren = self.children; + let (key, fully_keyed, vchildren) = self.split_for_blist(); + let mut children = Vec::with_capacity(vchildren.len()); for child in vchildren.into_iter() { @@ -469,7 +486,7 @@ mod feat_hydration { BList { rev_children: children, fully_keyed, - key: self.key, + key, } } } diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 0dc2e25f0..fc211ce97 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1,5 +1,6 @@ //! This module contains fragments implementation. use std::ops::{Deref, DerefMut}; +use std::rc::Rc; use super::{Key, VNode}; @@ -14,7 +15,7 @@ enum FullyKeyedState { #[derive(Clone, Debug)] pub struct VList { /// The list of child [VNode]s - pub(crate) children: Vec, + pub(crate) children: Option>>, /// All [VNode]s in the VList have keys fully_keyed: FullyKeyedState, @@ -24,7 +25,15 @@ pub struct VList { impl PartialEq for VList { fn eq(&self, other: &Self) -> bool { - self.children == other.children && self.key == other.key + self.key == other.key + && match (self.children.as_ref(), other.children.as_ref()) { + // We try to use ptr_eq if both are behind Rc, + // Somehow VNode is not Eq? + (Some(l), Some(r)) if Rc::ptr_eq(l, r) => true, + // We fallback to PartialEq if left and right didn't point to the same memory + // address. + (l, r) => l == r, + } } } @@ -38,14 +47,22 @@ impl Deref for VList { type Target = Vec; fn deref(&self) -> &Self::Target { - &self.children + match self.children { + Some(ref m) => m, + None => { + // This is mutable because the Vec is not Sync + static mut EMPTY: Vec = Vec::new(); + // SAFETY: The EMPTY value is always read-only + unsafe { &EMPTY } + } + } } } impl DerefMut for VList { fn deref_mut(&mut self) -> &mut Self::Target { self.fully_keyed = FullyKeyedState::Unknown; - &mut self.children + self.children_mut() } } @@ -53,7 +70,7 @@ impl VList { /// Creates a new empty [VList] instance. pub const fn new() -> Self { Self { - children: Vec::new(), + children: None, key: None, fully_keyed: FullyKeyedState::KnownFullyKeyed, } @@ -63,30 +80,40 @@ impl VList { pub fn with_children(children: Vec, key: Option) -> Self { let mut vlist = VList { fully_keyed: FullyKeyedState::Unknown, - children, + children: Some(Rc::new(children)), key, }; - vlist.fully_keyed = if vlist.fully_keyed() { - FullyKeyedState::KnownFullyKeyed - } else { - FullyKeyedState::KnownMissingKeys - }; + vlist.recheck_fully_keyed(); vlist } + // Returns a mutable reference to children, allocates the children if it hasn't been done. + // + // This method does not reassign key state. So it should only be used internally. + fn children_mut(&mut self) -> &mut Vec { + loop { + match self.children { + Some(ref mut m) => return Rc::make_mut(m), + None => { + self.children = Some(Rc::new(Vec::new())); + } + } + } + } + /// Add [VNode] child. pub fn add_child(&mut self, child: VNode) { if self.fully_keyed == FullyKeyedState::KnownFullyKeyed && !child.has_key() { self.fully_keyed = FullyKeyedState::KnownMissingKeys; } - self.children.push(child); + self.children_mut().push(child); } /// Add multiple [VNode] children. pub fn add_children(&mut self, children: impl IntoIterator) { let it = children.into_iter(); let bound = it.size_hint(); - self.children.reserve(bound.1.unwrap_or(bound.0)); + self.children_mut().reserve(bound.1.unwrap_or(bound.0)); for ch in it { self.add_child(ch); } @@ -108,7 +135,7 @@ impl VList { match self.fully_keyed { FullyKeyedState::KnownFullyKeyed => true, FullyKeyedState::KnownMissingKeys => false, - FullyKeyedState::Unknown => self.children.iter().all(|c| c.has_key()), + FullyKeyedState::Unknown => self.iter().all(|c| c.has_key()), } } } @@ -173,7 +200,7 @@ mod feat_ssr { parent_scope: &AnyScope, hydratable: bool, ) { - match &self.children[..] { + match &self[..] { [] => {} [child] => { child.render_into_stream(w, parent_scope, hydratable).await; @@ -237,7 +264,7 @@ mod feat_ssr { } } - let children = self.children.iter(); + let children = self.iter(); render_child_iter(children, w, parent_scope, hydratable).await; } } diff --git a/tools/benchmark-ssr/src/main.rs b/tools/benchmark-ssr/src/main.rs index 410bbfa68..0add56b3c 100644 --- a/tools/benchmark-ssr/src/main.rs +++ b/tools/benchmark-ssr/src/main.rs @@ -82,6 +82,56 @@ async fn bench_router_app() -> Duration { start_time.elapsed() } +async fn bench_many_providers() -> Duration { + static TOTAL: usize = 250_000; + + #[derive(Properties, PartialEq, Clone)] + struct ProviderProps { + children: Children, + } + + #[function_component] + fn Provider(props: &ProviderProps) -> Html { + let ProviderProps { children } = props.clone(); + + html! {<>{children}} + } + + #[function_component] + fn App() -> Html { + // Let's make 10 providers. + html! { + + + + + + + + + + {"Hello, World!"} + + + + + + + + + + } + } + + let start_time = Instant::now(); + + for _ in 0..TOTAL { + yew::LocalServerRenderer::::new().render().await; + } + + start_time.elapsed() +} + async fn bench_concurrent_task() -> Duration { static TOTAL: usize = 100; @@ -214,12 +264,13 @@ async fn main() { let args = Args::parse(); // Tests in each round. - static TESTS: usize = 4; + static TESTS: usize = 5; let mut baseline_results = Vec::with_capacity(args.rounds); let mut hello_world_results = Vec::with_capacity(args.rounds); let mut function_router_results = Vec::with_capacity(args.rounds); let mut concurrent_tasks_results = Vec::with_capacity(args.rounds); + let mut many_provider_results = Vec::with_capacity(args.rounds); let bar = (!args.no_term).then(|| create_progress(TESTS, args.rounds)); @@ -266,6 +317,14 @@ async fn main() { bar.inc(1); } } + + let dur = bench_many_providers().await; + if i > 0 { + many_provider_results.push(dur); + if let Some(ref bar) = bar { + bar.inc(1); + } + } } }) .await; @@ -280,6 +339,7 @@ async fn main() { Statistics::from_results("Hello World", args.rounds, hello_world_results), Statistics::from_results("Function Router", args.rounds, function_router_results), Statistics::from_results("Concurrent Task", args.rounds, concurrent_tasks_results), + Statistics::from_results("Many Providers", args.rounds, many_provider_results), ]; println!("{}", output.as_ref().table().with(Style::rounded()));