-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
yew-macro
: optimise macros' memory usage
#3845
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
base: master
Are you sure you want to change the base?
yew-macro
: optimise macros' memory usage
#3845
Conversation
Size Comparison
✅ None of the examples has changed their size significantly. |
Visit the preview URL for this PR (updated for commit 40e4caa): https://yew-rs-api--pr3845-remove-to-string-cal-xve8iwlv.web.app (expires Sat, 26 Apr 2025 01:03:42 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 |
Benchmark - SSRYew Master
Pull Request
|
e233af2
to
788cc44
Compare
- Removed most of `.to_string()` calls - Factored out the check for a component-like name
788cc44
to
40e4caa
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR optimizes macro memory usage in the yew-macro crate by eliminating many unnecessary allocations using the new DisplayExt trait methods. Key changes include:
- Replacing .to_string() comparisons with non-allocating DisplayExt methods (repr_eq, repr_eq_ignore_ascii_case, etc.).
- Removing the custom non_capitalized_ascii helper in favor of a method on Ident and similar types.
- Updating diff files across props, html_tree, hook, and function component modules to use the new DisplayExt APIs.
Reviewed Changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
packages/yew-macro/src/props/prop_macro.rs | Added import for DisplayExt and updated associated properties checking logic. |
packages/yew-macro/src/props/prop.rs | Replaced .to_string() comparisons with DisplayExt's repr_eq methods. |
packages/yew-macro/src/lib.rs | Removed non_capitalized_ascii and added DisplayExt trait implementation. |
packages/yew-macro/src/html_tree/tag.rs | Updated error replacement to pass the original error instead of its string version. |
packages/yew-macro/src/html_tree/mod.rs | Simplified imports and replaced non_capitalized_ascii with DisplayExt methods. |
packages/yew-macro/src/html_tree/lint/mod.rs | Updated attribute comparisons using repr_eq_ignore_ascii_case. |
packages/yew-macro/src/html_tree/html_node.rs | Updated logic to use DisplayExt for literal comparisons. |
packages/yew-macro/src/html_tree/html_element.rs | Replaced string comparisons with DisplayExt methods and cleaned up related logic. |
packages/yew-macro/src/html_tree/html_dashed_name.rs | Removed redundant eq_ignore_ascii_case helper in favor of DisplayExt-based checks. |
packages/yew-macro/src/html_tree/html_component.rs | Added is_component_name helper using DisplayExt, and updated component name logic. |
packages/yew-macro/src/hook/mod.rs | Updated hook naming checks to use starts_with directly on Identifier. |
packages/yew-macro/src/hook/body.rs | Replaced to_string() checks with non-allocating starts_with comparisons. |
packages/yew-macro/src/function_component.rs | Updated attribute filtering using DisplayExt methods for consistency. |
Comments suppressed due to low confidence (1)
packages/yew-macro/src/lib.rs:88
- Consider adding unit tests for the new DisplayExt methods to verify their correctness and prevent regressions.
trait DisplayExt: Display {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you've done any measurement on performance impact, feel free to add this to the PR, but since you already did the work, I still nevertheless think it's a nice idea that can serve some purpose. (in general more performance is left on the table in yew-macro by quoting, then parsing and quoting again into different token streams but that's a different issue).
Our CI doesn't measure anything inside the macro or during compilation, only of the resulting binaries, so will be a bit useless here for performance.
is_ide_completion: is_ide_completion(), | ||
empty: true, | ||
}; | ||
write!(writer, "{s}").is_ok_and(|_| !writer.empty) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This deserves a better comment and improved naming, as it is very hard to follow currently.
A name is, at the moment, assumed to be a component name if it
- starts with an ascii uppercase letter
- OR ide completion is enable and it contains any ascii uppercase
Both of these are rough approximations, for example I think the second condition could be changed to match a specific identifier passed via the RUST_IDE_PROC_MACRO_COMPLETION_DUMMY_IDENTIFIER
env variable, and I'm not even sure how much adoption that experiment got at this point.
/// Needed to check the plentiful token-like values in the impl of the macros, which are | ||
/// `Display`able but which either correspond to multiple source code tokens, or are themselves | ||
/// tokens that don't provide a reference to their repr. | ||
trait DisplayExt: Display { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea! But alas, the current implementation is hard to read when you first see it.
Let me phrase the idea differently. We use write!
to feed chunks to an accumulator instead of writing them into a new allocation via to_string
. This could be more clear, in my opinion, if the implementations would go through a common method expressing this.
/// A pattern can consume successive chunks of a String to determine
// if it matches, until the end of string is reached.
trait Pattern {
type Match; // bool for all in this PR. Could one imagine more uses?
// Returns a Result to enable short-curcuit. Another idea would be std::ops::ControlFlow<Self::Match>
fn feed_chunk(&mut self, chunk: &str) -> Result<(), Self::Match>;
fn end_of_string(self) -> Self::Match;
}
// Or add this as a method to DisplayExt
fn match_repr<P: Pattern>(this: impl Display, pattern: P) -> P::Match {
struct PatternWrite<P: Pattern> { pattern: P, r#match: Option<P::Match> }
impl<P: Pattern> Write for PatternWrite<P> {
fn write_str(&mut self, chunk: &str) -> std::fmt::Result {
debug_assert!(self.r#match.is_none());
self.pattern.feed_chunk(chunk).map_err(|m| {
self.r#match = Some(m);
std::fmt::Error // Short-curcuit further chunks
})
}
}
let mut writer = PatternWrite { pattern, r#match: None };
match write!(writer, "{this}") {
Ok(()) => writer.pattern.end_of_string(),
Err(_) => writer.r#match.unwrap(),
}
}
With this machinery in place, I think a lot of the checks become a lot more digestible. For example
fn split_off_start<'s>(s: &mut &'s str, mid: usize) -> Option<&'s str> {
if mid <= s.len() {
let start;
(start, *s) = s.split_at(mid);
Some(start)
} else {
None
}
}
struct EqualsIgnoreAsciiCase<'src>(&'src str);
impl Pattern for EqualsIgnoreAsciiCase<'_> {
type Match = bool;
fn feed_chunk(&mut self, chunk: &str) -> Result<(), bool> {
match split_off_start(&mut self.0, chunk.len()) {
Some(start) if start.eq_ignore_ascii_case(chunk) => Ok(()),
_ => Err(false),
}
}
fn end_of_string(self) -> bool {
self.0.is_empty()
}
}
struct EqualsExact<'src>(&'src str);
impl Pattern for EqualsExact<'_> {
type Match = bool;
fn feed_chunk(&mut self, chunk: &str) -> Result<(), bool> {
match split_off_start(&mut self.0, chunk.len()) {
Some(start) if chunk == start => Ok(()),
_ => Err(false),
}
}
fn end_of_string(self) -> bool {
self.0.is_empty()
}
}
trait DisplayExt: Display {
fn repr_eq_ignore_ascii_case(&self, pattern: &str) -> bool {
match_repr(self, EqualsIgnoreAsciiCase(pattern))
}
// Etc...
}
|
||
/// Equivalent of [`str::starts_with`], but works for anything that's `Display` without | ||
/// allocations | ||
fn starts_with(&self, prefix: &str) -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this method not use the repr_
prefix like the ones above, or rather, I think without prefix would be clearer. I would call the above methods
repr_eq_ignore_ascii_case
->eq_str_ignore_ascii_case
repr_eq
->eq_str
Description
Removes most of
.to_string()
calls from the impl ofyew-macro
by utilising theDisplay
impls of tokens & AST values, analysing them chunk by chunk instead of allocating all of them into a stringChecklist