mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-12-26 12:41:59 +03:00
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:
parent
432536f0b3
commit
3c3e591f03
Notes:
sideshowbarker
2024-07-16 17:12:03 +09:00
Author: https://github.com/awesomekling Commit: https://github.com/SerenityOS/serenity/commit/3c3e591f03 Pull-request: https://github.com/SerenityOS/serenity/pull/23637
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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&);
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user