Skip to content

Commit

Permalink
Use CheckPseudoHasFastRejectFilter to reject :has() argument checking
Browse files Browse the repository at this point in the history
Use CheckPseudoHasFastRejectFilter before checking :has() argument
selector to reject the argument checking early.

To minimize the overhead of bloom filter allocation and element
identifier hash collection, the bloom filter in
CheckPseudoHasFastRejectFilter will not be allocated at the first
:has() argument checking on a :has() anchor element.

If there is only one :has() argument selector that checks the anchor
element, the fast reject filter will not be used since the argument
check is redundant if it cannot be rejected.

The bloom filter will be allocated and element identifier hashes will be
added to the filter at the second :has() argument checking on the same
:has() anchor element. The created fast reject filter will be stored
in CheckPseudoHasCacheScope to minimize the overhead of bloom filter
allocation and element identifier cache collection.

Once we used the fast reject filter for a :has() anchor element, mark
the element as AffectedByMultipleHas so that we always create and use
the fast reject filter for the :has() anchor element.

There is a relatively small amount of regression on single :has() rule
invalidation:
- HasDescendantInvalidationWithoutNonMatchingHasRule.html
  https://pinpoint-dot-chromeperf.appspot.com/job/1662f8406a0000
  3,465.147 -> 3,291.691 (-5.0%)

But there is a significant performance enhancement on multiple
non-matching :has() rule invalidation by avoiding unnecessary :has()
argument selector checking:
- HasDescendantInvalidationWith1NonMatchingHasRule.html
  https://pinpoint-dot-chromeperf.appspot.com/job/133b68286a0000
  1,677.276 -> 2,649.180 (+57.9%)
- HasDescendantInvalidationWithMultipleNonMatchingHasRules.html
  https://pinpoint-dot-chromeperf.appspot.com/job/13a515bfaa0000
  545.203 -> 2,571.323 (+371.6%)

Bug: 1341893
Change-Id: Icb99cbc6596f64ea585669ce543d612ec508a495
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3744327
Commit-Queue: Byungwoo Lee <blee@igalia.com>
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1033383}
  • Loading branch information
byung-woo authored and Chromium LUCI CQ committed Aug 10, 2022
1 parent d370bf5 commit f3aa984
Show file tree
Hide file tree
Showing 14 changed files with 826 additions and 141 deletions.
Expand Up @@ -95,6 +95,8 @@ CheckPseudoHasArgumentContext::CheckPseudoHasArgumentContext(
}
}
DCHECK_NE(leftmost_relation_, CSSSelector::kSubSelector);
DCHECK_LE(adjacent_distance_limit_, kInfiniteAdjacentDistance);
DCHECK_LE(depth_limit_, kInfiniteDepth);

switch (leftmost_relation_) {
case CSSSelector::kRelativeDescendant:
Expand Down
Expand Up @@ -58,8 +58,19 @@ enum CheckPseudoHasArgumentTraversalScope {
// (.e.g. :has(~ .a > .b), :has(+ .a ~ .b > .c),
// :has(~ .a > .b ~ .c), :has(+ .a ~ .b > .c ~ .d),
kAllNextSiblingsFixedDepthDescendants,

kTraversalScopeMax = kAllNextSiblingsFixedDepthDescendants,
};

// Unique value of each traversal type. The value can be used as a key of
// fast reject filter cache.
//
// These 3 values are stored by dividing the 4-byte field by:
// - depth limit : 0 ~ 13 (14bits)
// - adjacent distance limit : 14 ~ 27 (14 bits)
// - traversal scope : 28 ~ 31 (4 bits)
using CheckPseudoHasArgumentTraversalType = uint32_t;

class CORE_EXPORT CheckPseudoHasArgumentContext {
STACK_ALLOCATED();

Expand Down Expand Up @@ -98,9 +109,24 @@ class CORE_EXPORT CheckPseudoHasArgumentContext {
return pseudo_has_argument_hashes_;
}

CheckPseudoHasArgumentTraversalType TraversalType() const {
return depth_limit_ | (adjacent_distance_limit_ << kDepthBits) |
(traversal_scope_ << (kDepthBits + kAdjacentBits));
}

private:
const static int kInfiniteDepth = std::numeric_limits<int>::max();
const static int kInfiniteAdjacentDistance = std::numeric_limits<int>::max();
const static size_t kDepthBits = 14;
const static size_t kAdjacentBits = 14;
const static size_t kTraversalScopeBits = 4;

const static int kInfiniteDepth = (1 << kDepthBits) - 1;
const static int kInfiniteAdjacentDistance = (1 << kAdjacentBits) - 1;

static_assert(kTraversalScopeMax <= ((1 << kTraversalScopeBits) - 1),
"traversal scope size check");
static_assert((kDepthBits + kAdjacentBits + kTraversalScopeBits) <=
sizeof(CheckPseudoHasArgumentTraversalType) * 8,
"traversal type size check");

inline const CSSSelector* GetCurrentRelationAndNextCompound(
const CSSSelector* compound_selector,
Expand Down Expand Up @@ -236,6 +262,8 @@ class CORE_EXPORT CheckPseudoHasArgumentContext {
const CSSSelector* has_argument_;

Vector<unsigned> pseudo_has_argument_hashes_;

friend class CheckPseudoHasArgumentContextTest;
};

// Subtree traversal iterator class for :has() argument checking. To solve the
Expand Down

Large diffs are not rendered by default.

Expand Up @@ -52,6 +52,25 @@ ElementCheckPseudoHasResultMap& CheckPseudoHasCacheScope::GetResultMap(
return *entry.stored_value->value;
}

// static
ElementCheckPseudoHasFastRejectFilterMap&
CheckPseudoHasCacheScope::GetFastRejectFilterMap(
const Document* document,
CheckPseudoHasArgumentTraversalType traversal_type) {
DCHECK(document);
DCHECK(document->GetCheckPseudoHasCacheScope());

auto entry = document->GetCheckPseudoHasCacheScope()
->GetFastRejectFilterCache()
.insert(traversal_type, nullptr);
if (entry.is_new_entry) {
entry.stored_value->value =
MakeGarbageCollected<ElementCheckPseudoHasFastRejectFilterMap>();
}
DCHECK(entry.stored_value->value);
return *entry.stored_value->value;
}

CheckPseudoHasCacheScope::Context::Context(
const Document* document,
const CheckPseudoHasArgumentContext& argument_context)
Expand All @@ -64,6 +83,9 @@ CheckPseudoHasCacheScope::Context::Context(
cache_allowed_ = true;
result_map_ = &CheckPseudoHasCacheScope::GetResultMap(
document, argument_context.HasArgument());
fast_reject_filter_map_ =
&CheckPseudoHasCacheScope::GetFastRejectFilterMap(
document, argument_context.TraversalType());
break;
default:
cache_allowed_ = false;
Expand Down Expand Up @@ -225,4 +247,21 @@ bool CheckPseudoHasCacheScope::Context::AlreadyChecked(Element* element) const {
return false;
}

CheckPseudoHasFastRejectFilter&
CheckPseudoHasCacheScope::Context::EnsureFastRejectFilter(Element* element,
bool& is_new_entry) {
DCHECK(element);
DCHECK(cache_allowed_);
DCHECK(fast_reject_filter_map_);

auto entry = fast_reject_filter_map_->insert(element, nullptr);
is_new_entry = entry.is_new_entry;
if (entry.is_new_entry) {
entry.stored_value->value =
std::make_unique<CheckPseudoHasFastRejectFilter>();
}
DCHECK(entry.stored_value->value);
return *entry.stored_value->value.get();
}

} // namespace blink
65 changes: 57 additions & 8 deletions third_party/blink/renderer/core/css/check_pseudo_has_cache_scope.h
Expand Up @@ -6,6 +6,8 @@
#define THIRD_PARTY_BLINK_RENDERER_CORE_CSS_CHECK_PSEUDO_HAS_CACHE_SCOPE_H_

#include "third_party/blink/renderer/core/core_export.h"
#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.h"
#include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h"

Expand Down Expand Up @@ -141,7 +143,7 @@ enum CheckPseudoHasResult : uint8_t {
// because we can get the result of ':has(.a)' from the cache with the cache
// key '.a'.
//
// The :has() checking result cache uses 2 dimensional hash map to store the
// The :has() checking result cache uses a 2 dimensional hash map to store the
// result.
// - hashmap[<argument-selector>][<element>] = <result>
//
Expand All @@ -153,12 +155,39 @@ using ElementCheckPseudoHasResultMap =
using CheckPseudoHasResultCache =
HeapHashMap<String, Member<ElementCheckPseudoHasResultMap>>;

// The :has() result cache keeps a bloom filter for rejecting :has() argument
// selector checking.
//
// The element identifier hashes in the bloom filter depend on the relationship
// between the :has() anchor element and the :has() argument subject element.
// The relationship can be categorized by this information in
// CheckPseudoHasArgumentContext.
// - traversal scope
// - adjacent limit
// - depth limit
// (Please refer the comment of CheckPseudoHasArgumentTraversalType)
//
// The CheckPseudoHasFastRejectFilterCache uses a 2 dimensional hash map to
// store the filter.
// - hashmap[<traversal type>][<element>] = <filter>
//
// ElementCheckPseudoHasFastRejectFilterMap is a hash map that stores the
// filter for each element.
// - hashmap[<element>] = <filter>
using ElementCheckPseudoHasFastRejectFilterMap =
HeapHashMap<Member<const Element>,
std::unique_ptr<CheckPseudoHasFastRejectFilter>>;
using CheckPseudoHasFastRejectFilterCache =
HeapHashMap<CheckPseudoHasArgumentTraversalType,
Member<ElementCheckPseudoHasFastRejectFilterMap>>;

// CheckPseudoHasCacheScope is the stack-allocated scoping class for :has()
// pseudo class checking result cache.
// pseudo class checking result cache and :has() pseudo class checking fast
// reject filter cache.
//
// This class has hashmap to hold the checking result, so the lifecycle of the
// cache follows the lifecycle of the CheckPseudoHasCacheScope instance.
// (The hashmap for caching will be created at the construction of a
// This class has hashmap to hold the checking result and filter, so the
// lifecycle of the caches follow the lifecycle of the CheckPseudoHasCacheScope
// instance. (The hashmap for caching will be created at the construction of a
// CheckPseudoHasCacheScope instance, and removed at the destruction of the
// instance)
//
Expand Down Expand Up @@ -198,8 +227,10 @@ class CORE_EXPORT CheckPseudoHasCacheScope {
explicit CheckPseudoHasCacheScope(Document*);
~CheckPseudoHasCacheScope();

// Context provides getter and setter of the cached :has()
// pseudo class checking result in ElementCheckPseudoHasResultMap.
// Context provides getter and setter of the following cache items.
// - :has() pseudo class checking result in ElementCheckPseudoHasResultMap
// - :has() pseudo class checking fast reject filter in
// ElementCheckPseudoHasFastRejectFilterMap.
class CORE_EXPORT Context {
STACK_ALLOCATED();

Expand All @@ -218,6 +249,9 @@ class CORE_EXPORT CheckPseudoHasCacheScope {

bool AlreadyChecked(Element*) const;

CheckPseudoHasFastRejectFilter& EnsureFastRejectFilter(Element*,
bool& is_new_entry);

inline bool CacheAllowed() const { return cache_allowed_; }

private:
Expand All @@ -231,20 +265,35 @@ class CORE_EXPORT CheckPseudoHasCacheScope {
bool HasSiblingsWithAllDescendantsOrNextSiblingsChecked(Element*) const;
bool HasAncestorsWithAllDescendantsOrNextSiblingsChecked(Element*) const;

size_t GetCacheCount() { return cache_allowed_ ? result_map_->size() : 0; }
size_t GetResultCacheCountForTesting() const {
return cache_allowed_ ? result_map_->size() : 0;
}

size_t GetFastRejectFilterCacheCountForTesting() const {
return cache_allowed_ ? fast_reject_filter_map_->size() : 0;
}

bool cache_allowed_;
ElementCheckPseudoHasResultMap* result_map_;
ElementCheckPseudoHasFastRejectFilterMap* fast_reject_filter_map_;
const CheckPseudoHasArgumentContext& argument_context_;
};

private:
static ElementCheckPseudoHasResultMap& GetResultMap(const Document*,
const CSSSelector*);
static ElementCheckPseudoHasFastRejectFilterMap& GetFastRejectFilterMap(
const Document*,
CheckPseudoHasArgumentTraversalType);

CheckPseudoHasResultCache& GetResultCache() { return result_cache_; }

CheckPseudoHasFastRejectFilterCache& GetFastRejectFilterCache() {
return fast_reject_filter_cache_;
}

CheckPseudoHasResultCache result_cache_;
CheckPseudoHasFastRejectFilterCache fast_reject_filter_cache_;

Document* document_;
};
Expand Down

0 comments on commit f3aa984

Please sign in to comment.