Skip to content

Commit

Permalink
Add bloom filter to reject :has() argument selector checking
Browse files Browse the repository at this point in the history
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
byung-woo authored and Chromium LUCI CQ committed Aug 8, 2022
1 parent 08d82ec commit 9fb0775
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 9 deletions.
3 changes: 3 additions & 0 deletions third_party/blink/renderer/core/css/build.gni
Expand Up @@ -38,6 +38,8 @@ blink_core_sources_css = [
"check_pseudo_has_argument_context.h",
"check_pseudo_has_cache_scope.cc",
"check_pseudo_has_cache_scope.h",
"check_pseudo_has_fast_reject_filter.cc",
"check_pseudo_has_fast_reject_filter.h",
"clip_path_paint_image_generator.cc",
"clip_path_paint_image_generator.h",
"color_scheme_flags.h",
Expand Down Expand Up @@ -688,6 +690,7 @@ blink_core_tests_css = [
"cascade_layer_test.cc",
"check_pseudo_has_argument_context_test.cc",
"check_pseudo_has_cache_scope_context_test.cc",
"check_pseudo_has_fast_reject_filter_test.cc",
"computed_style_css_value_mapping_test.cc",
"container_query_evaluator_test.cc",
"container_query_test.cc",
Expand Down
Expand Up @@ -4,27 +4,28 @@

#include "third_party/blink/renderer/core/css/check_pseudo_has_argument_context.h"

#include "third_party/blink/renderer/core/css/check_pseudo_has_fast_reject_filter.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"

namespace blink {

namespace {

inline const CSSSelector* GetCurrentRelationAndNextCompound(
const CSSSelector*
CheckPseudoHasArgumentContext::GetCurrentRelationAndNextCompound(
const CSSSelector* compound_selector,
CSSSelector::RelationType& relation) {
DCHECK(compound_selector);
for (; compound_selector;
compound_selector = compound_selector->TagHistory()) {
relation = compound_selector->Relation();
for (const CSSSelector* simple_selector = compound_selector; simple_selector;
simple_selector = simple_selector->TagHistory()) {
CheckPseudoHasFastRejectFilter::CollectPseudoHasArgumentHashes(
pseudo_has_argument_hashes_, simple_selector);

relation = simple_selector->Relation();
if (relation != CSSSelector::kSubSelector)
return compound_selector->TagHistory();
return simple_selector->TagHistory();
}
return nullptr;
}

} // namespace

CheckPseudoHasArgumentContext::CheckPseudoHasArgumentContext(
const CSSSelector* selector)
: has_argument_(selector) {
Expand Down
Expand Up @@ -94,10 +94,18 @@ class CORE_EXPORT CheckPseudoHasArgumentContext {

const CSSSelector* HasArgument() const { return has_argument_; }

const Vector<unsigned>& GetPseudoHasArgumentHashes() const {
return pseudo_has_argument_hashes_;
}

private:
const static int kInfiniteDepth = std::numeric_limits<int>::max();
const static int kInfiniteAdjacentDistance = std::numeric_limits<int>::max();

inline const CSSSelector* GetCurrentRelationAndNextCompound(
const CSSSelector* compound_selector,
CSSSelector::RelationType& relation);

// Indicate the :has argument relative type and subtree traversal scope.
// If 'adjacent_distance_limit' is integer max, it means that all the
// adjacent subtrees need to be traversed. otherwise, it means that it is
Expand Down Expand Up @@ -226,6 +234,8 @@ class CORE_EXPORT CheckPseudoHasArgumentContext {
CheckPseudoHasArgumentTraversalScope traversal_scope_;
SiblingsAffectedByHasFlags siblings_affected_by_has_flags_;
const CSSSelector* has_argument_;

Vector<unsigned> pseudo_has_argument_hashes_;
};

// Subtree traversal iterator class for :has() argument checking. To solve the
Expand Down
@@ -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
@@ -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_
@@ -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

0 comments on commit 9fb0775

Please sign in to comment.