LibWeb: Bucket ancestor-filtered rules by ancestor hash

Add a late rule-cache bucket for selectors that do not expose a
current-element bucket but do expose ancestor-filter metadata. These
rules previously stayed in the global other bucket and relied on the
ancestor filter after broad enumeration.

Visit the new buckets only when the current ancestor bloom filter may
contain the bucket hash. Keep pseudo-class invalidation caches opted
out because those caches answer targeted invalidation questions rather
than normal style collection.

On the GitHub profile, other-bucket candidates fell from about 5.50k
to about 1.07k per 1k rule collection calls.
This commit is contained in:
Andreas Kling 2026-06-18 16:50:30 +02:00 committed by Andreas Kling
parent f39890bdc8
commit f45bf0e770
4 changed files with 65 additions and 20 deletions

View file

@ -71,7 +71,8 @@ static bool element_may_match_rule_containing_pseudo_class_in_style_scope(DOM::E
{
bool may_match = false;
auto abstract_element = DOM::AbstractElement { element };
style_scope.get_pseudo_class_rule_cache(pseudo_class).for_each_matching_rules(abstract_element, [&](auto const& matching_rules) {
Function<bool(u32)> const may_contain_ancestor_hash = [](u32) { return true; };
style_scope.get_pseudo_class_rule_cache(pseudo_class).for_each_matching_rules(abstract_element, may_contain_ancestor_hash, [&](auto const& matching_rules) {
for (auto const& matching_rule : matching_rules) {
if (pseudo_class_subject_may_match_element(element, matching_rule.selector, pseudo_class)) {
may_match = true;

View file

@ -507,17 +507,16 @@ Vector<StyleComputer::ScopedMatchingRule> StyleComputer::collect_matching_rules_
};
auto add_rules_from_cache = [&](RuleCache const& rule_cache, GC::Ptr<DOM::ShadowRoot const> rule_root) {
rule_cache.for_each_matching_rules(abstract_element, [&](auto const& matching_rules) {
Function<bool(u32)> may_contain_ancestor_hash = [&](u32 hash) { return m_ancestor_filter->may_contain(hash); };
rule_cache.for_each_matching_rules(abstract_element, may_contain_ancestor_hash, [&](auto const& matching_rules) {
add_rules_to_run(matching_rules, rule_root);
return IterationDecision::Continue;
});
if (!abstract_element.pseudo_element().has_value() && matching_pseudo_element_styles) {
for (auto const& pseudo_element_rules : rule_cache.rules_by_pseudo_element) {
(void)for_each_matching_rule_bucket(abstract_element, pseudo_element_rules, [&](auto const& matching_rules) {
add_rules_to_run(matching_rules, rule_root);
return IterationDecision::Continue;
});
}
rule_cache.for_each_matching_pseudo_element_rules(abstract_element, may_contain_ancestor_hash, [&](auto const& matching_rules) {
add_rules_to_run(matching_rules, rule_root);
return IterationDecision::Continue;
});
}
};
@ -3222,6 +3221,19 @@ static Optional<PseudoClass> subject_pseudo_class_bucket_for_is_or_where_selecto
return common_bucket;
}
static Optional<u32> ancestor_hash_bucket_for_selector(Selector const& selector)
{
if (!selector.can_use_ancestor_filter())
return {};
for (auto hash : selector.ancestor_hashes()) {
if (hash == 0)
break;
return hash;
}
return {};
}
static bool matches_hover_pseudo_class_for_rule_bucket(DOM::Element const& element)
{
auto* hovered_node = element.document().hovered_node();
@ -4001,7 +4013,7 @@ void StyleComputer::pop_ancestor(DOM::Element const& element)
}
template<typename RuleBuckets>
static void add_rule_to_rule_buckets(RuleBuckets& rule_buckets, MatchingRule const& matching_rule, Selector::CompoundSelector const& bucket_compound_selector, bool contains_root_pseudo_class, SubjectPseudoClassBuckets subject_pseudo_class_buckets)
static void add_rule_to_rule_buckets(RuleBuckets& rule_buckets, MatchingRule const& matching_rule, Selector::CompoundSelector const& bucket_compound_selector, bool contains_root_pseudo_class, SubjectPseudoClassBuckets subject_pseudo_class_buckets, AncestorHashBuckets ancestor_hash_buckets)
{
// NOTE: We traverse the simple selectors in reverse order to make sure that class/ID buckets are preferred over tag buckets
// in the common case of div.foo or div#foo selectors.
@ -4091,12 +4103,18 @@ static void add_rule_to_rule_buckets(RuleBuckets& rule_buckets, MatchingRule con
add_to_subject_pseudo_class_bucket(subject_pseudo_class_bucket.value());
return;
}
if (ancestor_hash_buckets == AncestorHashBuckets::Yes) {
if (auto ancestor_hash = ancestor_hash_bucket_for_selector(matching_rule.selector); ancestor_hash.has_value()) {
rule_buckets.rules_by_ancestor_hash.ensure(ancestor_hash.value()).append(matching_rule);
return;
}
}
rule_buckets.other_rules.append(matching_rule);
}
}
template<typename RuleBuckets>
static IterationDecision for_each_matching_rule_bucket(DOM::AbstractElement abstract_element, RuleBuckets const& rule_buckets, Function<IterationDecision(Vector<MatchingRule> const&)> const& callback)
static IterationDecision for_each_matching_rule_bucket(DOM::AbstractElement abstract_element, RuleBuckets const& rule_buckets, Function<bool(u32)> const& may_contain_ancestor_hash, Function<IterationDecision(Vector<MatchingRule> const&)> const& callback)
{
for (auto const& class_name : abstract_element.element().class_names()) {
if (auto it = rule_buckets.rules_by_class.find(class_name); it != rule_buckets.rules_by_class.end()) {
@ -4156,10 +4174,17 @@ static IterationDecision for_each_matching_rule_bucket(DOM::AbstractElement abst
return IterationDecision::Break;
}
for (auto const& [hash, rules] : rule_buckets.rules_by_ancestor_hash) {
if (!may_contain_ancestor_hash(hash))
continue;
if (callback(rules) == IterationDecision::Break)
return IterationDecision::Break;
}
return callback(rule_buckets.other_rules);
}
void RuleCache::add_rule(MatchingRule const& matching_rule, Optional<PseudoElement> pseudo_element, bool contains_root_pseudo_class, SubjectPseudoClassBuckets subject_pseudo_class_buckets)
void RuleCache::add_rule(MatchingRule const& matching_rule, Optional<PseudoElement> pseudo_element, bool contains_root_pseudo_class, SubjectPseudoClassBuckets subject_pseudo_class_buckets, AncestorHashBuckets ancestor_hash_buckets)
{
if (matching_rule.slotted) {
slotted_rules.append(matching_rule);
@ -4181,26 +4206,35 @@ void RuleCache::add_rule(MatchingRule const& matching_rule, Optional<PseudoEleme
}
return matching_rule.selector.compound_selectors().last();
}();
add_rule_to_rule_buckets(rules_by_pseudo_element[to_underlying(pseudo_element.value())], matching_rule, bucket_compound_selector, contains_root_pseudo_class, subject_pseudo_class_buckets);
add_rule_to_rule_buckets(rules_by_pseudo_element[to_underlying(pseudo_element.value())], matching_rule, bucket_compound_selector, contains_root_pseudo_class, subject_pseudo_class_buckets, ancestor_hash_buckets);
}
return;
}
add_rule_to_rule_buckets(*this, matching_rule, matching_rule.selector.compound_selectors().last(), contains_root_pseudo_class, subject_pseudo_class_buckets);
add_rule_to_rule_buckets(*this, matching_rule, matching_rule.selector.compound_selectors().last(), contains_root_pseudo_class, subject_pseudo_class_buckets, ancestor_hash_buckets);
}
void RuleCache::for_each_matching_rules(DOM::AbstractElement abstract_element, Function<IterationDecision(Vector<MatchingRule> const&)> callback) const
void RuleCache::for_each_matching_rules(DOM::AbstractElement abstract_element, Function<bool(u32)> const& may_contain_ancestor_hash, Function<IterationDecision(Vector<MatchingRule> const&)> callback) const
{
if (abstract_element.pseudo_element().has_value()) {
if (Selector::PseudoElementSelector::is_known_pseudo_element_type(abstract_element.pseudo_element().value())) {
(void)for_each_matching_rule_bucket(abstract_element, rules_by_pseudo_element.at(to_underlying(abstract_element.pseudo_element().value())), callback);
(void)for_each_matching_rule_bucket(abstract_element, rules_by_pseudo_element.at(to_underlying(abstract_element.pseudo_element().value())), may_contain_ancestor_hash, callback);
} else {
// NOTE: We don't cache rules for unknown pseudo-elements. They can't match anything anyway.
}
return;
}
(void)for_each_matching_rule_bucket(abstract_element, *this, callback);
(void)for_each_matching_rule_bucket(abstract_element, *this, may_contain_ancestor_hash, callback);
}
void RuleCache::for_each_matching_pseudo_element_rules(DOM::AbstractElement abstract_element, Function<bool(u32)> const& may_contain_ancestor_hash, Function<IterationDecision(Vector<MatchingRule> const&)> callback) const
{
VERIFY(!abstract_element.pseudo_element().has_value());
for (auto const& pseudo_element_rules : rules_by_pseudo_element) {
if (for_each_matching_rule_bucket(abstract_element, pseudo_element_rules, may_contain_ancestor_hash, callback) == IterationDecision::Break)
return;
}
}
void StyleComputer::ScopedMatchingRule::visit_edges(GC::Cell::Visitor& visitor)

View file

@ -105,12 +105,14 @@ void RuleCache::visit_edges(GC::Cell::Visitor& visitor)
visit_map(rules_by_tag_name);
visit_map(rules_by_attribute_name);
visit_pseudo_class_buckets(rules_by_subject_pseudo_class);
visit_map(rules_by_ancestor_hash);
for (auto& rules : rules_by_pseudo_element) {
visit_map(rules.rules_by_id);
visit_map(rules.rules_by_class);
visit_map(rules.rules_by_tag_name);
visit_map(rules.rules_by_attribute_name);
visit_pseudo_class_buckets(rules.rules_by_subject_pseudo_class);
visit_map(rules.rules_by_ancestor_hash);
visit_vector(rules.root_rules);
visit_vector(rules.other_rules);
}
@ -551,11 +553,11 @@ void StyleScope::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin
continue;
if (selector.contains_pseudo_class(pseudo_class)) {
// For pseudo class rule caches we intentionally pass no pseudo-element, because we don't want to bucket pseudo class rules by pseudo-element type.
rule_cache.pseudo_class_rule_cache[i]->add_rule(matching_rule, {}, contains_root_pseudo_class, SubjectPseudoClassBuckets::No);
rule_cache.pseudo_class_rule_cache[i]->add_rule(matching_rule, {}, contains_root_pseudo_class, SubjectPseudoClassBuckets::No, AncestorHashBuckets::No);
}
}
matching_rule_cache.add_rule(matching_rule, selector.target_pseudo_element(), contains_root_pseudo_class, SubjectPseudoClassBuckets::Yes);
matching_rule_cache.add_rule(matching_rule, selector.target_pseudo_element(), contains_root_pseudo_class, SubjectPseudoClassBuckets::Yes, AncestorHashBuckets::Yes);
}
++rule_index;
});

View file

@ -59,12 +59,18 @@ enum class SubjectPseudoClassBuckets {
Yes,
};
enum class AncestorHashBuckets {
No,
Yes,
};
struct RuleCache {
HashMap<FlyString, Vector<MatchingRule>> rules_by_id;
HashMap<FlyString, Vector<MatchingRule>> rules_by_class;
HashMap<FlyString, Vector<MatchingRule>> rules_by_tag_name;
HashMap<FlyString, Vector<MatchingRule>, AK::ASCIICaseInsensitiveFlyStringTraits> rules_by_attribute_name;
Array<Vector<MatchingRule>, to_underlying(PseudoClass::__Count)> rules_by_subject_pseudo_class;
HashMap<u32, Vector<MatchingRule>> rules_by_ancestor_hash;
Vector<MatchingRule> root_rules;
Vector<MatchingRule> slotted_rules;
Vector<MatchingRule> part_rules;
@ -76,6 +82,7 @@ struct RuleCache {
HashMap<FlyString, Vector<MatchingRule>> rules_by_tag_name;
HashMap<FlyString, Vector<MatchingRule>, AK::ASCIICaseInsensitiveFlyStringTraits> rules_by_attribute_name;
Array<Vector<MatchingRule>, to_underlying(PseudoClass::__Count)> rules_by_subject_pseudo_class;
HashMap<u32, Vector<MatchingRule>> rules_by_ancestor_hash;
Vector<MatchingRule> root_rules;
Vector<MatchingRule> other_rules;
};
@ -83,8 +90,9 @@ struct RuleCache {
HashMap<FlyString, NonnullRefPtr<Animations::KeyframeEffect::KeyFrameSet>> rules_by_animation_keyframes;
void add_rule(MatchingRule const&, Optional<PseudoElement>, bool contains_root_pseudo_class, SubjectPseudoClassBuckets);
void for_each_matching_rules(DOM::AbstractElement, Function<IterationDecision(Vector<MatchingRule> const&)> callback) const;
void add_rule(MatchingRule const&, Optional<PseudoElement>, bool contains_root_pseudo_class, SubjectPseudoClassBuckets, AncestorHashBuckets);
void for_each_matching_rules(DOM::AbstractElement, Function<bool(u32)> const& may_contain_ancestor_hash, Function<IterationDecision(Vector<MatchingRule> const&)> callback) const;
void for_each_matching_pseudo_element_rules(DOM::AbstractElement, Function<bool(u32)> const& may_contain_ancestor_hash, Function<IterationDecision(Vector<MatchingRule> const&)> callback) const;
void visit_edges(GC::Cell::Visitor&);
};