Make VList's children Rc'ed (#3050)

* Make VList's children Rc'ed.

* Slightly optimise for bundle size.

* Revert manual cloning.

* Add a benchmark.

* Revert "Revert manual cloning."

This reverts commit 1f737f26f85a13e49186df71eb41425387f3aa02.

* Revert "Slightly optimise for bundle size."

This reverts commit 71cf2a1d36f1a99463c68aae3d128aa3be93dc03.

* Add size marker.

* Revert "Add size marker."

This reverts commit 3ca55be4ea309f516d91ecd8e2aa8256dd810d11.

* Update benchmark.

* Fix children_mut visibility.

* Try to exclude add_child behaviour.

* Fix typo.

* Adjust visibility and docs.

* Fix hydration with empty children.
This commit is contained in:
Kaede Hoshikawa 2023-04-02 23:51:59 +09:00 committed by GitHub
parent 86b6cb4949
commit 9d7fafa3ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 130 additions and 26 deletions

View File

@ -4,6 +4,7 @@ use std::cmp::Ordering;
use std::collections::HashSet; use std::collections::HashSet;
use std::hash::Hash; use std::hash::Hash;
use std::ops::Deref; use std::ops::Deref;
use std::rc::Rc;
use web_sys::Element; use web_sys::Element;
@ -22,6 +23,22 @@ pub(super) struct BList {
key: Option<Key>, key: Option<Key>,
} }
impl VList {
// Splits a VList for creating / reconciling to a BList.
fn split_for_blist(self) -> (Option<Key>, bool, Vec<VNode>) {
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 { impl Deref for BList {
type Target = Vec<BNode>; type Target = Vec<BNode>;
@ -397,7 +414,7 @@ impl Reconcilable for VList {
} }
fn reconcile( fn reconcile(
mut self, self,
root: &BSubtree, root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
@ -412,16 +429,16 @@ impl Reconcilable for VList {
// The left items are known since we want to insert them // The left items are known since we want to insert them
// (self.children). For the right ones, we will look at the bundle, // (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. // 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 // Without a placeholder the next element becomes first
// and corrupts the order of rendering // and corrupts the order of rendering
// We use empty text element to stake out a place // 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; let rights = &mut blist.rev_children;
test_log!("lefts: {:?}", lefts); test_log!("lefts: {:?}", lefts);
test_log!("rights: {:?}", rights); test_log!("rights: {:?}", rights);
@ -435,7 +452,7 @@ impl Reconcilable for VList {
BList::apply_unkeyed(root, parent_scope, parent, slot, lefts, rights) BList::apply_unkeyed(root, parent_scope, parent, slot, lefts, rights)
}; };
blist.fully_keyed = fully_keyed; blist.fully_keyed = fully_keyed;
blist.key = self.key; blist.key = key;
test_log!("result: {:?}", rights); test_log!("result: {:?}", rights);
first first
} }
@ -454,8 +471,8 @@ mod feat_hydration {
parent: &Element, parent: &Element,
fragment: &mut Fragment, fragment: &mut Fragment,
) -> Self::Bundle { ) -> Self::Bundle {
let fully_keyed = self.fully_keyed(); let (key, fully_keyed, vchildren) = self.split_for_blist();
let vchildren = self.children;
let mut children = Vec::with_capacity(vchildren.len()); let mut children = Vec::with_capacity(vchildren.len());
for child in vchildren.into_iter() { for child in vchildren.into_iter() {
@ -469,7 +486,7 @@ mod feat_hydration {
BList { BList {
rev_children: children, rev_children: children,
fully_keyed, fully_keyed,
key: self.key, key,
} }
} }
} }

View File

@ -1,5 +1,6 @@
//! This module contains fragments implementation. //! This module contains fragments implementation.
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use super::{Key, VNode}; use super::{Key, VNode};
@ -14,7 +15,7 @@ enum FullyKeyedState {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct VList { pub struct VList {
/// The list of child [VNode]s /// The list of child [VNode]s
pub(crate) children: Vec<VNode>, pub(crate) children: Option<Rc<Vec<VNode>>>,
/// All [VNode]s in the VList have keys /// All [VNode]s in the VList have keys
fully_keyed: FullyKeyedState, fully_keyed: FullyKeyedState,
@ -24,7 +25,15 @@ pub struct VList {
impl PartialEq for VList { impl PartialEq for VList {
fn eq(&self, other: &Self) -> bool { 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<VNode>; type Target = Vec<VNode>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.children match self.children {
Some(ref m) => m,
None => {
// This is mutable because the Vec<VNode> is not Sync
static mut EMPTY: Vec<VNode> = Vec::new();
// SAFETY: The EMPTY value is always read-only
unsafe { &EMPTY }
}
}
} }
} }
impl DerefMut for VList { impl DerefMut for VList {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
self.fully_keyed = FullyKeyedState::Unknown; self.fully_keyed = FullyKeyedState::Unknown;
&mut self.children self.children_mut()
} }
} }
@ -53,7 +70,7 @@ impl VList {
/// Creates a new empty [VList] instance. /// Creates a new empty [VList] instance.
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
children: Vec::new(), children: None,
key: None, key: None,
fully_keyed: FullyKeyedState::KnownFullyKeyed, fully_keyed: FullyKeyedState::KnownFullyKeyed,
} }
@ -63,30 +80,40 @@ impl VList {
pub fn with_children(children: Vec<VNode>, key: Option<Key>) -> Self { pub fn with_children(children: Vec<VNode>, key: Option<Key>) -> Self {
let mut vlist = VList { let mut vlist = VList {
fully_keyed: FullyKeyedState::Unknown, fully_keyed: FullyKeyedState::Unknown,
children, children: Some(Rc::new(children)),
key, key,
}; };
vlist.fully_keyed = if vlist.fully_keyed() { vlist.recheck_fully_keyed();
FullyKeyedState::KnownFullyKeyed
} else {
FullyKeyedState::KnownMissingKeys
};
vlist 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<VNode> {
loop {
match self.children {
Some(ref mut m) => return Rc::make_mut(m),
None => {
self.children = Some(Rc::new(Vec::new()));
}
}
}
}
/// Add [VNode] child. /// Add [VNode] child.
pub fn add_child(&mut self, child: VNode) { pub fn add_child(&mut self, child: VNode) {
if self.fully_keyed == FullyKeyedState::KnownFullyKeyed && !child.has_key() { if self.fully_keyed == FullyKeyedState::KnownFullyKeyed && !child.has_key() {
self.fully_keyed = FullyKeyedState::KnownMissingKeys; self.fully_keyed = FullyKeyedState::KnownMissingKeys;
} }
self.children.push(child); self.children_mut().push(child);
} }
/// Add multiple [VNode] children. /// Add multiple [VNode] children.
pub fn add_children(&mut self, children: impl IntoIterator<Item = VNode>) { pub fn add_children(&mut self, children: impl IntoIterator<Item = VNode>) {
let it = children.into_iter(); let it = children.into_iter();
let bound = it.size_hint(); 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 { for ch in it {
self.add_child(ch); self.add_child(ch);
} }
@ -108,7 +135,7 @@ impl VList {
match self.fully_keyed { match self.fully_keyed {
FullyKeyedState::KnownFullyKeyed => true, FullyKeyedState::KnownFullyKeyed => true,
FullyKeyedState::KnownMissingKeys => false, 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, parent_scope: &AnyScope,
hydratable: bool, hydratable: bool,
) { ) {
match &self.children[..] { match &self[..] {
[] => {} [] => {}
[child] => { [child] => {
child.render_into_stream(w, parent_scope, hydratable).await; 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; render_child_iter(children, w, parent_scope, hydratable).await;
} }
} }

View File

@ -82,6 +82,56 @@ async fn bench_router_app() -> Duration {
start_time.elapsed() 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! {
<Provider>
<Provider>
<Provider>
<Provider>
<Provider>
<Provider>
<Provider>
<Provider>
<Provider>
<Provider>{"Hello, World!"}</Provider>
</Provider>
</Provider>
</Provider>
</Provider>
</Provider>
</Provider>
</Provider>
</Provider>
</Provider>
}
}
let start_time = Instant::now();
for _ in 0..TOTAL {
yew::LocalServerRenderer::<App>::new().render().await;
}
start_time.elapsed()
}
async fn bench_concurrent_task() -> Duration { async fn bench_concurrent_task() -> Duration {
static TOTAL: usize = 100; static TOTAL: usize = 100;
@ -214,12 +264,13 @@ async fn main() {
let args = Args::parse(); let args = Args::parse();
// Tests in each round. // Tests in each round.
static TESTS: usize = 4; static TESTS: usize = 5;
let mut baseline_results = Vec::with_capacity(args.rounds); let mut baseline_results = Vec::with_capacity(args.rounds);
let mut hello_world_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 function_router_results = Vec::with_capacity(args.rounds);
let mut concurrent_tasks_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)); let bar = (!args.no_term).then(|| create_progress(TESTS, args.rounds));
@ -266,6 +317,14 @@ async fn main() {
bar.inc(1); 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; .await;
@ -280,6 +339,7 @@ async fn main() {
Statistics::from_results("Hello World", args.rounds, hello_world_results), Statistics::from_results("Hello World", args.rounds, hello_world_results),
Statistics::from_results("Function Router", args.rounds, function_router_results), Statistics::from_results("Function Router", args.rounds, function_router_results),
Statistics::from_results("Concurrent Task", args.rounds, concurrent_tasks_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())); println!("{}", output.as_ref().table().with(Style::rounded()));