Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make VList's children Rc'ed #3050

Merged
merged 16 commits into from Apr 2, 2023
45 changes: 31 additions & 14 deletions packages/yew/src/dom_bundle/blist.rs
Expand Up @@ -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;

Expand All @@ -22,6 +23,30 @@ pub(super) struct BList {
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 mut fully_keyed = self.fully_keyed();

let mut 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());

if children.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
children.push(VText::new("").into());
fully_keyed = false;
}

(self.key, fully_keyed, children)
}
}

impl Deref for BList {
type Target = Vec<BNode>;

Expand Down Expand Up @@ -421,7 +446,7 @@ impl Reconcilable for VList {
}

fn reconcile(
mut self,
self,
root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
Expand All @@ -436,16 +461,8 @@ 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, fully_keyed, lefts) = self.split_for_blist();

if self.children.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());
}

let fully_keyed = self.fully_keyed();
let lefts = self.children;
let rights = &mut blist.rev_children;
test_log!("lefts: {:?}", lefts);
test_log!("rights: {:?}", rights);
Expand All @@ -459,7 +476,7 @@ impl Reconcilable for VList {
BList::apply_unkeyed(root, parent_scope, parent, next_sibling, lefts, rights)
};
blist.fully_keyed = fully_keyed;
blist.key = self.key;
blist.key = key;
test_log!("result: {:?}", rights);
first
}
Expand All @@ -479,8 +496,8 @@ mod feat_hydration {
fragment: &mut Fragment,
) -> (NodeRef, Self::Bundle) {
let node_ref = NodeRef::default();
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 (index, child) in vchildren.into_iter().enumerate() {
Expand All @@ -500,7 +517,7 @@ mod feat_hydration {
BList {
rev_children: children,
fully_keyed,
key: self.key,
key,
},
)
}
Expand Down
56 changes: 40 additions & 16 deletions 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};

Expand All @@ -14,7 +15,7 @@ enum FullyKeyedState {
#[derive(Clone, Debug)]
pub struct VList {
/// The list of child [VNode]s
pub(crate) children: Vec<VNode>,
pub(crate) children: Option<Rc<Vec<VNode>>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe that's not relevant but I was thinking of using IArray would help here?

I see 2 possible values but I have not investigated in details:

  1. IArray uses Rc<[T]> instead of Rc<Vec<T>>. I guess that would save an allocation somewhere.
  2. IArray has a variant Static(&'static [T]), it can maybe used in place of None here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(The only issue is the hard requirement of VNode to implement ImplicitClone but I think we can allow that because in the end this would be desirable, see discussion #3022)

Copy link
Member Author

@futursolo futursolo Apr 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think VList implements DerefMut<Target = Vec<VNode>> which allows manipulation without cloning the entire array when the reference count is 1 in which we may not be able to keep this behaviour with IArray if it uses slices.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you mention it, I think I had issues when converting the code because of that haha


/// All [VNode]s in the VList have keys
fully_keyed: FullyKeyedState,
Expand All @@ -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,
}
}
}

Expand All @@ -38,22 +47,30 @@ impl Deref for VList {
type Target = Vec<VNode>;

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 }
}
}
Comment on lines +50 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a bit tricky overall. Is it possible to achieve this while using safety?

I'm also not sure I understand why we need an Option around the children type. Probably it is related to this whole thing.

Can't we have a default static value like this?

static EMPTY: Rc<Vec<VNode>> = Rc::new(Vec::new())

(Maybe a thread-local one but you see the idea)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since VNode is !Send, this makes a global static EMPTY: &Vec<VNode>= &Vec::new() inaccessible from all threads. (Rust will not compile this anyways...)

If we use thread_local!, this variable will only live for the period of the thread, not the period of the program, hence not 'static. This means that Rust cannot guarantee the reference will outlive the ownership. (Which is not quite right, since !Send types cannot live longer than the thread it resides on. So this is technically 'static for them.) In this case, I think the only way is to teach the Rust compiler a lesson with some additional knowledge.

image

However, we can still do it with safe Rust by leaking the memory.
If we don't have SSR, this might be the preferred method over unsafe since there is only 1 thread. But we also have SSR these days which will cause leaked memory for each thread ever created.

thread_local! {
    static EMPTY: &'static Vec<VNode> = Box::leak(Box::default());
}

EMPTY.with(|m| *m)

If there is a way to achieve this with safe Rust, I am also very curious to know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a stupid question but... do we actually need to impl Deref and DerefMut on VList??

The only use in its own crate is for this and it's kinda non-brainer to work around:

diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs
index fc211ce9..40c94d5d 100644
--- a/packages/yew/src/virtual_dom/vlist.rs
+++ b/packages/yew/src/virtual_dom/vlist.rs
@@ -43,6 +43,7 @@ impl Default for VList {
     }
 }
 
+/*
 impl Deref for VList {
     type Target = Vec<VNode>;
 
@@ -58,13 +59,16 @@ impl Deref for VList {
         }
     }
 }
+*/
 
+/*
 impl DerefMut for VList {
     fn deref_mut(&mut self) -> &mut Self::Target {
         self.fully_keyed = FullyKeyedState::Unknown;
         self.children_mut()
     }
 }
+*/
 
 impl VList {
     /// Creates a new empty [VList] instance.
@@ -135,7 +139,7 @@ impl VList {
         match self.fully_keyed {
             FullyKeyedState::KnownFullyKeyed => true,
             FullyKeyedState::KnownMissingKeys => false,
-            FullyKeyedState::Unknown => self.iter().all(|c| c.has_key()),
+            FullyKeyedState::Unknown => self.children.iter().flat_map(|x| x.iter()).all(|c| c.has_key()),
         }
     }
 }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the original intention for implementing VList with dereferencing to Vec<VNode> is to allow users use all methods available to a vector to CURD VLists.

E.g.: In bounce, I used VList as a Vec to recursively read all available html to be used with the <head /> element.
https://github.com/bounce-rs/bounce/blob/master/crates/bounce/src/helmet/comp.rs#L27
I didn't use any mutable operations in bounce helmet, but I can imagine a use case where manipulates VList would exist.

}
}

impl DerefMut for VList {
fn deref_mut(&mut self) -> &mut Self::Target {
self.fully_keyed = FullyKeyedState::Unknown;
&mut self.children
self.children_mut()
}
}

impl VList {
/// Creates a new empty [VList] instance.
pub const fn new() -> Self {
Self {
children: Vec::new(),
children: None,
Comment on lines 71 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(To answer my question earlier: why do we need an option around it? It's because we want to make a const fn constructor.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, Rc::new() also results in an allocation, even if the vector is empty. :)

key: None,
fully_keyed: FullyKeyedState::KnownFullyKeyed,
}
Expand All @@ -63,30 +80,37 @@ impl VList {
pub fn with_children(children: Vec<VNode>, key: Option<Key>) -> 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
}

pub(crate) 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.
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<Item = VNode>) {
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);
}
Expand All @@ -108,7 +132,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()),
}
}
}
Expand Down Expand Up @@ -173,7 +197,7 @@ mod feat_ssr {
parent_scope: &AnyScope,
hydratable: bool,
) {
match &self.children[..] {
match &self[..] {
[] => {}
[child] => {
child.render_into_stream(w, parent_scope, hydratable).await;
Expand Down Expand Up @@ -237,7 +261,7 @@ mod feat_ssr {
}
}

let children = self.children.iter();
let children = self.iter();
render_child_iter(children, w, parent_scope, hydratable).await;
}
}
Expand Down
62 changes: 61 additions & 1 deletion tools/benchmark-ssr/src/main.rs
Expand Up @@ -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! {
<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 {
static TOTAL: usize = 100;

Expand Down Expand Up @@ -215,12 +265,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));

Expand Down Expand Up @@ -267,6 +318,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;
Expand All @@ -281,6 +340,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()));
Expand Down