From 482e5deb8531ad9f2a34f01fabc9ced88b6ecbeb Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Sat, 4 Jan 2025 18:09:21 +0300 Subject: [PATCH] LibWeb: Further optimize :hover style invalidation Previously, we optimized hover style invalidation to mark for style updates only those elements that were matched by :hover selectors in the last style calculation. This change takes it a step further by invalidating only the elements where the set of selectors that use :hover changes after hovered element is modified. The implementation is as follows: 1. Collect all elements whose styles might be affected by a change in the hovered element. 2. Retrieve a list of all selectors that use :hover. 3. Test each selector against each element and record which selectors match. 4. Update m_hovered_node to the newly hovered element. 5. Repeat step 3. 6. For each element, compare the previous and current sets of matched selectors. If they differ, mark the element for style recalculation. --- Libraries/LibWeb/CSS/Selector.cpp | 7 +- Libraries/LibWeb/CSS/Selector.h | 2 + Libraries/LibWeb/CSS/StyleComputer.cpp | 19 ++++-- Libraries/LibWeb/CSS/StyleComputer.h | 4 +- Libraries/LibWeb/DOM/Document.cpp | 93 ++++++++++++++++++++------ Libraries/LibWeb/DOM/Document.h | 1 + 6 files changed, 101 insertions(+), 25 deletions(-) diff --git a/Libraries/LibWeb/CSS/Selector.cpp b/Libraries/LibWeb/CSS/Selector.cpp index 35049c5609f..e413b7f1c01 100644 --- a/Libraries/LibWeb/CSS/Selector.cpp +++ b/Libraries/LibWeb/CSS/Selector.cpp @@ -57,10 +57,15 @@ Selector::Selector(Vector&& compound_selectors) break; } if (simple_selector.type == SimpleSelector::Type::PseudoClass) { + if (simple_selector.pseudo_class().type == PseudoClass::Hover) { + m_contains_hover_pseudo_class = true; + } for (auto const& child_selector : simple_selector.pseudo_class().argument_selector_list) { if (child_selector->contains_the_nesting_selector()) { m_contains_the_nesting_selector = true; - break; + } + if (child_selector->contains_hover_pseudo_class()) { + m_contains_hover_pseudo_class = true; } } if (m_contains_the_nesting_selector) diff --git a/Libraries/LibWeb/CSS/Selector.h b/Libraries/LibWeb/CSS/Selector.h index 940015c03a4..4b61ea69d10 100644 --- a/Libraries/LibWeb/CSS/Selector.h +++ b/Libraries/LibWeb/CSS/Selector.h @@ -261,6 +261,7 @@ public: Optional const& pseudo_element() const { return m_pseudo_element; } NonnullRefPtr relative_to(SimpleSelector const&) const; bool contains_the_nesting_selector() const { return m_contains_the_nesting_selector; } + bool contains_hover_pseudo_class() const { return m_contains_hover_pseudo_class; } RefPtr absolutized(SimpleSelector const& selector_for_nesting) const; u32 specificity() const; String serialize() const; @@ -274,6 +275,7 @@ private: mutable Optional m_specificity; Optional m_pseudo_element; bool m_contains_the_nesting_selector { false }; + bool m_contains_hover_pseudo_class { false }; void collect_ancestor_hashes(); diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 1e5ccf96129..e2353ea0076 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -421,6 +421,11 @@ bool StyleComputer::should_reject_with_ancestor_filter(Selector const& selector) } return false; } +Vector const& StyleComputer::get_hover_rules() const +{ + build_rule_cache_if_needed(); + return m_hover_rules; +} Vector StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional pseudo_element, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name) const { @@ -2540,7 +2545,7 @@ void StyleComputer::collect_selector_insights(Selector const& selector, Selector } } -NonnullOwnPtr StyleComputer::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin, SelectorInsights& insights) +NonnullOwnPtr StyleComputer::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin, SelectorInsights& insights, Vector& hover_rules) { auto rule_cache = make(); @@ -2623,6 +2628,10 @@ NonnullOwnPtr StyleComputer::make_rule_cache_for_casca } } + if (selector.contains_hover_pseudo_class()) { + hover_rules.append(matching_rule); + } + // 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. bool added_to_bucket = false; @@ -2834,9 +2843,9 @@ void StyleComputer::build_rule_cache() build_qualified_layer_names_cache(); - m_author_rule_cache = make_rule_cache_for_cascade_origin(CascadeOrigin::Author, *m_selector_insights); - m_user_rule_cache = make_rule_cache_for_cascade_origin(CascadeOrigin::User, *m_selector_insights); - m_user_agent_rule_cache = make_rule_cache_for_cascade_origin(CascadeOrigin::UserAgent, *m_selector_insights); + m_author_rule_cache = make_rule_cache_for_cascade_origin(CascadeOrigin::Author, *m_selector_insights, m_hover_rules); + m_user_rule_cache = make_rule_cache_for_cascade_origin(CascadeOrigin::User, *m_selector_insights, m_hover_rules); + m_user_agent_rule_cache = make_rule_cache_for_cascade_origin(CascadeOrigin::UserAgent, *m_selector_insights, m_hover_rules); } void StyleComputer::invalidate_rule_cache() @@ -2852,6 +2861,8 @@ void StyleComputer::invalidate_rule_cache() // NOTE: It might not be necessary to throw away the UA rule cache. // If we are sure that it's safe, we could keep it as an optimization. m_user_agent_rule_cache = nullptr; + + m_hover_rules.clear_with_capacity(); } void StyleComputer::did_load_font(FlyString const&) diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index 0d213c12064..fdeafc54dce 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -146,6 +146,7 @@ public: [[nodiscard]] GC::Ref compute_style(DOM::Element&, Optional = {}) const; [[nodiscard]] GC::Ptr compute_pseudo_element_style_if_needed(DOM::Element&, Optional) const; + Vector const& get_hover_rules() const; Vector collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name = {}) const; void invalidate_rule_cache(); @@ -267,13 +268,14 @@ private: HashMap> rules_by_animation_keyframes; }; - NonnullOwnPtr make_rule_cache_for_cascade_origin(CascadeOrigin, SelectorInsights&); + NonnullOwnPtr make_rule_cache_for_cascade_origin(CascadeOrigin, SelectorInsights&, Vector& hover_rules); RuleCache const& rule_cache_for_cascade_origin(CascadeOrigin) const; static void collect_selector_insights(Selector const&, SelectorInsights&); OwnPtr m_selector_insights; + Vector m_hover_rules; OwnPtr m_author_rule_cache; OwnPtr m_user_rule_cache; OwnPtr m_user_agent_rule_cache; diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 3024797bb59..355fbc2750d 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -8,6 +8,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -36,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -1485,31 +1487,84 @@ static Node* find_common_ancestor(Node* a, Node* b) return nullptr; } +void Document::invalidate_style_for_elements_affected_by_hover_change(GC::Ptr old_new_hovered_common_ancestor, GC::Ptr hovered_node) +{ + auto const& hover_rules = style_computer().get_hover_rules(); + if (hover_rules.is_empty()) + return; + + auto& invalidation_root = [&] -> Node& { + if (style_computer().has_has_selectors()) + return *this; + return old_new_hovered_common_ancestor ? *old_new_hovered_common_ancestor : *this; + }(); + + Vector elements; + invalidation_root.for_each_shadow_including_inclusive_descendant([&](Node& node) { + if (!node.is_element()) + return TraversalDecision::Continue; + auto& element = static_cast(node); + if (element.affected_by_hover()) + elements.append(element); + return TraversalDecision::Continue; + }); + + auto compute_hover_selectors_match_state = [&] { + Vector state; + state.resize(elements.size()); + for (size_t element_index = 0; element_index < elements.size(); ++element_index) { + auto const& element = elements[element_index]; + state[element_index] = MUST(AK::Bitmap::create(hover_rules.size(), 0)); + for (size_t rule_index = 0; rule_index < hover_rules.size(); ++rule_index) { + auto const& rule = hover_rules[rule_index]; + auto const& selector = rule.absolutized_selectors()[rule.selector_index]; + + SelectorEngine::MatchContext context; + bool selector_matched = false; + if (rule.can_use_fast_matches) { + if (SelectorEngine::fast_matches(selector, element, {}, context)) + selector_matched = true; + } else { + if (SelectorEngine::matches(selector, element, {}, context, {})) + selector_matched = true; + } + if (element.has_pseudo_elements()) { + if (SelectorEngine::matches(selector, element, {}, context, CSS::Selector::PseudoElement::Type::Before)) + selector_matched = true; + if (SelectorEngine::matches(selector, element, {}, context, CSS::Selector::PseudoElement::Type::After)) + selector_matched = true; + } + if (selector_matched) + state[element_index].set(rule_index, true); + } + } + return state; + }; + + auto previous_hover_selectors_match_state = compute_hover_selectors_match_state(); + m_hovered_node = hovered_node; + auto new_hover_selectors_match_state = compute_hover_selectors_match_state(); + + for (size_t element_index = 0; element_index < elements.size(); ++element_index) { + if (previous_hover_selectors_match_state[element_index].view() == new_hover_selectors_match_state[element_index].view()) + continue; + + elements[element_index].set_needs_style_update(true); + elements[element_index].for_each_in_subtree_of_type([](auto& element) { + element.set_needs_inherited_style_update(true); + return TraversalDecision::Continue; + }); + } +} + void Document::set_hovered_node(Node* node) { if (m_hovered_node.ptr() == node) return; GC::Ptr old_hovered_node = move(m_hovered_node); - m_hovered_node = node; - - auto* common_ancestor = find_common_ancestor(old_hovered_node, m_hovered_node); - if (!style_computer().has_has_selectors()) { - Node& invalidation_root = common_ancestor ? *common_ancestor : document(); - invalidation_root.for_each_in_inclusive_subtree([&](Node& node) { - if (!node.is_element()) - return TraversalDecision::Continue; - auto& element = static_cast(node); - if (element.affected_by_hover()) { - element.set_needs_style_update(true); - } else { - element.set_needs_inherited_style_update(true); - } - return TraversalDecision::Continue; - }); - } else { - invalidate_style(StyleInvalidationReason::Hover); - } + auto* common_ancestor = find_common_ancestor(old_hovered_node, node); + invalidate_style_for_elements_affected_by_hover_change(common_ancestor, node); // https://w3c.github.io/uievents/#mouseout if (old_hovered_node && old_hovered_node != m_hovered_node) { diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index a8e4f9c46dc..18f9894b992 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -174,6 +174,7 @@ public: virtual FlyString node_name() const override { return "#document"_fly_string; } + void invalidate_style_for_elements_affected_by_hover_change(GC::Ptr old_new_hovered_common_ancestor, GC::Ptr hovered_node); void set_hovered_node(Node*); Node* hovered_node() { return m_hovered_node.ptr(); } Node const* hovered_node() const { return m_hovered_node.ptr(); }