Optimize VTag construction, memory footprint and patching (#1947)

* yew-macro: optimize VTag construction in html! macro

* yew/vtag: decrease VTag memory footpting and construction args

* yew,yew-macro: optimize VTag contruction, memory footprint and diffing

* yew/vlist: revert to VTag boxing

* yew-macro: add clippy allow for nightly rust

* yew-macro:  fix allow namespace

* *: bump MSRV to 1.49.0

* yew/vnode: restore == for VTag comparison

* yew/vtag: clean up reference casting

* yew-macro/html_element: fix error span regression
This commit is contained in:
bakape 2021-07-18 19:26:52 +03:00 committed by GitHub
parent 2412a68bee
commit d89f1ccc5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 839 additions and 466 deletions

View File

@ -113,7 +113,7 @@ jobs:
strategy:
matrix:
toolchain:
- 1.45.0 # MSRV
- 1.49.0 # MSRV
- stable
steps:

View File

@ -36,7 +36,7 @@ run_task = { name = ["lint-flow"], fork = true }
category = "Testing"
description = "Run all tests"
dependencies = ["tests-setup"]
env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"] }
env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*", "**/packages/changelog"] }
run_task = { name = ["test-flow", "doc-test-flow"], fork = true }
[tasks.benchmarks]

View File

@ -12,7 +12,7 @@
<a href="https://docs.rs/yew/"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-yew-green"/></a>
<a href="https://discord.gg/VQck8X4"><img alt="Discord Chat" src="https://img.shields.io/discord/701068342760570933"/></a>
<a href="https://gitlocalize.com/repo/4999/whole_project?utm_source=badge"> <img src="https://gitlocalize.com/repo/4999/whole_project/badge.svg" /> </a>
<a href="https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html"><img alt="Rustc Version 1.45+" src="https://img.shields.io/badge/rustc-1.45%2B-lightgrey.svg"/></a>
<a href="https://blog.rust-lang.org/2020/12/31/Rust-1.49.0.html"><img alt="Rustc Version 1.49.0+" src="https://img.shields.io/badge/rustc-1.49%2B-lightgrey.svg"/></a>
</p>
<h4>

View File

@ -53,9 +53,20 @@ pub fn render_markdown(src: &str) -> Html {
pre.add_child(top.into());
top = pre;
} else if let Tag::Table(aligns) = tag {
for r in top.children.iter_mut() {
for r in top
.children_mut()
.iter_mut()
.map(|ch| ch.iter_mut())
.flatten()
{
if let VNode::VTag(ref mut vtag) = r {
for (i, c) in vtag.children.iter_mut().enumerate() {
for (i, c) in vtag
.children_mut()
.iter_mut()
.map(|ch| ch.iter_mut())
.flatten()
.enumerate()
{
if let VNode::VTag(ref mut vtag) = c {
match aligns[i] {
Alignment::None => {}
@ -68,7 +79,12 @@ pub fn render_markdown(src: &str) -> Html {
}
}
} else if let Tag::TableHead = tag {
for c in top.children.iter_mut() {
for c in top
.children_mut()
.iter_mut()
.map(|ch| ch.iter_mut())
.flatten()
{
if let VNode::VTag(ref mut vtag) = c {
// TODO
// vtag.tag = "th".into();

View File

@ -97,30 +97,10 @@ impl ToTokens for HtmlElement {
children,
} = self;
let name_sr = match &name {
TagName::Lit(name) => name.stringify(),
TagName::Expr(name) => {
let expr = &name.expr;
let vtag_name = Ident::new("__yew_vtag_name", expr.span());
// this way we get a nice error message (with the correct span) when the expression doesn't return a valid value
quote_spanned! {expr.span()=> {
#[allow(unused_braces)]
let mut #vtag_name = ::std::convert::Into::<::std::borrow::Cow::<'static, str>>::into(#expr);
if !#vtag_name.is_ascii() {
::std::panic!("a dynamic tag returned a tag name containing non ASCII characters: `{}`", #vtag_name);
};
// convert to lowercase because the runtime checks rely on it.
#vtag_name.to_mut().make_ascii_lowercase();
#vtag_name
}}
}
};
let ElementProps {
classes,
attributes,
booleans,
kind,
value,
checked,
node_ref,
@ -128,44 +108,44 @@ impl ToTokens for HtmlElement {
listeners,
} = &props;
let vtag = Ident::new("__yew_vtag", name.span());
// attributes with special treatment
let set_node_ref = node_ref.as_ref().map(|attr| {
let value = &attr.value;
quote! {
#vtag.node_ref = #value;
}
});
let set_key = key.as_ref().map(|attr| {
let value = attr.value.optimize_literals();
quote! {
#vtag.__macro_set_key(#value);
}
});
let set_value = value.as_ref().map(|attr| {
let value = attr.value.optimize_literals();
quote! {
#vtag.set_value(#value);
}
});
let set_kind = kind.as_ref().map(|attr| {
let value = attr.value.optimize_literals();
quote! {
#vtag.set_kind(#value);
}
});
let set_checked = checked.as_ref().map(|attr| {
let value = &attr.value;
quote! {
#vtag.set_checked(#value);
}
});
let node_ref = node_ref
.as_ref()
.map(|attr| {
let value = &attr.value;
quote_spanned! {value.span()=>
::yew::html::IntoPropValue::<::yew::html::NodeRef>
::into_prop_value(#value)
}
})
.unwrap_or(quote! { ::std::default::Default::default() });
let key = key
.as_ref()
.map(|attr| {
let value = attr.value.optimize_literals();
quote_spanned! {value.span()=>
::std::option::Option::Some(
::std::convert::Into::<::yew::virtual_dom::Key>::into(#value)
)
}
})
.unwrap_or(quote! { ::std::option::Option::None });
let value = value
.as_ref()
.map(wrap_attr_prop)
.unwrap_or(quote! { ::std::option::Option::None });
let checked = checked
.as_ref()
.map(|attr| {
let value = &attr.value;
quote_spanned! {value.span()=> #value}
})
.unwrap_or(quote! { false });
// other attributes
let set_attributes = {
let attributes = {
let normal_attrs = attributes.iter().map(|Prop { label, value, .. }| {
let key = label.to_lit_str();
let value = value.optimize_literals();
@ -224,12 +204,12 @@ impl ToTokens for HtmlElement {
let attrs = normal_attrs.chain(boolean_attrs).chain(class_attr);
quote! {
#vtag.attributes = ::yew::virtual_dom::Attributes::Vec(::std::vec![#(#attrs),*]);
::yew::virtual_dom::Attributes::Vec(::std::vec![#(#attrs),*])
}
};
let set_listeners = if listeners.is_empty() {
None
let listeners = if listeners.is_empty() {
quote! { ::std::vec![] }
} else {
let listeners_it = listeners.iter().map(|Prop { label, value, .. }| {
let name = &label.name;
@ -238,75 +218,175 @@ impl ToTokens for HtmlElement {
}
});
Some(quote! {
#vtag.__macro_set_listeners(::std::vec![#(#listeners_it),*]);
})
quote! { ::std::vec![#(#listeners_it),*].into_iter().flatten().collect() }
};
let add_children = if children.is_empty() {
None
} else {
Some(quote! {
#[allow(clippy::redundant_clone, unused_braces)]
#vtag.add_children(#children);
})
let child_list = quote! {
::yew::virtual_dom::VList{
key: ::std::option::Option::None,
children: #children,
}
};
// These are the runtime-checks exclusive to dynamic tags.
// For literal tags this is already done at compile-time.
let dyn_tag_runtime_checks = if matches!(&name, TagName::Expr(_)) {
// when Span::source_file Span::start get stabilised or yew-macro introduces a nightly feature flag
// we should expand the panic message to contain the exact location of the dynamic tag.
Some(quote! {
// check void element
if !#vtag.children.is_empty() {
match #vtag.tag() {
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link"
| "meta" | "param" | "source" | "track" | "wbr" => {
::std::panic!("a dynamic tag tried to create a `<{0}>` tag with children. `<{0}>` is a void element which can't have any children.", #vtag.tag());
tokens.extend(match &name {
TagName::Lit(name) => {
let name_span = name.span();
let name = name.to_ascii_lowercase_string();
match &*name {
"input" => {
quote_spanned! {name_span=>
#[allow(clippy::redundant_clone, unused_braces)]
::std::convert::Into::<::yew::virtual_dom::VNode>::into(
::yew::virtual_dom::VTag::__new_input(
#value,
#checked,
#node_ref,
#key,
#attributes,
#listeners,
),
)
}
}
"textarea" => {
quote_spanned! {name_span=>
#[allow(clippy::redundant_clone, unused_braces)]
::std::convert::Into::<::yew::virtual_dom::VNode>::into(
::yew::virtual_dom::VTag::__new_textarea(
#value,
#node_ref,
#key,
#attributes,
#listeners,
),
)
}
_ => {}
}
};
// handle special attribute value
match #vtag.tag() {
"input" | "textarea" => {}
_ => {
let __yew_v = #vtag.value.take();
#vtag.__macro_push_attr(::yew::virtual_dom::PositionalAttr::new("value", __yew_v));
quote_spanned! {name_span=>
#[allow(clippy::redundant_clone, unused_braces)]
::std::convert::Into::<::yew::virtual_dom::VNode>::into(
::yew::virtual_dom::VTag::__new_other(
::std::borrow::Cow::<'static, str>::Borrowed(#name),
#node_ref,
#key,
#attributes,
#listeners,
#child_list,
),
)
}
}
}
})
} else {
None
};
tokens.extend(quote_spanned! {name.span()=>
{
#[allow(unused_braces)]
let mut #vtag = ::yew::virtual_dom::VTag::new(#name_sr);
#set_node_ref
#set_key
#set_value
#set_kind
#set_checked
#set_attributes
#set_listeners
#add_children
#dyn_tag_runtime_checks
{
use ::std::convert::From;
::yew::virtual_dom::VNode::from(#vtag)
}
}
TagName::Expr(name) => {
#[allow(unused_braces)]
let vtag = Ident::new("__yew_vtag", name.span());
let expr = &name.expr;
let vtag_name = Ident::new("__yew_vtag_name", expr.span());
// handle special attribute value
let handle_value_attr = props.value.as_ref().map(|prop| {
let v = prop.value.optimize_literals();
quote_spanned! {v.span()=> {
__yew_vtag.__macro_push_attr(
::yew::virtual_dom::PositionalAttr::new("value", #v),
);
}}
});
// this way we get a nice error message (with the correct span) when the expression
// doesn't return a valid value
quote_spanned! {expr.span()=> {
let mut #vtag_name = ::std::convert::Into::<
::std::borrow::Cow::<'static, str>
>::into(#expr);
if !#vtag_name.is_ascii() {
::std::panic!(
"a dynamic tag returned a tag name containing non ASCII characters: `{}`",
#vtag_name,
);
}
// convert to lowercase because the runtime checks rely on it.
#vtag_name.to_mut().make_ascii_lowercase();
#[allow(clippy::redundant_clone, unused_braces, clippy::let_and_return)]
let mut #vtag = match ::std::convert::AsRef::<str>::as_ref(&#vtag_name) {
"input" => {
::yew::virtual_dom::VTag::__new_textarea(
#value,
#node_ref,
#key,
#attributes,
#listeners,
)
}
"textarea" => {
::yew::virtual_dom::VTag::__new_textarea(
#value,
#node_ref,
#key,
#attributes,
#listeners,
)
}
_ => {
let mut __yew_vtag = ::yew::virtual_dom::VTag::__new_other(
#vtag_name,
#node_ref,
#key,
#attributes,
#listeners,
#child_list,
);
#handle_value_attr
__yew_vtag
}
};
// These are the runtime-checks exclusive to dynamic tags.
// For literal tags this is already done at compile-time.
//
// When Span::source_file Span::start get stabilised or yew-macro introduces a
// nightly feature flag we should expand the panic message to contain the exact
// location of the dynamic tag.
//
// check void element
if !#vtag.children().is_empty() {
match #vtag.tag() {
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input"
| "link" | "meta" | "param" | "source" | "track" | "wbr"
=> {
::std::panic!(
"a dynamic tag tried to create a `<{0}>` tag with children. `<{0}>` is a void element which can't have any children.",
#vtag.tag(),
);
}
_ => {}
}
}
::std::convert::Into::<::yew::virtual_dom::VNode>::into(#vtag)
}}
}
});
}
}
fn wrap_attr_prop(prop: &Prop) -> TokenStream {
let value = prop.value.optimize_literals();
quote_spanned! {value.span()=>
::yew::html::IntoPropValue::<
::std::option::Option<
::yew::virtual_dom::AttrValue
>
>
::into_prop_value(#value)
}
}
struct DynamicName {
at: Token![@],
expr: Option<Block>,

View File

@ -23,7 +23,6 @@ pub struct ElementProps {
pub classes: Option<ClassesForm>,
pub booleans: Vec<Prop>,
pub value: Option<Prop>,
pub kind: Option<Prop>,
pub checked: Option<Prop>,
pub node_ref: Option<Prop>,
pub key: Option<Prop>,
@ -46,7 +45,6 @@ impl Parse for ElementProps {
.pop("class")
.map(|prop| ClassesForm::from_expr(prop.value));
let value = props.pop("value");
let kind = props.pop("type");
let checked = props.pop("checked");
let SpecialProps { node_ref, key } = props.special;
@ -58,7 +56,6 @@ impl Parse for ElementProps {
checked,
booleans: booleans.into_vec(),
value,
kind,
node_ref,
key,
})

View File

@ -185,12 +185,19 @@ error[E0277]: the trait bound `(): IntoPropValue<Option<Cow<'static, str>>>` is
|
43 | html! { <input type=() /> };
| ^^ the trait `IntoPropValue<Option<Cow<'static, str>>>` is not implemented for `()`
|
::: $WORKSPACE/packages/yew/src/virtual_dom/mod.rs
|
| pub fn new(key: &'static str, value: impl IntoPropValue<Option<AttrValue>>) -> Self {
| -------------------------------- required by this bound in `PositionalAttr::new`
error[E0277]: the trait bound `(): IntoPropValue<Option<Cow<'static, str>>>` is not satisfied
--> $DIR/element-fail.rs:44:26
|
44 | html! { <input value=() /> };
| ^^ the trait `IntoPropValue<Option<Cow<'static, str>>>` is not implemented for `()`
|
= note: required by `into_prop_value`
error[E0277]: the trait bound `(): IntoPropValue<Option<Cow<'static, str>>>` is not satisfied
--> $DIR/element-fail.rs:45:21
@ -309,20 +316,25 @@ error[E0277]: the trait bound `Option<{integer}>: IntoPropValue<Option<yew::Call
<Option<&'static str> as IntoPropValue<Option<String>>>
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
error[E0308]: mismatched types
error[E0277]: the trait bound `(): IntoPropValue<yew::NodeRef>` is not satisfied
--> $DIR/element-fail.rs:56:24
|
56 | html! { <input ref=() /> };
| ^^ expected struct `yew::NodeRef`, found `()`
| ^^ the trait `IntoPropValue<yew::NodeRef>` is not implemented for `()`
|
= note: required by `into_prop_value`
error[E0308]: mismatched types
error[E0277]: the trait bound `Option<yew::NodeRef>: IntoPropValue<yew::NodeRef>` is not satisfied
--> $DIR/element-fail.rs:57:24
|
57 | html! { <input ref=Some(NodeRef::default()) /> };
| ^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `yew::NodeRef`, found enum `Option`
| ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoPropValue<yew::NodeRef>` is not implemented for `Option<yew::NodeRef>`
|
= note: expected struct `yew::NodeRef`
found enum `Option<yew::NodeRef>`
= help: the following implementations were found:
<Option<&'static str> as IntoPropValue<Option<Cow<'static, str>>>>
<Option<&'static str> as IntoPropValue<Option<String>>>
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
= note: required by `into_prop_value`
error[E0277]: the trait bound `Cow<'static, str>: From<{integer}>` is not satisfied
--> $DIR/element-fail.rs:71:15

View File

@ -21,6 +21,7 @@ pub trait IntoPropValue<T> {
}
impl<T> IntoPropValue<T> for T {
#[inline]
fn into_prop_value(self) -> T {
self
}
@ -29,12 +30,14 @@ impl<T> IntoPropValue<T> for &T
where
T: ImplicitClone,
{
#[inline]
fn into_prop_value(self) -> T {
self.clone()
}
}
impl<T> IntoPropValue<Option<T>> for T {
#[inline]
fn into_prop_value(self) -> Option<T> {
Some(self)
}
@ -43,6 +46,7 @@ impl<T> IntoPropValue<Option<T>> for &T
where
T: ImplicitClone,
{
#[inline]
fn into_prop_value(self) -> Option<T> {
Some(self.clone())
}
@ -52,6 +56,7 @@ macro_rules! impl_into_prop {
(|$value:ident: $from_ty:ty| -> $to_ty:ty { $conversion:expr }) => {
// implement V -> T
impl IntoPropValue<$to_ty> for $from_ty {
#[inline]
fn into_prop_value(self) -> $to_ty {
let $value = self;
$conversion
@ -59,6 +64,7 @@ macro_rules! impl_into_prop {
}
// implement V -> Option<T>
impl IntoPropValue<Option<$to_ty>> for $from_ty {
#[inline]
fn into_prop_value(self) -> Option<$to_ty> {
let $value = self;
Some({ $conversion })
@ -66,6 +72,7 @@ macro_rules! impl_into_prop {
}
// implement Option<V> -> Option<T>
impl IntoPropValue<Option<$to_ty>> for Option<$from_ty> {
#[inline]
fn into_prop_value(self) -> Option<$to_ty> {
self.map(IntoPropValue::into_prop_value)
}

View File

@ -26,6 +26,7 @@ macro_rules! impl_action {
}
#[doc(hidden)]
#[inline]
pub fn __macro_new(callback: impl IntoPropValue<Option<Callback<Event>>>) -> Option<Rc<dyn Listener>> {
let callback = callback.into_prop_value()?;
Some(Rc::new(Self::new(callback)))

View File

@ -16,7 +16,7 @@ pub mod vtext;
use crate::html::{AnyScope, IntoPropValue, NodeRef};
use gloo::events::EventListener;
use indexmap::IndexMap;
use std::{borrow::Cow, collections::HashMap, fmt, hint::unreachable_unchecked, iter, mem, rc::Rc};
use std::{borrow::Cow, collections::HashMap, fmt, hint::unreachable_unchecked, iter, mem};
use web_sys::{Element, Node};
#[doc(inline)]
@ -47,23 +47,34 @@ impl fmt::Debug for dyn Listener {
}
}
/// A list of event listeners.
type Listeners = Vec<Rc<dyn Listener>>;
/// Attribute value
pub type AttrValue = Cow<'static, str>;
/// Applies contained changes to DOM [Element]
trait Apply {
/// [Element] type to apply the changes to
type Element;
/// Apply contained values to [Element] with no ancestor
fn apply(&mut self, el: &Self::Element);
/// Apply diff between [self] and `ancestor` to [Element].
fn apply_diff(&mut self, el: &Self::Element, ancestor: Self);
}
/// Key-value tuple which makes up an item of the [`Attributes::Vec`] variant.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PositionalAttr(pub &'static str, pub Option<AttrValue>);
impl PositionalAttr {
/// Create a positional attribute
#[inline]
pub fn new(key: &'static str, value: impl IntoPropValue<Option<AttrValue>>) -> Self {
Self(key, value.into_prop_value())
}
/// Create a boolean attribute.
/// `present` controls whether the attribute is added
#[inline]
pub fn new_boolean(key: &'static str, present: bool) -> Self {
let value = if present {
Some(Cow::Borrowed(key))
@ -74,15 +85,18 @@ impl PositionalAttr {
}
/// Create a placeholder for removed attributes
#[inline]
pub fn new_placeholder(key: &'static str) -> Self {
Self(key, None)
}
#[inline]
fn transpose(self) -> Option<(&'static str, AttrValue)> {
let Self(key, value) = self;
value.map(|v| (key, v))
}
#[inline]
fn transposed<'a>(&'a self) -> Option<(&'static str, &'a AttrValue)> {
let Self(key, value) = self;
value.as_ref().map(|v| (*key, v))
@ -298,6 +312,46 @@ impl Attributes {
}
}
}
fn set_attribute(el: &Element, key: &str, value: &str) {
el.set_attribute(&key, &value)
.expect("invalid attribute key")
}
}
impl Apply for Attributes {
type Element = Element;
fn apply(&mut self, el: &Element) {
match self {
Self::Vec(v) => {
for attr in v.iter() {
if let Some(v) = &attr.1 {
Self::set_attribute(el, &attr.0, v)
}
}
}
Self::IndexMap(m) => {
for (k, v) in m.iter() {
Self::set_attribute(el, k, v)
}
}
}
}
fn apply_diff(&mut self, el: &Element, ancestor: Self) {
for change in Self::diff(self, &ancestor) {
match change {
Patch::Add(key, value) | Patch::Replace(key, value) => {
Self::set_attribute(el, key, value);
}
Patch::Remove(key) => {
el.remove_attribute(&key)
.expect("could not remove attribute");
}
}
}
}
}
impl From<Vec<PositionalAttr>> for Attributes {

View File

@ -39,8 +39,7 @@ impl VNode {
pub(crate) fn first_node(&self) -> Node {
match self {
VNode::VTag(vtag) => vtag
.reference
.as_ref()
.reference()
.expect("VTag is not mounted")
.clone()
.into(),
@ -133,24 +132,28 @@ impl Default for VNode {
}
impl From<VText> for VNode {
#[inline]
fn from(vtext: VText) -> Self {
VNode::VText(vtext)
}
}
impl From<VList> for VNode {
#[inline]
fn from(vlist: VList) -> Self {
VNode::VList(vlist)
}
}
impl From<VTag> for VNode {
#[inline]
fn from(vtag: VTag) -> Self {
VNode::VTag(Box::new(vtag))
}
}
impl From<VComp> for VNode {
#[inline]
fn from(vcomp: VComp) -> Self {
VNode::VComp(vcomp)
}
@ -173,11 +176,10 @@ impl<T: ToString> From<T> for VNode {
impl<A: Into<VNode>> FromIterator<A> for VNode {
fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
let vlist = iter.into_iter().fold(VList::default(), |mut acc, x| {
acc.add_child(x.into());
acc
});
VNode::VList(vlist)
VNode::VList(VList {
key: None,
children: iter.into_iter().map(|n| n.into()).collect(),
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ Your local development environment will need a couple of tools to compile, build
To install Rust follow the [official instructions](https://www.rust-lang.org/tools/install).
:::important
The minimum supported Rust version (MSRV) for Yew is `1.45.0`. Older versions can cause unexpected issues accompanied by incomprehensible error messages.
The minimum supported Rust version (MSRV) for Yew is `1.49.0`. Older versions can cause unexpected issues accompanied by incomprehensible error messages.
You can check your toolchain version using `rustup show` (under "active toolchain") or alternatively `rustc --version`. To update your toolchain, run `rustup update`.
:::

View File

@ -12,7 +12,7 @@ You also need to install the `wasm32-unknown-unknown` target to compile Rust to
If you're using rustup, you just need to run `rustup target add wasm32-unknown-unknown`.
:::important
The minimum supported Rust version (MSRV) for Yew is `1.45.0`. Older versions can cause unexpected issues accompanied by incomprehensible error messages.
The minimum supported Rust version (MSRV) for Yew is `1.49.0`. Older versions can cause unexpected issues accompanied by incomprehensible error messages.
You can check your toolchain version using `rustup show` (under "active toolchain") or alternatively `rustc --version`. To update your toolchain, run `rustup update`.
:::