Repository navigation
Conversation
|
@tstreule Can you verify that your change aligns with Servo AI contribution policy: https://book.servo.org/policy/ai-usage.html? In short, LLMs are fine for preparation / investigation / review, but no generated code, PR descriptions or comments are allowed. |
|
No AI was used for most of the work, like code and testing. However, I used AI to rename variables and clean up the PR description. If you like, I can gladly rewrite the PR description. Sorry for any inconvenience! |
Thanks! Yes, if you don't mind please rewrite the description in your own words. |
The tokenizer compared each attribute name with every earlier one on the tag, so a tag with n attributes cost O(n^2): 80,000 attributes took about 1.3 s to tokenize. From ten attributes on, look the name up in a set of the tag's names instead, as Blink does; below that, the scan is cheaper than hashing. Which attribute wins, the parse error and `had_duplicate_attributes` stay the same. Fixes servo#796. Part of servo#289.
d963a6a to
6566229
Compare
@mrobinson Sorry that it took a bit longer. Should be ready for your review now. |
|
Found this same issue, adding timing information for reference: On html5ever 0.39.0, a single ~1 MiB end tag carrying ~150,000 distinct attribute names did not finish within 25 s. End tags go through the same With a per-tag |
| fn is_duplicate_attribute(&self, name: &LocalName) -> bool { | ||
| let attrs = self.current_tag_attrs.borrow(); | ||
| if attrs.len() < MIN_ATTRS_TO_DEDUP_WITH_HASH { | ||
| return attrs.iter().any(|a| a.name.local == *name); | ||
| } | ||
| let mut names = self.current_tag_attr_names.borrow_mut(); | ||
| if names.is_empty() { | ||
| names.extend(attrs.iter().map(|a| a.name.local.clone())); | ||
| } | ||
| !names.insert(name.clone()) | ||
| } |
There was a problem hiding this comment.
It's not a good idea to name the function is_duplicate_attribute when it's mutating a data structure. The name makes it sound like it's idempotent, but it isn't.
Instead, I'd structure this method around appending to the list:
| fn is_duplicate_attribute(&self, name: &LocalName) -> bool { | |
| let attrs = self.current_tag_attrs.borrow(); | |
| if attrs.len() < MIN_ATTRS_TO_DEDUP_WITH_HASH { | |
| return attrs.iter().any(|a| a.name.local == *name); | |
| } | |
| let mut names = self.current_tag_attr_names.borrow_mut(); | |
| if names.is_empty() { | |
| names.extend(attrs.iter().map(|a| a.name.local.clone())); | |
| } | |
| !names.insert(name.clone()) | |
| } | |
| fn add_to_current_tag_attrs(&self, name: &LocalName) -> bool { | |
| let mut attrs = self.current_tag_attrs.borrow_mut(); | |
| let should_add = | |
| if attrs.len() < MIN_ATTRS_TO_DEDUP_WITH_HASH { | |
| !attrs.iter().any(|a| a.name.local == *name) | |
| } else { | |
| let mut names = self.current_tag_attr_names.borrow_mut(); | |
| if names.is_empty() { | |
| names.extend(attrs.iter().map(|a| a.name.local.clone())); | |
| } | |
| names.insert(name.clone()) | |
| }; | |
| if should_add { | |
| attrs.push(Attribute { | |
| // The tree builder will adjust the namespace if necessary. | |
| // This only happens in foreign elements. | |
| name: QualName::new(None, ns!(), name), | |
| value: mem::take(&mut self.current_attr_value.borrow_mut()), | |
| }) | |
| } | |
| should_add | |
| } |
Then the spot where it's actually used becomes:
if !self.add_to_current_tag_attrs(&name) {
self.emit_error(Borrowed("Duplicate attribute"));
self.current_tag_had_duplicate_attributes.set(true);
self.current_attr_value.borrow_mut().clear();
}
Fixes #796. Part of #289.
Problem
Until now
Tokenizer::finish_attributescanned every attribute already on the tag for each new name. So one tag with n attributes costs O(n²).Solution
Following what's proposed in #796, we adjust the tokenizer to mirror how Blink does it (
kMinimumNumAttributesToDedupWithHashinatomic_html_token.h):HashSet<LocalName>), one step regardless of how many attributes there are.Changed.
The tokenizer in
html5ever/src/tokenizer/mod.rsgets a new fieldcurrent_tag_attr_names: RefCell<HashSet<LocalName>>and methodis_duplicate_attribute, which returns whether the tag already has an attribute with that name.Behavior is unchanged: the first occurrence wins, later ones are dropped with the
Duplicate attributeparse error, andhad_duplicate_attributesis set.Tests
Tags with fewer than 10 attributes still take the linear scan, which the html5lib tokenizer tests already cover. Four new unit tests in
tokenizer::testcover the set:duplicate_of_an_attribute_before_the_threshold: a duplicate ofa0on tags with 9, 10 and 11 attributes. From 10 on,a0is found among the names put into the set when it is first filled.duplicate_of_an_attribute_after_the_threshold:id=first id=secondafter 12 attributes.idis found among the names added to the set one by one.no_duplicates_on_a_next_tag_with_the_same_attributes: two tags with the same 12 attributes report no duplicates, so the set is cleared between tags.duplicate_on_a_next_tag_with_other_attributes: a duplicate on a second tag with other names is still found, so the set is filled again for each tag.They check the kept attributes, the
Duplicate attributeerrors andhad_duplicate_attributes. The threshold is a module-level constant,MIN_ATTRS_TO_DEDUP_WITH_HASH, so the tests follow it if it changes.Timing
The program.
Results.
Release build, Apple M1 Pro, 5 runs per side. Median, with the range in brackets:
7760920)6566229)While
maingrows about 4x per doubling on the attribute count (quadratic), this branch grows about 2x per doubling (linear).