Skip to content

Commit

Permalink
[:has() pseudo-class] Use Bloom filter to quickly reject :has() selec…
Browse files Browse the repository at this point in the history
…tors

https://bugs.webkit.org/show_bug.cgi?id=234341

Reviewed by Dean Jackson.

We can dramatically speed up cases where there are many :has rules, most of which don't match their argument
by building a Bloom filter describing the features of the subtree.

* Sources.txt:
* WebCore.xcodeproj/project.pbxproj:
* css/SelectorChecker.cpp:
(WebCore::SelectorChecker::matchHasPseudoClass const):

Build and cache HasSelectorFilter per element/filter type. It will be constructed only if multiple :has()
selectors are tested for the same element (otherwise the regular match cache is more efficient).
Use it to quickly reject selectors.
Also add a basic inital optimization to bail out if there are no child/sibling elements that could match.

* css/SelectorFilter.cpp:
(WebCore::SelectorFilter::collectElementIdentifierHashes):
(WebCore::SelectorFilter::collectSimpleSelectorHash):
(WebCore::SelectorFilter::collectSelectorHashes):
(WebCore::SelectorFilter::chooseSelectorHashesForFilter):
(WebCore::collectElementIdentifierHashes): Deleted.
(WebCore::collectSimpleSelectorHash): Deleted.
(WebCore::collectSelectorHashes): Deleted.
(WebCore::chooseSelectorHashesForFilter): Deleted.
* css/SelectorFilter.h:
* style/HasSelectorFilter.cpp: Added.
(WebCore::Style::HasSelectorFilter::HasSelectorFilter):
(WebCore::Style::HasSelectorFilter::typeForMatchElement):
(WebCore::Style::HasSelectorFilter::makeKey):

The key consists of the most specific string in the rightmost compound of the selector along with
:hover pseudo class, if any.

(WebCore::Style::HasSelectorFilter::add):

Add an Element to the filter.
The features collected are the same as for the regular selector filter, plus permutations with
:hover pseudo-class if it would match.

* style/HasSelectorFilter.h: Copied from Source/WebCore/style/SelectorMatchingState.h.
(WebCore::Style::HasSelectorFilter::type const):
(WebCore::Style::HasSelectorFilter::reject const):

Add HasSelectorFilter which uses non-counting BloomFilter internally. The size of the filter is 512 bytes.

* style/SelectorMatchingState.h:
(WebCore::Style::makeHasPseudoClassSelectorFilterKey):



Canonical link: https://commits.webkit.org/245283@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@287091 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
anttijk committed Dec 15, 2021
1 parent f522b9d commit 596fdf7
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 36 deletions.
53 changes: 53 additions & 0 deletions Source/WebCore/ChangeLog
@@ -1,3 +1,56 @@
2021-12-15 Antti Koivisto <antti@apple.com>

[:has() pseudo-class] Use Bloom filter to quickly reject :has() selectors
https://bugs.webkit.org/show_bug.cgi?id=234341

Reviewed by Dean Jackson.

We can dramatically speed up cases where there are many :has rules, most of which don't match their argument
by building a Bloom filter describing the features of the subtree.

* Sources.txt:
* WebCore.xcodeproj/project.pbxproj:
* css/SelectorChecker.cpp:
(WebCore::SelectorChecker::matchHasPseudoClass const):

Build and cache HasSelectorFilter per element/filter type. It will be constructed only if multiple :has()
selectors are tested for the same element (otherwise the regular match cache is more efficient).
Use it to quickly reject selectors.
Also add a basic inital optimization to bail out if there are no child/sibling elements that could match.

* css/SelectorFilter.cpp:
(WebCore::SelectorFilter::collectElementIdentifierHashes):
(WebCore::SelectorFilter::collectSimpleSelectorHash):
(WebCore::SelectorFilter::collectSelectorHashes):
(WebCore::SelectorFilter::chooseSelectorHashesForFilter):
(WebCore::collectElementIdentifierHashes): Deleted.
(WebCore::collectSimpleSelectorHash): Deleted.
(WebCore::collectSelectorHashes): Deleted.
(WebCore::chooseSelectorHashesForFilter): Deleted.
* css/SelectorFilter.h:
* style/HasSelectorFilter.cpp: Added.
(WebCore::Style::HasSelectorFilter::HasSelectorFilter):
(WebCore::Style::HasSelectorFilter::typeForMatchElement):
(WebCore::Style::HasSelectorFilter::makeKey):

The key consists of the most specific string in the rightmost compound of the selector along with
:hover pseudo class, if any.

(WebCore::Style::HasSelectorFilter::add):

Add an Element to the filter.
The features collected are the same as for the regular selector filter, plus permutations with
:hover pseudo-class if it would match.

* style/HasSelectorFilter.h: Copied from Source/WebCore/style/SelectorMatchingState.h.
(WebCore::Style::HasSelectorFilter::type const):
(WebCore::Style::HasSelectorFilter::reject const):

Add HasSelectorFilter which uses non-counting BloomFilter internally. The size of the filter is 512 bytes.

* style/SelectorMatchingState.h:
(WebCore::Style::makeHasPseudoClassSelectorFilterKey):

2021-12-15 Alexey Shvayka <ashvayka@apple.com>

[WebIDL] onselectionchange IDL attribute should not Document-reflect event listeners
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/Sources.txt
Expand Up @@ -2543,6 +2543,7 @@ style/AttributeChangeInvalidation.cpp
style/ChildChangeInvalidation.cpp
style/ClassChangeInvalidation.cpp
style/ElementRuleCollector.cpp
style/HasSelectorFilter.cpp
style/IdChangeInvalidation.cpp
style/InlineTextBoxStyle.cpp
style/InspectorCSSOMWrappers.cpp
Expand Down
6 changes: 6 additions & 0 deletions Source/WebCore/WebCore.xcodeproj/project.pbxproj
Expand Up @@ -5384,6 +5384,7 @@
E4E8B4EC216B79E500B8834D /* SystemFontDatabaseCoreText.h in Headers */ = {isa = PBXBuildFile; fileRef = E4E8B4EA216B79E500B8834D /* SystemFontDatabaseCoreText.h */; };
E4E8B4F5216B956500B8834D /* FontCascadeDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = E4E8B4F2216B8B6000B8834D /* FontCascadeDescription.h */; settings = {ATTRIBUTES = (Private, ); }; };
E4E94D6122FF158A00DD191F /* LegacyLineLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = E4A1AC7822FAFD500017B75B /* LegacyLineLayout.h */; settings = {ATTRIBUTES = (Private, ); }; };
E4ED3ECC2768A51D00F17AC8 /* HasSelectorFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = E4ED3ECA2768A51C00F17AC8 /* HasSelectorFilter.h */; };
E4F0BE3125712F6E009E7431 /* CaretRectComputation.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F0BE2E25710A75009E7431 /* CaretRectComputation.h */; };
E4F38D1B2626F13B007B1064 /* DefaultResourceLoadPriority.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F38D192626F13B007B1064 /* DefaultResourceLoadPriority.h */; };
E4F819C626FB4EBF0094E162 /* InlineBoxPainter.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F819C526FB4EBF0094E162 /* InlineBoxPainter.h */; };
Expand Down Expand Up @@ -17466,6 +17467,8 @@
E4E8B4ED216B79F400B8834D /* SystemFontDatabaseCoreText.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SystemFontDatabaseCoreText.cpp; sourceTree = "<group>"; };
E4E8B4F0216B8B5F00B8834D /* FontCascadeDescription.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FontCascadeDescription.cpp; sourceTree = "<group>"; };
E4E8B4F2216B8B6000B8834D /* FontCascadeDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FontCascadeDescription.h; sourceTree = "<group>"; };
E4ED3ECA2768A51C00F17AC8 /* HasSelectorFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HasSelectorFilter.h; sourceTree = "<group>"; };
E4ED3ECD2768A68800F17AC8 /* HasSelectorFilter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = HasSelectorFilter.cpp; sourceTree = "<group>"; };
E4F0BE2E25710A75009E7431 /* CaretRectComputation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CaretRectComputation.h; sourceTree = "<group>"; };
E4F0BE3025710A76009E7431 /* CaretRectComputation.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CaretRectComputation.cpp; sourceTree = "<group>"; };
E4F38D192626F13B007B1064 /* DefaultResourceLoadPriority.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DefaultResourceLoadPriority.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -31080,6 +31083,8 @@
E4A814D31C6DEC4000BF85AC /* ClassChangeInvalidation.h */,
FBDB619A16D6032A00BB3394 /* ElementRuleCollector.cpp */,
FBDB619E16D6036500BB3394 /* ElementRuleCollector.h */,
E4ED3ECD2768A68800F17AC8 /* HasSelectorFilter.cpp */,
E4ED3ECA2768A51C00F17AC8 /* HasSelectorFilter.h */,
E4A814DD1C7338D100BF85AC /* IdChangeInvalidation.cpp */,
E4A814DF1C7338EB00BF85AC /* IdChangeInvalidation.h */,
1C0106FE192594DF008A4201 /* InlineTextBoxStyle.cpp */,
Expand Down Expand Up @@ -34473,6 +34478,7 @@
26EA89A71B4F2B75008C5FD2 /* HashableActionList.h in Headers */,
8482B7461198C35400BFB005 /* HashChangeEvent.h in Headers */,
A8748BE012CBF2DC001FBA41 /* HashTools.h in Headers */,
E4ED3ECC2768A51D00F17AC8 /* HasSelectorFilter.h in Headers */,
CD3EEF3D25799FB5006563BB /* HdrMetadataType.h in Headers */,
CDA595932146DEC300A84185 /* HEVCUtilities.h in Headers */,
CDA595982146DF7800A84185 /* HEVCUtilitiesCocoa.h in Headers */,
Expand Down
85 changes: 65 additions & 20 deletions Source/WebCore/css/SelectorChecker.cpp
Expand Up @@ -1249,12 +1249,31 @@ bool SelectorChecker::matchSelectorList(CheckingContext& checkingContext, const

bool SelectorChecker::matchHasPseudoClass(CheckingContext& checkingContext, const Element& element, const CSSSelector& hasSelector) const
{
auto matchElement = Style::computeHasPseudoClassMatchElement(hasSelector);

auto canMatch = [&] {
switch (matchElement) {
case Style::MatchElement::HasChild:
case Style::MatchElement::HasDescendant:
return !!element.firstElementChild();
case Style::MatchElement::HasSibling:
case Style::MatchElement::HasSiblingDescendant:
return !!element.nextElementSibling();
default:
return true;
};
};

// See if there are any elements that this :has() selector could match.
if (!canMatch())
return false;

auto* cache = checkingContext.selectorMatchingState ? &checkingContext.selectorMatchingState->hasPseudoClassMatchCache : nullptr;

Style::HasPseudoClassMatch* cachedMatch = nullptr;
if (cache) {
cachedMatch = &cache->add(Style::makeHasPseudoClassCacheKey(hasSelector, element), Style::HasPseudoClassMatch::None).iterator->value;
switch (*cachedMatch) {
auto checkForCachedMatch = [&]() -> std::optional<bool> {
if (!cache)
return { };
switch (cache->get(Style::makeHasPseudoClassCacheKey(element, hasSelector))) {
case Style::HasPseudoClassMatch::Matches:
return true;
case Style::HasPseudoClassMatch::Fails:
Expand All @@ -1263,6 +1282,34 @@ bool SelectorChecker::matchHasPseudoClass(CheckingContext& checkingContext, cons
case Style::HasPseudoClassMatch::None:
break;
}
return { };
};

// See if we know the result already.
if (auto match = checkForCachedMatch())
return *match;

auto filterForElement = [&]() -> Style::HasSelectorFilter* {
if (!checkingContext.selectorMatchingState)
return nullptr;
auto type = Style::HasSelectorFilter::typeForMatchElement(matchElement);
if (!type)
return nullptr;
auto& filtersMap = checkingContext.selectorMatchingState->hasPseudoClassSelectorFilters;
auto addResult = filtersMap.add(Style::makeHasPseudoClassFilterKey(element, *type), std::unique_ptr<Style::HasSelectorFilter>());
// Only build a filter if the same element gets checked second time with a different selector (misses the match cache).
if (addResult.isNewEntry)
return nullptr;

if (!addResult.iterator->value)
addResult.iterator->value = makeUnique<Style::HasSelectorFilter>(element, *type);
return addResult.iterator->value.get();
};

// Check if the bloom filter rejects this selector
if (auto* filter = filterForElement()) {
if (filter->reject(hasSelector))
return false;
}

SelectorChecker hasChecker(element.document());
Expand All @@ -1283,8 +1330,8 @@ bool SelectorChecker::matchHasPseudoClass(CheckingContext& checkingContext, cons
auto checkDescendants = [&](const Element& descendantRoot) {
for (auto it = descendantsOfType<Element>(descendantRoot).begin(); it;) {
auto& descendant = *it;
if (cache) {
auto key = Style::makeHasPseudoClassCacheKey(hasSelector, descendant);
if (cache && descendant.firstElementChild()) {
auto key = Style::makeHasPseudoClassCacheKey(descendant, hasSelector);
if (cache->get(key) == Style::HasPseudoClassMatch::FailsSubtree) {
it.traverseNextSkippingChildren();
continue;
Expand All @@ -1300,8 +1347,6 @@ bool SelectorChecker::matchHasPseudoClass(CheckingContext& checkingContext, cons
};

auto match = [&] {
auto matchElement = Style::computeHasPseudoClassMatchElement(hasSelector);

switch (matchElement) {
// :has(> .child)
case Style::MatchElement::HasChild:
Expand All @@ -1312,18 +1357,17 @@ bool SelectorChecker::matchHasPseudoClass(CheckingContext& checkingContext, cons
break;
// :has(.descendant)
case Style::MatchElement::HasDescendant: {
if (!element.firstElementChild())
return false;
if (cache) {
// See if we already know this descendant selector doesn't match in this subtree.
for (auto* ancestor = element.parentElement(); ancestor; ancestor = ancestor->parentElement()) {
auto key = Style::makeHasPseudoClassCacheKey(hasSelector, *ancestor);
auto key = Style::makeHasPseudoClassCacheKey(*ancestor, hasSelector);
if (cache->get(key) == Style::HasPseudoClassMatch::FailsSubtree)
return false;
}
}
if (checkDescendants(element))
return true;

break;
}
// FIXME: Add a separate case for adjacent combinator.
Expand Down Expand Up @@ -1354,15 +1398,16 @@ bool SelectorChecker::matchHasPseudoClass(CheckingContext& checkingContext, cons

auto result = match();

if (cachedMatch) {
*cachedMatch = [&] {
if (result)
return Style::HasPseudoClassMatch::Matches;
if (matchedInsideScope)
return Style::HasPseudoClassMatch::Fails;
return Style::HasPseudoClassMatch::FailsSubtree;
}();
}
auto matchTypeForCache = [&] {
if (result)
return Style::HasPseudoClassMatch::Matches;
if (matchedInsideScope)
return Style::HasPseudoClassMatch::Fails;
return Style::HasPseudoClassMatch::FailsSubtree;
};

if (cache)
cache->add(Style::makeHasPseudoClassCacheKey(element, hasSelector), matchTypeForCache());

return result;
}
Expand Down
18 changes: 5 additions & 13 deletions Source/WebCore/css/SelectorFilter.cpp
Expand Up @@ -45,7 +45,7 @@ static bool isExcludedAttribute(const AtomString& name)
return name == HTMLNames::classAttr->localName() || name == HTMLNames::idAttr->localName() || name == HTMLNames::styleAttr->localName();
}

static inline void collectElementIdentifierHashes(const Element& element, Vector<unsigned, 4>& identifierHashes)
void SelectorFilter::collectElementIdentifierHashes(const Element& element, Vector<unsigned, 4>& identifierHashes)
{
AtomString tagLowercaseLocalName = element.localName().convertToASCIILowercase();
identifierHashes.append(tagLowercaseLocalName.impl()->existingHash() * TagNameSalt);
Expand Down Expand Up @@ -134,15 +134,7 @@ void SelectorFilter::popParentsUntil(Element* parent)
}
}

struct CollectedSelectorHashes {
using HashVector = Vector<unsigned, 8>;
HashVector ids;
HashVector classes;
HashVector tags;
HashVector attributes;
};

static inline void collectSimpleSelectorHash(CollectedSelectorHashes& collectedHashes, const CSSSelector& selector)
void SelectorFilter::collectSimpleSelectorHash(CollectedSelectorHashes& collectedHashes, const CSSSelector& selector)
{
switch (selector.match()) {
case CSSSelector::Id:
Expand Down Expand Up @@ -176,7 +168,7 @@ static inline void collectSimpleSelectorHash(CollectedSelectorHashes& collectedH
}
}

static CollectedSelectorHashes collectSelectorHashes(const CSSSelector& rightmostSelector)
auto SelectorFilter::collectSelectorHashes(const CSSSelector& rightmostSelector) -> CollectedSelectorHashes
{
CollectedSelectorHashes collectedHashes;

Expand Down Expand Up @@ -210,9 +202,9 @@ static CollectedSelectorHashes collectSelectorHashes(const CSSSelector& rightmos
return collectedHashes;
}

static SelectorFilter::Hashes chooseSelectorHashesForFilter(const CollectedSelectorHashes& collectedSelectorHashes)
auto SelectorFilter::chooseSelectorHashesForFilter(const CollectedSelectorHashes& collectedSelectorHashes) -> Hashes
{
SelectorFilter::Hashes resultHashes;
Hashes resultHashes;
unsigned index = 0;

auto addIfNew = [&] (unsigned hash) {
Expand Down
13 changes: 13 additions & 0 deletions Source/WebCore/css/SelectorFilter.h
Expand Up @@ -50,8 +50,21 @@ class SelectorFilter {
bool fastRejectSelector(const Hashes&) const;
static Hashes collectHashes(const CSSSelector&);

static void collectElementIdentifierHashes(const Element&, Vector<unsigned, 4>&);

struct CollectedSelectorHashes {
using HashVector = Vector<unsigned, 8>;
HashVector ids;
HashVector classes;
HashVector tags;
HashVector attributes;
};
static void collectSimpleSelectorHash(CollectedSelectorHashes&, const CSSSelector&);

private:
void initializeParentStack(Element& parent);
static CollectedSelectorHashes collectSelectorHashes(const CSSSelector& rightmostSelector);
static Hashes chooseSelectorHashesForFilter(const CollectedSelectorHashes&);

struct ParentStackFrame {
ParentStackFrame() : element(0) { }
Expand Down

0 comments on commit 596fdf7

Please sign in to comment.