Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add bloom filter to reject :has() argument selector checking
For child or descendant relationships, WebKit applies bloom filter to reject :has() argument selector checking quickly by creating a bloom filter for the :has() anchor element with the identifier hashes from the children or descendants of the element. - https://commits.webkit.org/245283@main Similar to the above, we can apply the fast rejecting approach so that we can skip :has() argument selector checking early if the argument selector doesn't match any elements in the :has() argument checking traversal. This CL adds a bloom filter class 'CheckPseudoHasFastRejectFilter' to reject :has() argument checking. The filter doesn't need the counting or parent frame functionality. For better performance and memory consumption, this CL uses BloomFilter (non-counting filter) instead of SelectorFilter or CountingBloomFilter. The filter provides these methods: - CollectPseudoHasArgumentHashes(): Collect identifier hashes from a :has() argument selector. - AddElementIdentifierHashes(): Add element identifier hashes to the filter. - FastReject(): check whether the :has() argument selector checking can be rejected early. (return true if the filter doesn't contain any :has() argument identifier hash. CheckPseudoHasArgumentContext calls CollectPseudoHasArgumentHashes() to collect identifier hashes from :has() argument. The sources of the identifier hash are tag-name, id, class-value and attribute-name of a :has() argument selector or an element. This CL doesn't have a logic of caching the filter and using it while :has() pseudo class selector checking. The logic will be added by a separated CL. Bug: 1341893 Change-Id: I6d7d23ea7143c3f94bb3c47d466938000a9d7fb5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3744326 Reviewed-by: Rune Lillesveen <futhark@chromium.org> Commit-Queue: Byungwoo Lee <blee@igalia.com> Cr-Commit-Position: refs/heads/main@{#1032443}
- Loading branch information
Showing
6 changed files
with
332 additions
and
9 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
118 changes: 118 additions & 0 deletions
118
third_party/blink/renderer/core/css/check_pseudo_has_fast_reject_filter.cc
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,118 @@ | ||
// Copyright 2022 The Chromium Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
#include "third_party/blink/renderer/core/css/check_pseudo_has_fast_reject_filter.h" | ||
|
||
#include "third_party/blink/renderer/core/css/css_selector.h" | ||
|
||
namespace blink { | ||
|
||
namespace { | ||
|
||
// Salt to separate otherwise identical string hashes so a class-selector like | ||
// .article won't match <article> elements. | ||
enum { kTagNameSalt = 13, kIdSalt = 17, kClassSalt = 19, kAttributeSalt = 23 }; | ||
|
||
inline bool IsExcludedAttribute(const AtomicString& name) { | ||
return name == html_names::kClassAttr.LocalName() || | ||
name == html_names::kIdAttr.LocalName() || | ||
name == html_names::kStyleAttr.LocalName(); | ||
} | ||
|
||
inline unsigned GetTagHash(const AtomicString& tag_name) { | ||
return tag_name.Impl()->ExistingHash() * kTagNameSalt; | ||
} | ||
|
||
inline unsigned GetClassHash(const AtomicString& class_name) { | ||
return class_name.Impl()->ExistingHash() * kClassSalt; | ||
} | ||
|
||
inline unsigned GetIdHash(const AtomicString& id) { | ||
return id.Impl()->ExistingHash() * kIdSalt; | ||
} | ||
|
||
inline unsigned GetAttributeHash(const AtomicString& attribute_name) { | ||
return attribute_name.Impl()->ExistingHash() * kAttributeSalt; | ||
} | ||
|
||
} // namespace | ||
|
||
void CheckPseudoHasFastRejectFilter::AddElementIdentifierHashes( | ||
const Element& element) { | ||
filter_.Add(GetTagHash(element.LocalNameForSelectorMatching())); | ||
if (element.HasID()) | ||
filter_.Add(GetIdHash(element.IdForStyleResolution())); | ||
if (element.HasClass()) { | ||
const SpaceSplitString& class_names = element.ClassNames(); | ||
wtf_size_t count = class_names.size(); | ||
for (wtf_size_t i = 0; i < count; ++i) | ||
filter_.Add(GetClassHash(class_names[i])); | ||
} | ||
AttributeCollection attributes = element.AttributesWithoutUpdate(); | ||
for (const auto& attribute_item : attributes) { | ||
auto attribute_name = attribute_item.LocalName(); | ||
if (IsExcludedAttribute(attribute_name)) | ||
continue; | ||
auto lower = attribute_name.IsLowerASCII() ? attribute_name | ||
: attribute_name.LowerASCII(); | ||
filter_.Add(GetAttributeHash(lower)); | ||
} | ||
} | ||
|
||
bool CheckPseudoHasFastRejectFilter::FastReject( | ||
const Vector<unsigned>& pseudo_has_argument_hashes) const { | ||
if (pseudo_has_argument_hashes.IsEmpty()) | ||
return false; | ||
for (unsigned hash : pseudo_has_argument_hashes) { | ||
if (!filter_.MayContain(hash)) | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
// static | ||
void CheckPseudoHasFastRejectFilter::CollectPseudoHasArgumentHashes( | ||
Vector<unsigned>& pseudo_has_argument_hashes, | ||
const CSSSelector* simple_selector) { | ||
DCHECK(simple_selector); | ||
switch (simple_selector->Match()) { | ||
case CSSSelector::kId: | ||
if (simple_selector->Value().IsEmpty()) | ||
break; | ||
pseudo_has_argument_hashes.push_back(GetIdHash(simple_selector->Value())); | ||
break; | ||
case CSSSelector::kClass: | ||
if (simple_selector->Value().IsEmpty()) | ||
break; | ||
pseudo_has_argument_hashes.push_back( | ||
GetClassHash(simple_selector->Value())); | ||
break; | ||
case CSSSelector::kTag: | ||
if (simple_selector->TagQName().LocalName() != | ||
CSSSelector::UniversalSelectorAtom()) { | ||
pseudo_has_argument_hashes.push_back( | ||
GetTagHash(simple_selector->TagQName().LocalName())); | ||
} | ||
break; | ||
case CSSSelector::kAttributeExact: | ||
case CSSSelector::kAttributeSet: | ||
case CSSSelector::kAttributeList: | ||
case CSSSelector::kAttributeContain: | ||
case CSSSelector::kAttributeBegin: | ||
case CSSSelector::kAttributeEnd: | ||
case CSSSelector::kAttributeHyphen: { | ||
auto attribute_name = simple_selector->Attribute().LocalName(); | ||
if (IsExcludedAttribute(attribute_name)) | ||
break; | ||
auto lower_name = attribute_name.IsLowerASCII() | ||
? attribute_name | ||
: attribute_name.LowerASCII(); | ||
pseudo_has_argument_hashes.push_back(GetAttributeHash(lower_name)); | ||
} break; | ||
default: | ||
break; | ||
} | ||
} | ||
|
||
} // namespace blink |
59 changes: 59 additions & 0 deletions
59
third_party/blink/renderer/core/css/check_pseudo_has_fast_reject_filter.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,59 @@ | ||
// Copyright 2022 The Chromium Authors. All rights reserved. | ||
// 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_CHECK_PSEUDO_HAS_FAST_REJECT_FILTER_H_ | ||
#define THIRD_PARTY_BLINK_RENDERER_CORE_CSS_CHECK_PSEUDO_HAS_FAST_REJECT_FILTER_H_ | ||
|
||
#include "third_party/blink/renderer/core/core_export.h" | ||
#include "third_party/blink/renderer/core/dom/element.h" | ||
#include "third_party/blink/renderer/platform/wtf/bloom_filter.h" | ||
|
||
namespace blink { | ||
|
||
// CheckPseudoHasFastRejectFilter uses a bloom filter for quickly rejecting | ||
// :has() argument selector checking. | ||
// | ||
// We can create the bloom filter by adding identifier hashes (tag hash, id hash | ||
// and class hashes) of all elements in the :has() argument checking traversal. | ||
// | ||
// Once the filter have been created, we can cheaply check whether a :has() | ||
// argument selector possibly matches one of the elements in the :has() argument | ||
// checking traversal by checking whether the filter contains all the identifier | ||
// hashes from the :has() argument selector. | ||
// | ||
// For example, assume this tree: | ||
// | ||
// <div id="has_anchor"> | ||
// <div id="child"> | ||
// <span class="a"> | ||
// | ||
// When we check ':has(.a .b)' on '#has_anchor', the bloom filter will contain | ||
// hashes corresponding to 'div', 'span', '#child' and '.a'. From the :has() | ||
// argument selector '.a .b', we will collect identifier hashes corresponding to | ||
// '.a' and '.b'. Then, we will look up the hashes from argument selector in the | ||
// bloom filter and get negative result proving that the argument selector | ||
// '.a .b' doesn't match any descendants of '#has_anchor' since the bloom filter | ||
// doesn't contain the hash for '.b'. | ||
class CORE_EXPORT CheckPseudoHasFastRejectFilter { | ||
USING_FAST_MALLOC(CheckPseudoHasFastRejectFilter); | ||
|
||
public: | ||
CheckPseudoHasFastRejectFilter() = default; | ||
CheckPseudoHasFastRejectFilter(CheckPseudoHasFastRejectFilter&) = delete; | ||
|
||
static void CollectPseudoHasArgumentHashes( | ||
Vector<unsigned>& pseudo_has_argument_hashes, | ||
const CSSSelector* simple_selector); | ||
|
||
void AddElementIdentifierHashes(const Element& element); | ||
|
||
bool FastReject(const Vector<unsigned>& pseudo_has_argument_hashes) const; | ||
|
||
private: | ||
WTF::BloomFilter<12> filter_; | ||
}; | ||
|
||
} // namespace blink | ||
|
||
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_CSS_CHECK_PSEUDO_HAS_FAST_REJECT_FILTER_H_ |
132 changes: 132 additions & 0 deletions
132
third_party/blink/renderer/core/css/check_pseudo_has_fast_reject_filter_test.cc
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,132 @@ | ||
// Copyright 2022 The Chromium Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
#include "third_party/blink/renderer/core/css/check_pseudo_has_fast_reject_filter.h" | ||
|
||
#include "testing/gtest/include/gtest/gtest.h" | ||
#include "third_party/blink/renderer/core/css/check_pseudo_has_argument_context.h" | ||
#include "third_party/blink/renderer/core/css/css_selector_list.h" | ||
#include "third_party/blink/renderer/core/css/css_test_helpers.h" | ||
#include "third_party/blink/renderer/core/dom/document.h" | ||
#include "third_party/blink/renderer/core/html_names.h" | ||
#include "third_party/blink/renderer/core/testing/page_test_base.h" | ||
|
||
namespace blink { | ||
|
||
class CheckPseudoHasFastRejectFilterTest : public PageTestBase { | ||
protected: | ||
struct ElementInfo { | ||
const char* tag_name; | ||
const char* id; | ||
const char* class_names; | ||
const char* attribute_name; | ||
const char* attribute_value; | ||
}; | ||
|
||
template <unsigned length> | ||
void AddElementIdentifierHashes( | ||
CheckPseudoHasFastRejectFilter& filter, | ||
const ElementInfo (&element_info_list)[length]) { | ||
for (unsigned i = 0; i < length; i++) { | ||
NonThrowableExceptionState no_exceptions; | ||
Element* element = GetDocument().CreateElementForBinding( | ||
element_info_list[i].tag_name, nullptr, no_exceptions); | ||
element->setAttribute(html_names::kIdAttr, element_info_list[i].id); | ||
element->setAttribute(html_names::kClassAttr, | ||
element_info_list[i].class_names); | ||
element->setAttribute(element_info_list[i].attribute_name, | ||
element_info_list[i].attribute_value); | ||
filter.AddElementIdentifierHashes(*element); | ||
} | ||
} | ||
|
||
bool CheckFastReject(CheckPseudoHasFastRejectFilter& filter, | ||
const char* selector_text) { | ||
CSSSelectorList selector_list = | ||
css_test_helpers::ParseSelectorList(selector_text); | ||
|
||
EXPECT_EQ(selector_list.First()->GetPseudoType(), CSSSelector::kPseudoHas); | ||
|
||
CheckPseudoHasArgumentContext context( | ||
selector_list.First()->SelectorList()->First()); | ||
|
||
return filter.FastReject(context.GetPseudoHasArgumentHashes()); | ||
} | ||
}; | ||
|
||
TEST_F(CheckPseudoHasFastRejectFilterTest, CheckFastReject) { | ||
CheckPseudoHasFastRejectFilter filter; | ||
|
||
AddElementIdentifierHashes( | ||
filter, {{/* tag_name */ "div", /* id */ "d1", /* class_names */ "a", | ||
/* attribute_name */ "attr1", /* attribute_value */ "val1"}, | ||
{/* tag_name */ "div", /* id */ "d2", /* class_names */ "b", | ||
/* attribute_name */ "attr2", /* attribute_value */ "val2"}, | ||
{/* tag_name */ "span", /* id */ "s1", /* class_names */ "c", | ||
/* attribute_name */ "attr3", /* attribute_value */ "val3"}, | ||
{/* tag_name */ "span", /* id */ "s2", /* class_names */ "d", | ||
/* attribute_name */ "attr4", /* attribute_value */ "val4"}}); | ||
|
||
EXPECT_FALSE(CheckFastReject(filter, ":has(div)")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(span)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(h1)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(#div)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(.div)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has([div])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has([div=div])")); | ||
|
||
EXPECT_FALSE(CheckFastReject(filter, ":has(#d1)")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(#d2)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(#d3)")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(#s1)")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(#s2)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(#s3)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(d1)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(.d1)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has([d1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has([d1=d1])")); | ||
|
||
EXPECT_FALSE(CheckFastReject(filter, ":has(.a)")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(.b)")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(.c)")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(.d)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(.e)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(a)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(#a)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has([a])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has([a=a])")); | ||
|
||
EXPECT_FALSE(CheckFastReject(filter, ":has([attr1])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has([attr2])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has([attr3])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has([attr4])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has([attr1=x])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has([attr2=x])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has([attr3=x])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has([attr4=x])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(attr1)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(#attr1)")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(.attr1)")); | ||
|
||
EXPECT_FALSE(CheckFastReject(filter, ":has(div#d1.a[attr1=val1])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(span#d1.a[attr1=val1])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(div#s1.a[attr1=val1])")); | ||
EXPECT_FALSE(CheckFastReject(filter, ":has(div#d1.c[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(h1#d1.a[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#d3.a[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#d1.e[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#d1.a[attr5=val1])")); | ||
|
||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#div.a[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#d1.div[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#d1.a[div=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(d1#d1.a[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#d1.d1[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#d1.a[d1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(a#d1.a[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#a.a[attr1=val1])")); | ||
EXPECT_TRUE(CheckFastReject(filter, ":has(div#d1.a[a=val1])")); | ||
} | ||
|
||
} // namespace blink |