diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index 45148716af..11cf73071e 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -39,9 +40,206 @@ namespace Web::SelectorEngine { +static u32 salted_tag_name_hash(FlyString const& tag_name) +{ + return CSS::ancestor_filter_hash_for_tag_name(tag_name.ascii_case_insensitive_hash()); +} + +static u32 salted_id_hash(FlyString const& id) +{ + return CSS::ancestor_filter_hash_for_id(id.hash()); +} + +static u32 salted_class_hash(FlyString const& class_name) +{ + return CSS::ancestor_filter_hash_for_class(class_name.hash()); +} + +static u32 salted_attribute_hash(FlyString const& attribute_name) +{ + return CSS::ancestor_filter_hash_for_attribute(attribute_name.ascii_case_insensitive_hash()); +} + +void HasFastRejectFilter::add(u32 hash) +{ + auto const first_bit = hash & 4095; + auto const second_bit = (hash >> 16) & 4095; + buckets[first_bit / 64] |= 1ull << (first_bit % 64); + buckets[second_bit / 64] |= 1ull << (second_bit % 64); +} + +bool HasFastRejectFilter::may_contain(u32 hash) const +{ + auto const first_bit = hash & 4095; + auto const second_bit = (hash >> 16) & 4095; + return (buckets[first_bit / 64] & (1ull << (first_bit % 64))) + && (buckets[second_bit / 64] & (1ull << (second_bit % 64))); +} + static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& simple_selector, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context); static bool fast_matches_compound_selector(CSS::Selector::CompoundSelector const& compound_selector, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context); +static bool is_excluded_attribute_for_has_filter(FlyString const& name) +{ + return name == HTML::AttributeNames::class_ + || name == HTML::AttributeNames::id + || name == HTML::AttributeNames::style; +} + +static void add_element_identifier_hashes(HasFastRejectFilter& filter, DOM::Element const& element, MatchContext const& context) +{ + filter.add(salted_tag_name_hash(element.local_name())); + if (element.id().has_value()) + filter.add(salted_id_hash(element.id().value())); + for (auto const& class_name : element.class_names()) + filter.add(salted_class_hash(class_name)); + element.for_each_attribute([&](auto const& attribute) { + auto const& name = attribute.name(); + if (is_excluded_attribute_for_has_filter(name)) + return; + filter.add(salted_attribute_hash(name)); + }); + + if (context.inside_has_argument_match && context.collect_per_element_selector_involvement_metadata) + const_cast(element).set_in_has_scope(true); +} + +static void populate_has_fast_reject_filter(HasFastRejectFilter& filter, DOM::Element const& anchor, HasFastRejectFilterTraversalType traversal_type, MatchContext const& context) +{ + // This intentionally mirrors the traversal scope that a matching :has() + // argument would inspect. The filter is only populated on the second + // :has() check for the same anchor/scope, so one subtree walk can reject + // several later arguments without penalizing the single-check case. + switch (traversal_type) { + case HasFastRejectFilterTraversalType::Children: + anchor.for_each_child([&](DOM::Node const& child) { + if (child.is_element()) + add_element_identifier_hashes(filter, static_cast(child), context); + return IterationDecision::Continue; + }); + break; + case HasFastRejectFilterTraversalType::Descendants: + anchor.for_each_in_subtree([&](DOM::Node const& descendant) { + if (descendant.is_element()) + add_element_identifier_hashes(filter, static_cast(descendant), context); + return TraversalDecision::Continue; + }); + break; + } + filter.populated = true; +} + +static void collect_has_fast_reject_hashes(CSS::Selector::SimpleSelector const& simple_selector, Vector& hashes, bool in_quirks_mode) +{ + switch (simple_selector.type) { + case CSS::Selector::SimpleSelector::Type::TagName: + hashes.append(salted_tag_name_hash(simple_selector.qualified_name().name.lowercase_name)); + break; + case CSS::Selector::SimpleSelector::Type::Id: + hashes.append(salted_id_hash(simple_selector.name())); + break; + case CSS::Selector::SimpleSelector::Type::Class: + if (in_quirks_mode) + break; + hashes.append(salted_class_hash(simple_selector.name())); + break; + case CSS::Selector::SimpleSelector::Type::Attribute: { + auto const& name = simple_selector.attribute().qualified_name.name.lowercase_name; + if (!is_excluded_attribute_for_has_filter(name)) + hashes.append(salted_attribute_hash(name)); + break; + } + case CSS::Selector::SimpleSelector::Type::Universal: + case CSS::Selector::SimpleSelector::Type::PseudoClass: + case CSS::Selector::SimpleSelector::Type::PseudoElement: + case CSS::Selector::SimpleSelector::Type::Nesting: + case CSS::Selector::SimpleSelector::Type::Invalid: + break; + } +} + +static Vector collect_has_fast_reject_hashes(CSS::Selector const& selector, bool in_quirks_mode) +{ + Vector hashes; + for (auto const& compound_selector : selector.compound_selectors()) { + for (auto const& simple_selector : compound_selector.simple_selectors) + collect_has_fast_reject_hashes(simple_selector, hashes, in_quirks_mode); + } + return hashes; +} + +static Optional has_fast_reject_filter_traversal_type(CSS::Selector const& selector) +{ + if (selector.compound_selectors().is_empty()) + return {}; + + switch (selector.compound_selectors().first().combinator) { + case CSS::Selector::Combinator::ImmediateChild: + if (selector.compound_selectors().size() == 1) + return HasFastRejectFilterTraversalType::Children; + // The argument can still contain later descendant combinators, e.g. + // `:has(> .wrapper .hit)`. Since we collect hashes from the whole + // relative selector, use the descendant scope so nested requirements + // do not cause false rejections. + return HasFastRejectFilterTraversalType::Descendants; + case CSS::Selector::Combinator::Descendant: + return HasFastRejectFilterTraversalType::Descendants; + case CSS::Selector::Combinator::None: + case CSS::Selector::Combinator::NextSibling: + case CSS::Selector::Combinator::SubsequentSibling: + case CSS::Selector::Combinator::Column: + case CSS::Selector::Combinator::PseudoElement: + return {}; + } + VERIFY_NOT_REACHED(); +} + +static bool selector_contains_sibling_combinator(CSS::Selector const& selector) +{ + for (auto const& compound_selector : selector.compound_selectors()) { + if (compound_selector.combinator == CSS::Selector::Combinator::NextSibling + || compound_selector.combinator == CSS::Selector::Combinator::SubsequentSibling) { + return true; + } + } + return false; +} + +static bool should_reject_with_has_fast_reject_filter(CSS::Selector const& selector, DOM::Element const& anchor, MatchContext& context) +{ + if (!context.has_fast_reject_filter_cache) + return false; + + if (context.collect_per_element_selector_involvement_metadata && selector_contains_sibling_combinator(selector)) + return false; + + auto traversal_type = has_fast_reject_filter_traversal_type(selector); + if (!traversal_type.has_value()) + return false; + + auto hashes = collect_has_fast_reject_hashes(selector, anchor.document().in_quirks_mode()); + if (hashes.is_empty()) + return false; + + HasFastRejectFilterKey key { + .element = &anchor, + .traversal_type = *traversal_type, + }; + auto& filter = context.has_fast_reject_filter_cache->ensure(key); + if (!filter.seen_once) { + filter.seen_once = true; + return false; + } + if (!filter.populated) + populate_has_fast_reject_filter(filter, anchor, *traversal_type, context); + + for (auto hash : hashes) { + if (!filter.may_contain(hash)) + return true; + } + return false; +} + static CSS::Selector::SimpleSelector const* simple_has_child_tag_selector(CSS::Selector const& selector) { if (selector.compound_selectors().size() != 1) @@ -349,7 +547,9 @@ static inline bool matches_has_pseudo_class(CSS::Selector const& selector, DOM:: ScopeGuard restore_inside_has = [&] { context.inside_has_argument_match = saved_inside_has; }; bool result; - if (context.collect_per_element_selector_involvement_metadata) { + if (should_reject_with_has_fast_reject_filter(selector, anchor, context)) { + result = false; + } else if (context.collect_per_element_selector_involvement_metadata) { result = matches_relative_selector(selector, 0, anchor, shadow_host, context, anchor, scope); } else if (auto const* simple_selector = simple_has_child_tag_selector(selector)) { result = matches_has_child_tag_fast_path(*simple_selector, anchor, shadow_host, context); diff --git a/Libraries/LibWeb/CSS/SelectorEngine.h b/Libraries/LibWeb/CSS/SelectorEngine.h index 6dc715f7bd..7cfd9a91dd 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.h +++ b/Libraries/LibWeb/CSS/SelectorEngine.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -43,6 +44,43 @@ struct HasResultCacheKeyTraits : Traits { using HasResultCache = HashMap; +enum class HasFastRejectFilterTraversalType : u8 { + Children, + Descendants, +}; + +struct HasFastRejectFilterKey { + GC::Ptr element; + HasFastRejectFilterTraversalType traversal_type; + + void visit_edges(GC::Cell::Visitor& visitor) + { + visitor.visit(element); + } + + bool operator==(HasFastRejectFilterKey const&) const = default; +}; + +struct HasFastRejectFilterKeyTraits : Traits { + static unsigned hash(HasFastRejectFilterKey const& key) + { + return pair_int_hash(ptr_hash(key.element.ptr()), to_underlying(key.traversal_type)); + } +}; + +struct HasFastRejectFilter { + static constexpr size_t bucket_count = 64; + + bool seen_once { false }; + bool populated { false }; + Array buckets {}; + + void add(u32 hash); + [[nodiscard]] bool may_contain(u32 hash) const; +}; + +using HasFastRejectFilterCache = HashMap; + struct MatchContext { GC::Ptr style_sheet_for_rule {}; GC::Ptr subject {}; @@ -55,6 +93,7 @@ struct MatchContext { // by matches_has_pseudo_class with a ScopeGuard. bool inside_has_argument_match { false }; HasResultCache* has_result_cache { nullptr }; + HasFastRejectFilterCache* has_fast_reject_filter_cache { nullptr }; }; bool matches(CSS::Selector const&, DOM::AbstractElement const&, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope = {}, SelectorKind selector_kind = SelectorKind::Normal, GC::Ptr anchor = nullptr); diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index cfe4d2b199..1ff96c8366 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -164,6 +164,8 @@ void StyleComputer::visit_edges(Visitor& visitor) visitor.visit(m_document); if (m_has_result_cache) visitor.visit(*m_has_result_cache); + if (m_has_fast_reject_filter_cache) + visitor.visit(*m_has_fast_reject_filter_cache); if (m_cached_font_computation_context.has_value()) m_cached_font_computation_context->visit_edges(visitor); @@ -705,8 +707,9 @@ Vector StyleComputer::collect_matching_rules_ // (for :host::part() within the shadow DOM's own stylesheet). if (shadow_root && (abstract_element.pseudo_element().has_value() || !abstract_element.element().part_names().is_empty())) { if (context_shadow_root == shadow_root) { - if (auto const* rule_cache = rule_cache_for_cascade_origin(cascade_origin, qualified_layer_name, shadow_root)) + if (auto const* rule_cache = rule_cache_for_cascade_origin(cascade_origin, qualified_layer_name, shadow_root)) { add_rules_to_run(rule_cache->part_rules, shadow_root); + } } for (auto* part_shadow_root = abstract_element.element().first_flat_tree_ancestor_of_type(); part_shadow_root; @@ -714,12 +717,14 @@ Vector StyleComputer::collect_matching_rules_ if (context_shadow_root != part_shadow_root) continue; - if (auto const* rule_cache = rule_cache_for_cascade_origin(cascade_origin, qualified_layer_name, part_shadow_root)) + if (auto const* rule_cache = rule_cache_for_cascade_origin(cascade_origin, qualified_layer_name, part_shadow_root)) { add_rules_to_run(rule_cache->part_rules, part_shadow_root); + } } if (!context_shadow_root) { - if (auto const* rule_cache = rule_cache_for_cascade_origin(cascade_origin, qualified_layer_name, nullptr)) + if (auto const* rule_cache = rule_cache_for_cascade_origin(cascade_origin, qualified_layer_name, nullptr)) { add_rules_to_run(rule_cache->part_rules, nullptr); + } } } @@ -750,6 +755,7 @@ Vector StyleComputer::collect_matching_rules_ .rule_shadow_root = rule_root, .collect_per_element_selector_involvement_metadata = true, .has_result_cache = m_has_result_cache.ptr(), + .has_fast_reject_filter_cache = m_has_fast_reject_filter_cache.ptr(), }; if (!abstract_element.pseudo_element().has_value() && matching_pseudo_element_styles) { if (auto pseudo_element = selector.target_pseudo_element(); pseudo_element.has_value()) { @@ -4189,6 +4195,11 @@ void StyleComputer::reset_has_result_cache() m_has_result_cache = make(); else m_has_result_cache->clear(); + + if (!m_has_fast_reject_filter_cache) + m_has_fast_reject_filter_cache = make(); + else + m_has_fast_reject_filter_cache->clear(); } void StyleComputer::push_ancestor(DOM::Element const& element) @@ -4382,8 +4393,9 @@ static IterationDecision for_each_matching_rule_bucket(DOM::AbstractElement abst IterationDecision decision = IterationDecision::Continue; abstract_element.element().for_each_attribute([&](auto& name, auto&) { - if (auto it = rule_buckets.rules_by_attribute_name.find(name); it != rule_buckets.rules_by_attribute_name.end()) + if (auto it = rule_buckets.rules_by_attribute_name.find(name); it != rule_buckets.rules_by_attribute_name.end()) { decision = callback(it->value); + } }); if (decision == IterationDecision::Break) return IterationDecision::Break; diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index d72687f555..75f24ad48e 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -248,6 +248,7 @@ private: OwnPtr> m_ancestor_filter; OwnPtr m_has_result_cache; + OwnPtr m_has_fast_reject_filter_cache; }; inline bool StyleComputer::should_reject_with_ancestor_filter(Selector const& selector) const diff --git a/Tests/LibWeb/Text/expected/css/has-fast-reject-filter-quirks-class-selector.txt b/Tests/LibWeb/Text/expected/css/has-fast-reject-filter-quirks-class-selector.txt new file mode 100644 index 0000000000..457107d589 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/has-fast-reject-filter-quirks-class-selector.txt @@ -0,0 +1,2 @@ +Quirks mode: BackCompat +:has() matches case-insensitive class selector after cached miss: true diff --git a/Tests/LibWeb/Text/expected/css/has-fast-reject-filter-sibling-combinator-invalidation.txt b/Tests/LibWeb/Text/expected/css/has-fast-reject-filter-sibling-combinator-invalidation.txt new file mode 100644 index 0000000000..9954456028 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/has-fast-reject-filter-sibling-combinator-invalidation.txt @@ -0,0 +1,2 @@ +initial match: false +match after adding sibling: true diff --git a/Tests/LibWeb/Text/input/css/has-fast-reject-filter-quirks-class-selector.html b/Tests/LibWeb/Text/input/css/has-fast-reject-filter-quirks-class-selector.html new file mode 100644 index 0000000000..5fe4f2eb3f --- /dev/null +++ b/Tests/LibWeb/Text/input/css/has-fast-reject-filter-quirks-class-selector.html @@ -0,0 +1,13 @@ + + + +
+ diff --git a/Tests/LibWeb/Text/input/css/has-fast-reject-filter-sibling-combinator-invalidation.html b/Tests/LibWeb/Text/input/css/has-fast-reject-filter-sibling-combinator-invalidation.html new file mode 100644 index 0000000000..23092db4a5 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/has-fast-reject-filter-sibling-combinator-invalidation.html @@ -0,0 +1,19 @@ + + + +
+