mirror of
https://github.com/servo/servo.git
synced 2025-08-06 14:10:11 +01:00
Auto merge of #17888 - emilio:o-to-the-n, r=heycam
style: Avoid exponential blowup when processing invalidations of the same kind. Bug: 1383981 Reviewed-by: heycam MozReview-Commit-ID: HZ97UwvblkQ
This commit is contained in:
commit
a56d3a5381
1 changed files with 143 additions and 29 deletions
|
@ -35,6 +35,16 @@ pub struct TreeStyleInvalidator<'a, 'b: 'a, E>
|
||||||
|
|
||||||
type InvalidationVector = SmallVec<[Invalidation; 10]>;
|
type InvalidationVector = SmallVec<[Invalidation; 10]>;
|
||||||
|
|
||||||
|
/// The kind of invalidation we're processing.
|
||||||
|
///
|
||||||
|
/// We can use this to avoid pushing invalidations of the same kind to our
|
||||||
|
/// descendants or siblings.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
enum InvalidationKind {
|
||||||
|
Descendant,
|
||||||
|
Sibling,
|
||||||
|
}
|
||||||
|
|
||||||
/// An `Invalidation` is a complex selector that describes which elements,
|
/// An `Invalidation` is a complex selector that describes which elements,
|
||||||
/// relative to a current element we are processing, must be restyled.
|
/// relative to a current element we are processing, must be restyled.
|
||||||
///
|
///
|
||||||
|
@ -46,6 +56,37 @@ type InvalidationVector = SmallVec<[Invalidation; 10]>;
|
||||||
struct Invalidation {
|
struct Invalidation {
|
||||||
selector: Selector<SelectorImpl>,
|
selector: Selector<SelectorImpl>,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
|
/// Whether the invalidation was already matched by any previous sibling or
|
||||||
|
/// ancestor.
|
||||||
|
///
|
||||||
|
/// If this is the case, we can avoid pushing invalidations generated by
|
||||||
|
/// this one if the generated invalidation is effective for all the siblings
|
||||||
|
/// or descendants after us.
|
||||||
|
matched_by_any_previous: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Invalidation {
|
||||||
|
/// Whether this invalidation is effective for the next sibling or
|
||||||
|
/// descendant after us.
|
||||||
|
fn effective_for_next(&self) -> bool {
|
||||||
|
// TODO(emilio): For pseudo-elements this should be mostly false, except
|
||||||
|
// for the weird pseudos in <input type="number">.
|
||||||
|
//
|
||||||
|
// We should be able to do better here!
|
||||||
|
match self.selector.combinator_at(self.offset) {
|
||||||
|
Combinator::NextSibling |
|
||||||
|
Combinator::Child => false,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> InvalidationKind {
|
||||||
|
if self.selector.combinator_at(self.offset).is_ancestor() {
|
||||||
|
InvalidationKind::Descendant
|
||||||
|
} else {
|
||||||
|
InvalidationKind::Sibling
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Invalidation {
|
impl fmt::Debug for Invalidation {
|
||||||
|
@ -67,9 +108,9 @@ impl fmt::Debug for Invalidation {
|
||||||
struct InvalidationResult {
|
struct InvalidationResult {
|
||||||
/// Whether the element itself was invalidated.
|
/// Whether the element itself was invalidated.
|
||||||
invalidated_self: bool,
|
invalidated_self: bool,
|
||||||
/// Whether the invalidation we've processed is effective for the next
|
/// Whether the invalidation matched, either invalidating the element or
|
||||||
/// sibling or descendant after us.
|
/// generating another invalidation.
|
||||||
effective_for_next: bool,
|
matched: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
|
impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
|
||||||
|
@ -460,14 +501,16 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
|
||||||
let result = self.process_invalidation(
|
let result = self.process_invalidation(
|
||||||
&sibling_invalidations[i],
|
&sibling_invalidations[i],
|
||||||
descendant_invalidations,
|
descendant_invalidations,
|
||||||
&mut new_sibling_invalidations
|
&mut new_sibling_invalidations,
|
||||||
|
InvalidationKind::Sibling,
|
||||||
);
|
);
|
||||||
|
|
||||||
invalidated_self |= result.invalidated_self;
|
invalidated_self |= result.invalidated_self;
|
||||||
if !result.effective_for_next {
|
sibling_invalidations[i].matched_by_any_previous |= result.matched;
|
||||||
sibling_invalidations.remove(i);
|
if sibling_invalidations[i].effective_for_next() {
|
||||||
} else {
|
|
||||||
i += 1;
|
i += 1;
|
||||||
|
} else {
|
||||||
|
sibling_invalidations.remove(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,10 +536,13 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
|
||||||
invalidation,
|
invalidation,
|
||||||
descendant_invalidations,
|
descendant_invalidations,
|
||||||
sibling_invalidations,
|
sibling_invalidations,
|
||||||
|
InvalidationKind::Descendant,
|
||||||
);
|
);
|
||||||
|
|
||||||
invalidated |= result.invalidated_self;
|
invalidated |= result.invalidated_self;
|
||||||
if result.effective_for_next {
|
if invalidation.effective_for_next() {
|
||||||
|
let mut invalidation = invalidation.clone();
|
||||||
|
invalidation.matched_by_any_previous |= result.matched;
|
||||||
descendant_invalidations.push(invalidation.clone());
|
descendant_invalidations.push(invalidation.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -514,10 +560,11 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
|
||||||
&mut self,
|
&mut self,
|
||||||
invalidation: &Invalidation,
|
invalidation: &Invalidation,
|
||||||
descendant_invalidations: &mut InvalidationVector,
|
descendant_invalidations: &mut InvalidationVector,
|
||||||
sibling_invalidations: &mut InvalidationVector
|
sibling_invalidations: &mut InvalidationVector,
|
||||||
|
invalidation_kind: InvalidationKind,
|
||||||
) -> InvalidationResult {
|
) -> InvalidationResult {
|
||||||
debug!("TreeStyleInvalidator::process_invalidation({:?}, {:?})",
|
debug!("TreeStyleInvalidator::process_invalidation({:?}, {:?}, {:?})",
|
||||||
self.element, invalidation);
|
self.element, invalidation, invalidation_kind);
|
||||||
|
|
||||||
let mut context =
|
let mut context =
|
||||||
MatchingContext::new_for_visited(
|
MatchingContext::new_for_visited(
|
||||||
|
@ -535,14 +582,17 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut invalidated_self = false;
|
let mut invalidated_self = false;
|
||||||
|
let mut matched = false;
|
||||||
match matching_result {
|
match matching_result {
|
||||||
CompoundSelectorMatchingResult::Matched { next_combinator_offset: 0 } => {
|
CompoundSelectorMatchingResult::Matched { next_combinator_offset: 0 } => {
|
||||||
debug!(" > Invalidation matched completely");
|
debug!(" > Invalidation matched completely");
|
||||||
|
matched = true;
|
||||||
invalidated_self = true;
|
invalidated_self = true;
|
||||||
}
|
}
|
||||||
CompoundSelectorMatchingResult::Matched { next_combinator_offset } => {
|
CompoundSelectorMatchingResult::Matched { next_combinator_offset } => {
|
||||||
let next_combinator =
|
let next_combinator =
|
||||||
invalidation.selector.combinator_at(next_combinator_offset);
|
invalidation.selector.combinator_at(next_combinator_offset);
|
||||||
|
matched = true;
|
||||||
|
|
||||||
if matches!(next_combinator, Combinator::PseudoElement) {
|
if matches!(next_combinator, Combinator::PseudoElement) {
|
||||||
let pseudo_selector =
|
let pseudo_selector =
|
||||||
|
@ -578,14 +628,90 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
|
||||||
let next_invalidation = Invalidation {
|
let next_invalidation = Invalidation {
|
||||||
selector: invalidation.selector.clone(),
|
selector: invalidation.selector.clone(),
|
||||||
offset: next_combinator_offset,
|
offset: next_combinator_offset,
|
||||||
|
matched_by_any_previous: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(" > Invalidation matched, next: {:?}, ({:?})",
|
debug!(" > Invalidation matched, next: {:?}, ({:?})",
|
||||||
next_invalidation, next_combinator);
|
next_invalidation, next_combinator);
|
||||||
if next_combinator.is_ancestor() {
|
|
||||||
descendant_invalidations.push(next_invalidation);
|
let next_invalidation_kind = next_invalidation.kind();
|
||||||
|
|
||||||
|
// We can skip pushing under some circumstances, and we should
|
||||||
|
// because otherwise the invalidation list could grow
|
||||||
|
// exponentially.
|
||||||
|
//
|
||||||
|
// * First of all, both invalidations need to be of the same
|
||||||
|
// kind. This is because of how we propagate them going to
|
||||||
|
// the right of the tree for sibling invalidations and going
|
||||||
|
// down the tree for children invalidations. A sibling
|
||||||
|
// invalidation that ends up generating a children
|
||||||
|
// invalidation ends up (correctly) in five different lists,
|
||||||
|
// not in the same list five different times.
|
||||||
|
//
|
||||||
|
// * Then, the invalidation needs to be matched by a previous
|
||||||
|
// ancestor/sibling, in order to know that this invalidation
|
||||||
|
// has been generated already.
|
||||||
|
//
|
||||||
|
// * Finally, the new invalidation needs to be
|
||||||
|
// `effective_for_next()`, in order for us to know that it is
|
||||||
|
// still in the list, since we remove the dependencies that
|
||||||
|
// aren't from the lists for our children / siblings.
|
||||||
|
//
|
||||||
|
// To go through an example, let's imagine we are processing a
|
||||||
|
// dom subtree like:
|
||||||
|
//
|
||||||
|
// <div><address><div><div/></div></address></div>
|
||||||
|
//
|
||||||
|
// And an invalidation list with a single invalidation like:
|
||||||
|
//
|
||||||
|
// [div div div]
|
||||||
|
//
|
||||||
|
// When we process the invalidation list for the outer div, we
|
||||||
|
// match it, and generate a `div div` invalidation, so for the
|
||||||
|
// <address> child we have:
|
||||||
|
//
|
||||||
|
// [div div div, div div]
|
||||||
|
//
|
||||||
|
// With the first of them marked as `matched`.
|
||||||
|
//
|
||||||
|
// When we process the <address> child, we don't match any of
|
||||||
|
// them, so both invalidations go untouched to our children.
|
||||||
|
//
|
||||||
|
// When we process the second <div>, we match _both_
|
||||||
|
// invalidations.
|
||||||
|
//
|
||||||
|
// However, when matching the first, we can tell it's been
|
||||||
|
// matched, and not push the corresponding `div div`
|
||||||
|
// invalidation, since we know it's necessarily already on the
|
||||||
|
// list.
|
||||||
|
//
|
||||||
|
// Thus, without skipping the push, we'll arrive to the
|
||||||
|
// innermost <div> with:
|
||||||
|
//
|
||||||
|
// [div div div, div div, div div, div]
|
||||||
|
//
|
||||||
|
// While skipping it, we won't arrive here with duplicating
|
||||||
|
// dependencies:
|
||||||
|
//
|
||||||
|
// [div div div, div div, div]
|
||||||
|
//
|
||||||
|
let can_skip_pushing =
|
||||||
|
next_invalidation_kind == invalidation_kind &&
|
||||||
|
invalidation.matched_by_any_previous &&
|
||||||
|
next_invalidation.effective_for_next();
|
||||||
|
|
||||||
|
if can_skip_pushing {
|
||||||
|
debug!(" > Can avoid push, since the invalidation had \
|
||||||
|
already been matched before");
|
||||||
} else {
|
} else {
|
||||||
sibling_invalidations.push(next_invalidation);
|
match next_invalidation_kind {
|
||||||
|
InvalidationKind::Descendant => {
|
||||||
|
descendant_invalidations.push(next_invalidation);
|
||||||
|
}
|
||||||
|
InvalidationKind::Sibling => {
|
||||||
|
sibling_invalidations.push(next_invalidation);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CompoundSelectorMatchingResult::NotMatched => {}
|
CompoundSelectorMatchingResult::NotMatched => {}
|
||||||
|
@ -597,21 +723,7 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(emilio): For pseudo-elements this should be mostly false, except
|
InvalidationResult { invalidated_self, matched, }
|
||||||
// for the weird pseudos in <input type="number">.
|
|
||||||
//
|
|
||||||
// We should be able to do better here!
|
|
||||||
let effective_for_next =
|
|
||||||
match invalidation.selector.combinator_at(invalidation.offset) {
|
|
||||||
Combinator::NextSibling |
|
|
||||||
Combinator::Child => false,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
InvalidationResult {
|
|
||||||
invalidated_self: invalidated_self,
|
|
||||||
effective_for_next: effective_for_next,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -830,12 +942,14 @@ impl<'a, 'b: 'a, E> InvalidationCollector<'a, 'b, E>
|
||||||
self.descendant_invalidations.push(Invalidation {
|
self.descendant_invalidations.push(Invalidation {
|
||||||
selector: dependency.selector.clone(),
|
selector: dependency.selector.clone(),
|
||||||
offset: dependency.selector_offset,
|
offset: dependency.selector_offset,
|
||||||
|
matched_by_any_previous: false,
|
||||||
});
|
});
|
||||||
} else if dependency.affects_later_siblings() {
|
} else if dependency.affects_later_siblings() {
|
||||||
debug_assert_ne!(dependency.selector_offset, 0);
|
debug_assert_ne!(dependency.selector_offset, 0);
|
||||||
self.sibling_invalidations.push(Invalidation {
|
self.sibling_invalidations.push(Invalidation {
|
||||||
selector: dependency.selector.clone(),
|
selector: dependency.selector.clone(),
|
||||||
offset: dependency.selector_offset,
|
offset: dependency.selector_offset,
|
||||||
|
matched_by_any_previous: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue