LibWeb: Add a fast (iterative) selector matcher for trivial selectors

If we determine that a selector is simple enough, we now run it using a
special matching loop that traverses up the DOM ancestor chain without
recursion.

The criteria for this fast path are:

- All combinators involved must be either descendant or child.
- Only tag name, class, ID and attribute selectors allowed.

It's definitely possible to increase the coverage of this fast path,
but this first version already provides a substantial reduction in time
spent evaluating selectors.

48% of the selectors evaluated when loading our GitHub repo are now
using this fast path.

18% speed-up on the "Descendant and child combinators" subtest of
StyleBench. :^)
This commit is contained in:
Andreas Kling 2024-03-19 10:36:08 +01:00
parent 432536f0b3
commit 3c3e591f03
Notes: sideshowbarker 2024-07-16 17:12:03 +09:00
4 changed files with 128 additions and 4 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2018-2024, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
@ -667,4 +667,115 @@ bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&>
return matches(selector, style_sheet_for_rule, selector.compound_selectors().size() - 1, element, scope);
}
static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& simple_selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element)
{
switch (simple_selector.type) {
case CSS::Selector::SimpleSelector::Type::Universal:
return matches_namespace(simple_selector.qualified_name(), element, style_sheet_for_rule);
case CSS::Selector::SimpleSelector::Type::TagName:
if (element.document().document_type() == DOM::Document::Type::HTML) {
if (simple_selector.qualified_name().name.lowercase_name != element.local_name())
return false;
} else if (!Infra::is_ascii_case_insensitive_match(simple_selector.qualified_name().name.name, element.local_name())) {
return false;
}
return matches_namespace(simple_selector.qualified_name(), element, style_sheet_for_rule);
case CSS::Selector::SimpleSelector::Type::Class:
return element.has_class(simple_selector.name());
case CSS::Selector::SimpleSelector::Type::Id:
return simple_selector.name() == element.id();
case CSS::Selector::SimpleSelector::Type::Attribute:
return matches_attribute(simple_selector.attribute(), style_sheet_for_rule, element);
default:
VERIFY_NOT_REACHED();
}
}
static bool fast_matches_compound_selector(CSS::Selector::CompoundSelector const& compound_selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element)
{
for (auto const& simple_selector : compound_selector.simple_selectors) {
if (!fast_matches_simple_selector(simple_selector, style_sheet_for_rule, element))
return false;
}
return true;
}
FLATTEN bool fast_matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element_to_match)
{
DOM::Element const* current = &element_to_match;
ssize_t compound_selector_index = selector.compound_selectors().size() - 1;
if (!fast_matches_compound_selector(selector.compound_selectors().last(), style_sheet_for_rule, *current))
return false;
// NOTE: If we fail after following a child combinator, we may need to backtrack
// to the last matched descendant. We store the state here.
struct {
DOM::Element const* element = nullptr;
ssize_t compound_selector_index = 0;
} backtrack_state;
for (;;) {
// NOTE: There should always be a leftmost compound selector without combinator that kicks us out of this loop.
VERIFY(compound_selector_index >= 0);
auto const* compound_selector = &selector.compound_selectors()[compound_selector_index];
switch (compound_selector->combinator) {
case CSS::Selector::Combinator::None:
return true;
case CSS::Selector::Combinator::Descendant:
backtrack_state = { current->parent_element(), compound_selector_index };
compound_selector = &selector.compound_selectors()[--compound_selector_index];
for (current = current->parent_element(); current; current = current->parent_element()) {
if (fast_matches_compound_selector(*compound_selector, style_sheet_for_rule, *current))
break;
}
if (!current)
return false;
break;
case CSS::Selector::Combinator::ImmediateChild:
compound_selector = &selector.compound_selectors()[--compound_selector_index];
current = current->parent_element();
if (!current)
return false;
if (!fast_matches_compound_selector(*compound_selector, style_sheet_for_rule, *current)) {
if (backtrack_state.element) {
current = backtrack_state.element;
compound_selector_index = backtrack_state.compound_selector_index;
continue;
}
return false;
}
break;
default:
VERIFY_NOT_REACHED();
}
}
}
bool can_use_fast_matches(CSS::Selector const& selector)
{
for (auto const& compound_selector : selector.compound_selectors()) {
if (compound_selector.combinator != CSS::Selector::Combinator::None
&& compound_selector.combinator != CSS::Selector::Combinator::Descendant
&& compound_selector.combinator != CSS::Selector::Combinator::ImmediateChild) {
return false;
}
for (auto const& simple_selector : compound_selector.simple_selectors) {
if (simple_selector.type != CSS::Selector::SimpleSelector::Type::TagName
&& simple_selector.type != CSS::Selector::SimpleSelector::Type::Universal
&& simple_selector.type != CSS::Selector::SimpleSelector::Type::Class
&& simple_selector.type != CSS::Selector::SimpleSelector::Type::Id
&& simple_selector.type != CSS::Selector::SimpleSelector::Type::Attribute) {
return false;
}
}
}
return true;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2018-2024, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -13,4 +13,7 @@ namespace Web::SelectorEngine {
bool matches(CSS::Selector const&, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const&, Optional<CSS::Selector::PseudoElement::Type> = {}, JS::GCPtr<DOM::ParentNode const> scope = {});
[[nodiscard]] bool fast_matches(CSS::Selector const&, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const&);
[[nodiscard]] bool can_use_fast_matches(CSS::Selector const&);
}

View File

@ -358,7 +358,13 @@ Vector<MatchingRule> StyleComputer::collect_matching_rules(DOM::Element const& e
continue;
auto const& selector = rule_to_run.rule->selectors()[rule_to_run.selector_index];
if (SelectorEngine::matches(selector, *rule_to_run.sheet, element, pseudo_element))
if (rule_to_run.can_use_fast_matches) {
if (!SelectorEngine::fast_matches(selector, *rule_to_run.sheet, element))
continue;
} else {
if (!SelectorEngine::matches(selector, *rule_to_run.sheet, element, pseudo_element))
continue;
}
matching_rules.append(rule_to_run);
}
return matching_rules;
@ -2331,6 +2337,9 @@ NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_casca
selector_index,
selector.specificity(),
cascade_origin,
false,
false,
SelectorEngine::can_use_fast_matches(selector),
};
for (auto const& simple_selector : selector.compound_selectors().last().simple_selectors) {

View File

@ -41,6 +41,7 @@ struct MatchingRule {
CascadeOrigin cascade_origin;
bool contains_pseudo_element { false };
bool contains_root_pseudo_class { false };
bool can_use_fast_matches { false };
};
struct FontFaceKey {