diff --git a/Userland/Libraries/LibWeb/CSS/Selector.cpp b/Userland/Libraries/LibWeb/CSS/Selector.cpp index 1536e22bde..e55e98500d 100644 --- a/Userland/Libraries/LibWeb/CSS/Selector.cpp +++ b/Userland/Libraries/LibWeb/CSS/Selector.cpp @@ -23,6 +23,53 @@ Selector::Selector(Vector&& compound_selectors) } } } + + collect_ancestor_hashes(); +} + +void Selector::collect_ancestor_hashes() +{ + size_t next_hash_index = 0; + auto append_unique_hash = [&](u32 hash) -> bool { + if (next_hash_index >= m_ancestor_hashes.size()) + return true; + for (size_t i = 0; i < next_hash_index; ++i) { + if (m_ancestor_hashes[i] == hash) + return false; + } + m_ancestor_hashes[next_hash_index++] = hash; + return false; + }; + + auto last_combinator = m_compound_selectors.last().combinator; + for (ssize_t compound_selector_index = static_cast(m_compound_selectors.size()) - 2; compound_selector_index >= 0; --compound_selector_index) { + auto const& compound_selector = m_compound_selectors[compound_selector_index]; + if (last_combinator == Combinator::Descendant) { + for (auto const& simple_selector : compound_selector.simple_selectors) { + switch (simple_selector.type) { + case SimpleSelector::Type::Id: + case SimpleSelector::Type::Class: + if (append_unique_hash(simple_selector.name().hash())) + return; + break; + case SimpleSelector::Type::TagName: + if (append_unique_hash(simple_selector.qualified_name().name.name.hash())) + return; + break; + case SimpleSelector::Type::Attribute: + if (append_unique_hash(simple_selector.attribute().qualified_name.name.name.hash())) + return; + break; + default: + break; + } + } + } + last_combinator = compound_selector.combinator; + } + + for (size_t i = next_hash_index; i < m_ancestor_hashes.size(); ++i) + m_ancestor_hashes[i] = 0; } // https://www.w3.org/TR/selectors-4/#specificity-rules diff --git a/Userland/Libraries/LibWeb/CSS/Selector.h b/Userland/Libraries/LibWeb/CSS/Selector.h index 1cf0115623..c4389fdefb 100644 --- a/Userland/Libraries/LibWeb/CSS/Selector.h +++ b/Userland/Libraries/LibWeb/CSS/Selector.h @@ -242,12 +242,18 @@ public: u32 specificity() const; String serialize() const; + auto const& ancestor_hashes() const { return m_ancestor_hashes; } + private: explicit Selector(Vector&&); Vector m_compound_selectors; mutable Optional m_specificity; Optional m_pseudo_element; + + void collect_ancestor_hashes(); + + Array m_ancestor_hashes; }; String serialize_a_group_of_selectors(Vector> const& selectors); diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp index 5f71541bae..f61af71eb0 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -61,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -302,6 +303,17 @@ StyleComputer::RuleCache const& StyleComputer::rule_cache_for_cascade_origin(Cas return true; } +bool StyleComputer::should_reject_with_ancestor_filter(Selector const& selector) const +{ + for (u32 hash : selector.ancestor_hashes()) { + if (hash == 0) + break; + if (!m_ancestor_filter.may_contain(hash)) + return true; + } + return false; +} + Vector StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional pseudo_element) const { auto const& root_node = element.root(); @@ -358,6 +370,10 @@ Vector StyleComputer::collect_matching_rules(DOM::Element const& e continue; auto const& selector = rule_to_run.rule->selectors()[rule_to_run.selector_index]; + + if (should_reject_with_ancestor_filter(*selector)) + continue; + if (rule_to_run.can_use_fast_matches) { if (!SelectorEngine::fast_matches(selector, *rule_to_run.sheet, element)) continue; @@ -2585,4 +2601,35 @@ void StyleComputer::compute_math_depth(StyleProperties& style, DOM::Element cons style.set_math_depth(inherited_math_depth()); } +static void for_each_element_hash(DOM::Element const& element, auto callback) +{ + callback(element.local_name().hash()); + if (element.id().has_value()) + callback(element.id().value().hash()); + for (auto const& class_ : element.class_names()) + callback(class_.hash()); + element.for_each_attribute([&](auto& attribute) { + callback(attribute.local_name().hash()); + }); +} + +void StyleComputer::reset_ancestor_filter() +{ + m_ancestor_filter.clear(); +} + +void StyleComputer::push_ancestor(DOM::Element const& element) +{ + for_each_element_hash(element, [&](u32 hash) { + m_ancestor_filter.increment(hash); + }); +} + +void StyleComputer::pop_ancestor(DOM::Element const& element) +{ + for_each_element_hash(element, [&](u32 hash) { + m_ancestor_filter.decrement(hash); + }); +} + } diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.h b/Userland/Libraries/LibWeb/CSS/StyleComputer.h index 4635624638..35cf8fd1eb 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2018-2024, Andreas Kling * Copyright (c) 2021-2023, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause @@ -20,6 +20,55 @@ namespace Web::CSS { +// A counting bloom filter with 2 hash functions. +// NOTE: If a counter overflows, it's kept maxed-out until the whole filter is cleared. +template +class CountingBloomFilter { +public: + CountingBloomFilter() { } + + void clear() { __builtin_memset(m_buckets, 0, sizeof(m_buckets)); } + + void increment(u32 key) + { + auto& first = bucket1(key); + if (first < NumericLimits::max()) + ++first; + auto& second = bucket2(key); + if (second < NumericLimits::max()) + ++second; + } + + void decrement(u32 key) + { + auto& first = bucket1(key); + if (first < NumericLimits::max()) + --first; + auto& second = bucket2(key); + if (second < NumericLimits::max()) + --second; + } + + [[nodiscard]] bool may_contain(u32 hash) const + { + return bucket1(hash) && bucket2(hash); + } + +private: + static constexpr u32 bucket_count = 1 << key_bits; + static constexpr u32 key_mask = bucket_count - 1; + + [[nodiscard]] u32 hash1(u32 key) const { return key & key_mask; } + [[nodiscard]] u32 hash2(u32 key) const { return (key >> 16) & key_mask; } + + [[nodiscard]] CounterType& bucket1(u32 key) { return m_buckets[hash1(key)]; } + [[nodiscard]] CounterType& bucket2(u32 key) { return m_buckets[hash2(key)]; } + [[nodiscard]] CounterType bucket1(u32 key) const { return m_buckets[hash1(key)]; } + [[nodiscard]] CounterType bucket2(u32 key) const { return m_buckets[hash2(key)]; } + + CounterType m_buckets[bucket_count]; +}; + // https://www.w3.org/TR/css-cascade/#origin enum class CascadeOrigin : u8 { Author, @@ -69,6 +118,10 @@ public: DOM::Document& document() { return m_document; } DOM::Document const& document() const { return m_document; } + void reset_ancestor_filter(); + void push_ancestor(DOM::Element const&); + void pop_ancestor(DOM::Element const&); + NonnullRefPtr create_document_style() const; NonnullRefPtr compute_style(DOM::Element&, Optional = {}) const; @@ -103,6 +156,8 @@ private: class FontLoader; struct MatchingFontCandidate; + [[nodiscard]] bool should_reject_with_ancestor_filter(Selector const&) const; + RefPtr compute_style_impl(DOM::Element&, Optional, ComputeStyleMode) const; void compute_cascaded_values(StyleProperties&, DOM::Element&, Optional, bool& did_match_any_pseudo_element_rules, ComputeStyleMode) const; static RefPtr find_matching_font_weight_ascending(Vector const& candidates, int target_weight, float font_size_in_pt, bool inclusive); @@ -167,6 +222,8 @@ private: Length::FontMetrics m_root_element_font_metrics; CSSPixelRect m_viewport_rect; + + CountingBloomFilter m_ancestor_filter; }; } diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index b3d77c0db0..40059e9ca0 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -1105,11 +1105,14 @@ void Document::update_layout() m_needs_layout = false; } -[[nodiscard]] static CSS::RequiredInvalidationAfterStyleChange update_style_recursively(Node& node) +[[nodiscard]] static CSS::RequiredInvalidationAfterStyleChange update_style_recursively(Node& node, CSS::StyleComputer& style_computer) { bool const needs_full_style_update = node.document().needs_full_style_update(); CSS::RequiredInvalidationAfterStyleChange invalidation; + if (node.is_element()) + style_computer.push_ancestor(static_cast(node)); + // NOTE: If the current node has `display:none`, we can disregard all invalidation // caused by its children, as they will not be rendered anyway. // We will still recompute style for the children, though. @@ -1125,7 +1128,7 @@ void Document::update_layout() if (node.is_element()) { if (auto* shadow_root = static_cast(node).shadow_root_internal()) { if (needs_full_style_update || shadow_root->needs_style_update() || shadow_root->child_needs_style_update()) { - auto subtree_invalidation = update_style_recursively(*shadow_root); + auto subtree_invalidation = update_style_recursively(*shadow_root, style_computer); if (!is_display_none) invalidation |= subtree_invalidation; } @@ -1134,7 +1137,7 @@ void Document::update_layout() node.for_each_child([&](auto& child) { if (needs_full_style_update || child.needs_style_update() || child.child_needs_style_update()) { - auto subtree_invalidation = update_style_recursively(child); + auto subtree_invalidation = update_style_recursively(child, style_computer); if (!is_display_none) invalidation |= subtree_invalidation; } @@ -1143,6 +1146,10 @@ void Document::update_layout() } node.set_child_needs_style_update(false); + + if (node.is_element()) + style_computer.pop_ancestor(static_cast(node)); + return invalidation; } @@ -1165,7 +1172,9 @@ void Document::update_style() evaluate_media_rules(); - auto invalidation = update_style_recursively(*this); + style_computer().reset_ancestor_filter(); + + auto invalidation = update_style_recursively(*this, style_computer()); if (invalidation.rebuild_layout_tree) { invalidate_layout(); } else { diff --git a/Userland/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Userland/Libraries/LibWeb/Layout/TreeBuilder.cpp index 4bf10329fd..03adbd0837 100644 --- a/Userland/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Userland/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -290,6 +290,14 @@ i32 TreeBuilder::calculate_list_item_index(DOM::Node& dom_node) void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& context) { + if (dom_node.is_element()) + dom_node.document().style_computer().push_ancestor(static_cast(dom_node)); + + ScopeGuard pop_ancestor_guard = [&] { + if (dom_node.is_element()) + dom_node.document().style_computer().pop_ancestor(static_cast(dom_node)); + }; + JS::GCPtr layout_node; Optional> has_svg_root_change; @@ -469,6 +477,8 @@ JS::GCPtr TreeBuilder::build(DOM::Node& dom_node) { VERIFY(dom_node.is_document()); + dom_node.document().style_computer().reset_ancestor_filter(); + Context context; m_quote_nesting_level = 0; create_layout_tree(dom_node, context);