-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a fast path for matching easy selectors.
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
Showing
7 changed files
with
360 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
183 changes: 183 additions & 0 deletions
183
third_party/blink/renderer/core/css/selector_checker-inl.h
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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_ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.