Skip to content

Commit

Permalink
Add a fast path for matching easy selectors.
Browse files Browse the repository at this point in the history
The full selector matcher is overkill for most selectors; it needs
to cover all sorts of oddities around pseudo-elements, unusual
combinators, :has() and so on, but most selectors are simply
matching against tags and class names. Thus, we add a concept of
“easy” selectors, and a simpler and faster way of matching against
them. This covers ~80% of all selector matching.

Style perftest (Zen 3, LTO but no PGO, _including_ the bucketing cover
patch):

Initial style (µs)     Before     After    Perf      95% CI (BCa)
=================== ========= ========= ======= =================
ECommerce                6932      6693   +3.6%  [ +2.8%,  +4.4%]
Encyclopedia            74726     73131   +2.2%  [ +1.4%,  +3.0%]
Extension               90592     89228   +1.5%  [ +0.7%,  +2.3%]
News                    30161     29459   +2.4%  [ +1.6%,  +3.1%]
Search                   1824      1863   -2.1%  [ -2.8%,  -1.4%]
Social1                 16657     15998   +4.1%  [ +3.3%,  +5.0%]
Social2                   717       709   +1.2%  [ +0.3%,  +2.0%]
Sports                  29724     29334   +1.3%  [ +0.5%,  +2.2%]
Video                   29918     29190   +2.5%  [ +1.7%,  +3.3%]
Geometric mean                            +1.8%  [ +1.4%,  +2.4%]

Recalc style (µs)      Before     After    Perf      95% CI (BCa)
=================== ========= ========= ======= =================
ECommerce                8263      7915   +4.4%  [ +3.5%,  +5.2%]
Encyclopedia            60499     58998   +2.5%  [ +1.7%,  +3.4%]
Extension               84117     82605   +1.8%  [ +1.0%,  +2.6%]
News                    22755     22069   +3.1%  [ +2.3%,  +4.0%]
Search                    189       185   +2.2%  [ +1.4%,  +3.1%]
Social1                 12765     12070   +5.8%  [ +4.9%,  +6.6%]
Social2                   410       401   +2.2%  [ +1.5%,  +3.0%]
Sports                  17463     16928   +3.2%  [ +2.3%,  +4.0%]
Video                   19250     18290   +5.2%  [ +4.4%,  +6.1%]
Geometric mean                            +3.4%  [ +2.8%,  +3.9%]

Based on a rough profile, the easy selector matching is ~2.4x
as fast as the normal selector matching (for those that it supports,
of course) -- this is excluding the ones that are entirely covered by
bucketing, on both sides. It is interesting to compare this number to
the previously cited number of 2x for JITing selectors, although it
is not given that the two are entirely comparable.

Speedometer2 and MotionMark are not affected, since they hardly
do any selector matching at all.

Fixed: 1410018
Change-Id: I9fdd3574ddadafbdcee6ad8c4515710ccc067592
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4208960
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Commit-Queue: Steinar H Gunderson <sesse@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1104494}
  • Loading branch information
Steinar H. Gunderson authored and Chromium LUCI CQ committed Feb 13, 2023
1 parent 2ad72e0 commit 0704675
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 2 deletions.
24 changes: 24 additions & 0 deletions third_party/blink/renderer/core/css/element_rule_collector.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include "third_party/blink/renderer/core/css/resolver/style_resolver.h"
#include "third_party/blink/renderer/core/css/resolver/style_resolver_stats.h"
#include "third_party/blink/renderer/core/css/resolver/style_rule_usage_tracker.h"
#include "third_party/blink/renderer/core/css/selector_checker-inl.h"
#include "third_party/blink/renderer/core/css/selector_statistics.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/dom/layout_tree_builder_traversal.h"
Expand Down Expand Up @@ -401,6 +402,10 @@ void ElementRuleCollector::CollectMatchingRulesForListInternal(
static_cast<wtf_size_t>(rules.size()));
}

const bool case_sensitive_tag_matching =
context.element->IsHTMLElement() ||
!IsA<HTMLDocument>(context.element->GetDocument());

for (const RuleData& rule_data : rules) {
if (perf_trace_enabled) {
selector_statistics_collector.EndCollectionForCurrentRule();
Expand Down Expand Up @@ -451,6 +456,25 @@ void ElementRuleCollector::CollectMatchingRulesForListInternal(
}
DCHECK(SlowMatchWithNoResultFlags(checker, context, selector, rule_data,
result.proximity));
} else if (case_sensitive_tag_matching && rule_data.SelectorIsEasy()) {
if (pseudo_style_request_.pseudo_id != kPseudoIdNone) {
continue;
}
bool easy_match = EasySelectorChecker::Match(&selector, context.element);

if (context.style_scope != nullptr &&
RuntimeEnabledFeatures::CSSScopeEnabled() &&
!checker.CheckInStyleScope(context, result)) {
easy_match = false;
}
DCHECK_EQ(easy_match,
SlowMatchWithNoResultFlags(checker, context, selector,
rule_data, result.proximity))
<< "Mismatch for selector " << selector.SelectorText()
<< " on element " << context.element;
if (!easy_match) {
continue;
}
} else {
context.selector = &selector;
context.is_inside_visited_link =
Expand Down
4 changes: 4 additions & 0 deletions third_party/blink/renderer/core/css/rule_set.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
#include "third_party/blink/renderer/core/css/css_font_selector.h"
#include "third_party/blink/renderer/core/css/css_selector.h"
#include "third_party/blink/renderer/core/css/css_selector_list.h"
#include "third_party/blink/renderer/core/css/selector_checker-inl.h"
#include "third_party/blink/renderer/core/css/selector_checker.h"
#include "third_party/blink/renderer/core/css/selector_filter.h"
#include "third_party/blink/renderer/core/css/style_rule_import.h"
#include "third_party/blink/renderer/core/css/style_sheet_contents.h"
Expand Down Expand Up @@ -122,9 +124,11 @@ RuleData::RuleData(StyleRule* rule,
DetermineValidPropertyFilter(add_rule_flags, Selector()))),
is_entirely_covered_by_bucketing_(
false), // Will be computed in ComputeEntirelyCoveredByBucketing().
is_easy_(false), // Ditto.
descendant_selector_identifier_hashes_() {}

void RuleData::ComputeEntirelyCoveredByBucketing() {
is_easy_ = EasySelectorChecker::IsEasy(&Selector());
is_entirely_covered_by_bucketing_ = true;
for (const CSSSelector* selector = &Selector(); selector;
selector = selector->TagHistory()) {
Expand Down
4 changes: 3 additions & 1 deletion third_party/blink/renderer/core/css/rule_set.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class CORE_EXPORT RuleData {
}
void ComputeEntirelyCoveredByBucketing();
void ResetEntirelyCoveredByBucketing();
bool SelectorIsEasy() const { return is_easy_; }

bool ContainsUncommonAttributeSelector() const {
return contains_uncommon_attribute_selector_;
Expand Down Expand Up @@ -191,7 +192,8 @@ class CORE_EXPORT RuleData {
unsigned has_document_security_origin_ : 1;
unsigned valid_property_filter_ : 3;
unsigned is_entirely_covered_by_bucketing_ : 1;
// 31 bits above
unsigned is_easy_ : 1; // See EasySelectorChecker.
// 32 bits above
union {
// Used by RuleMap before compaction, to hold what bucket this RuleData
// is to be sorted into. (If the RuleData lives in a RuleMap, the hashes
Expand Down
183 changes: 183 additions & 0 deletions third_party/blink/renderer/core/css/selector_checker-inl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_CSS_SELECTOR_CHECKER_INL_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_CSS_SELECTOR_CHECKER_INL_H_

#include "third_party/blink/renderer/core/css/css_selector.h"
#include "third_party/blink/renderer/core/css/selector_checker.h"

namespace blink {

bool EasySelectorChecker::IsEasy(const CSSSelector* selector) {
for (; selector != nullptr; selector = selector->TagHistory()) {
if (!selector->IsLastInTagHistory() &&
selector->Relation() != CSSSelector::kSubSelector &&
selector->Relation() != CSSSelector::kDescendant) {
// We don't support anything that requires us to recurse.
return false;
}
if (selector->IsCoveredByBucketing()) {
// No matter what this selector is, we won't need to check it,
// so it's fine.
continue;
}
switch (selector->Match()) {
case CSSSelector::kTag: {
const QualifiedName& tag_q_name = selector->TagQName();
if (tag_q_name == AnyQName() ||
tag_q_name.LocalName() == CSSSelector::UniversalSelectorAtom()) {
// We don't support the universal selector, to avoid checking
// for it when doing tag matching (most selectors are not
// the universal selector).
return false;
}
break;
}
case CSSSelector::kId:
case CSSSelector::kClass:
break;
case CSSSelector::kAttributeExact:
if (selector->AttributeMatch() ==
CSSSelector::AttributeMatchType::kCaseInsensitive ||
!selector->IsCaseSensitiveAttribute()) {
// We don't bother with case-insensitive attribute checks,
// for simplicity and avoiding the extra tests. (We probably
// could revisit this in the future if needed.)
return false;
}
[[fallthrough]];
case CSSSelector::kAttributeSet:
if (selector->Attribute().Prefix() == g_star_atom) {
// We don't support attribute matches with wildcard namespaces
// (e.g. [*|attr]), since those prevent short-circuiting in
// Match() once we've found the attribute; there might be more
// than one, so we would have to keep looking, and we don't
// want to support that.
return false;
}
break;
default:
// Unsupported selector.
return false;
}
}
return true;
}

bool EasySelectorChecker::Match(const CSSSelector* selector,
const Element* element) {
DCHECK(IsEasy(selector));

// Since we only support subselector and descendant combinators, we can do
// with a nonrecursive algorithm. The idea is fairly simple: We can match
// greedily and never need to backtrack. E.g. if we have .a.b .c.d .e.f {}
// and see an element matching .e.f and then later some parent matching .c.d,
// we never need to look for .c.d again.
//
// Apart from that, it's a simple matter of just matching the simple selectors
// against the current element, one by one. If we have a mismatch
// in the subject (.e.f in the example above), the match fails immediately.
// If we have a mismatch when looking for a parent (either .a.b or .c.d
// in the example above), we rewind to the start of the compound and move on
// to the parent element. (rewind_on_failure then points to the start of the
// compound; it's nullptr if we're matching the subject.)
//
// If all subselectors in a compound have matched, we move on to the next
// compound (setting rewind_on_failure to the start of it) and go to the
// parent element to check the next descendant.
const CSSSelector* rewind_on_failure = nullptr;

while (selector != nullptr) {
if (selector->IsCoveredByBucketing()) {
DCHECK(MatchOne(selector, element))
<< selector->SelectorText() << " unexpectedly didn't match "
<< element;
}
if (selector->IsCoveredByBucketing() || MatchOne(selector, element)) {
if (selector->Relation() == CSSSelector::kDescendant) {
// We matched the entire compound, but there are more.
// Move to the next one.
DCHECK(!selector->IsLastInTagHistory());
rewind_on_failure = selector->TagHistory();

element = element->parentElement();
if (element == nullptr) {
return false;
}
}
selector = selector->TagHistory();
} else if (rewind_on_failure) {
// We failed to match this compound, but we are looking for descendants,
// so rewind to start of the compound and try the parent element.
selector = rewind_on_failure;

element = element->parentElement();
if (element == nullptr) {
return false;
}
} else {
// We failed to match this compound, and we're in the subject,
// so fail immediately.
return false;
}
}

return true;
}

bool EasySelectorChecker::MatchOne(const CSSSelector* selector,
const Element* element) {
switch (selector->Match()) {
case CSSSelector::kTag: {
const QualifiedName& tag_q_name = selector->TagQName();
return element->localName() == tag_q_name.LocalName() &&
(element->namespaceURI() == tag_q_name.NamespaceURI() ||
tag_q_name.NamespaceURI() == g_star_atom);
}
case CSSSelector::kClass:
return element->HasClass() &&
element->ClassNames().Contains(selector->Value());
case CSSSelector::kId:
return element->HasID() &&
element->IdForStyleResolution() == selector->Value();
case CSSSelector::kAttributeSet:
return AttributeIsSet(*element, selector->Attribute());
case CSSSelector::kAttributeExact:
return AttributeMatches(*element, selector->Attribute(),
selector->Value());
default:
NOTREACHED();
}
return false;
}

bool EasySelectorChecker::AttributeIsSet(const Element& element,
const QualifiedName& attr) {
element.SynchronizeAttribute(attr.LocalName());
AttributeCollection attributes = element.AttributesWithoutUpdate();
for (const auto& attribute_item : attributes) {
if (attribute_item.Matches(attr)) {
return true;
}
}
return false;
}

bool EasySelectorChecker::AttributeMatches(const Element& element,
const QualifiedName& attr,
const AtomicString& value) {
element.SynchronizeAttribute(attr.LocalName());
AttributeCollection attributes = element.AttributesWithoutUpdate();
for (const auto& attribute_item : attributes) {
if (attribute_item.Matches(attr)) {
return attribute_item.Value() == value;
}
}
return false;
}

} // namespace blink

#endif // THIRD_PARTY_BLINK_RENDERER_CORE_CSS_SELECTOR_CHECKER_INL_H_
59 changes: 59 additions & 0 deletions third_party/blink/renderer/core/css/selector_checker.h
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,65 @@ class CORE_EXPORT SelectorChecker {
friend class NthIndexCache;
};

// An accelerated selector checker that matches only selectors with a
// certain set of restrictions, informally called “easy” selectors.
// (Not to be confused with simple selectors, which is a standards-defined
// term.) Easy selectors support only a very small subset of the full
// CSS selector machinery, but does so much faster than SelectorChecker
// (typically a bit over twice as fast), and that subset tends to be enough
// for ~80% of actual selectors checks on a typical web page. (It is also
// ree from the complexities of Shadow DOM and does not check whether
// the query exceeds the scope, so it cannot be used for querySelector().)
//
// The set of supported selectors is formally given as “anything IsEasy()
// returns true for”, but roughly encompasses the following:
//
// - Tag matches (e.g. div).
// - ID matches (e.g. #id).
// - Class matches (e.g. .c).
// - Case-sensitive attribute is-set and exact matches ([foo] and [foo="bar"]).
// - Subselector and descendant combinators.
// - Anything that does not need further checking
// (CSSSelector::IsCoveredByBucketing()).
//
// Given this, it does not need to set up any context, do recursion,
// backtracking, have large switch/cases for pseudos, or the similar.
//
// You must include selector_checker-inl.h to use this class;
// its functions are declared ALWAYS_INLINE because the call overhead
// is so large compared to what the functions are actually doing.
class CORE_EXPORT EasySelectorChecker {
public:
// Returns true iff the given selector is easy and can be given to Match().
// Should be precomputed for the given selector.
//
// If IsEasy() is true, this selector can never return any match flags,
// or match (dynamic) pseudos.
static ALWAYS_INLINE bool IsEasy(const CSSSelector* selector);

// Returns whether the given selector matches the given element.
// The following preconditions apply:
//
// - The selector must be easy (see IsEasy()).
// - Tag matching must be case-sensitive in the current context,
// i.e., that the element is _not_ a non-HTML element in an
// HTML document.
//
// Unlike SelectorChecker, does not check style_scope; the caller
// will need to do that if desired.
static ALWAYS_INLINE bool Match(const CSSSelector* selector,
const Element* element);

private:
static ALWAYS_INLINE bool MatchOne(const CSSSelector* selector,
const Element* element);
static ALWAYS_INLINE bool AttributeIsSet(const Element& element,
const QualifiedName& attr);
static ALWAYS_INLINE bool AttributeMatches(const Element& element,
const QualifiedName& attr,
const AtomicString& value);
};

} // namespace blink

#endif // THIRD_PARTY_BLINK_RENDERER_CORE_CSS_SELECTOR_CHECKER_H_

0 comments on commit 0704675

Please sign in to comment.