Skip to content

Commit

Permalink
LibWeb: Further optimize :hover style invalidation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kalenikaliaksandr authored and awesomekling committed Jan 4, 2025
1 parent ae9257b commit 482e5de
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 23 deletions.
7 changes: 6 additions & 1 deletion Libraries/LibWeb/CSS/Selector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,15 @@ Selector::Selector(Vector<CompoundSelector>&& 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)
Expand Down
2 changes: 2 additions & 0 deletions Libraries/LibWeb/CSS/Selector.h
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class Selector : public RefCounted<Selector> {
Optional<PseudoElement> const& pseudo_element() const { return m_pseudo_element; }
NonnullRefPtr<Selector> 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<Selector> absolutized(SimpleSelector const& selector_for_nesting) const;
u32 specificity() const;
String serialize() const;
Expand All @@ -274,6 +275,7 @@ class Selector : public RefCounted<Selector> {
mutable Optional<u32> m_specificity;
Optional<Selector::PseudoElement> m_pseudo_element;
bool m_contains_the_nesting_selector { false };
bool m_contains_hover_pseudo_class { false };

void collect_ancestor_hashes();

Expand Down
19 changes: 15 additions & 4 deletions Libraries/LibWeb/CSS/StyleComputer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,11 @@ bool StyleComputer::should_reject_with_ancestor_filter(Selector const& selector)
}
return false;
}
Vector<MatchingRule> const& StyleComputer::get_hover_rules() const
{
build_rule_cache_if_needed();
return m_hover_rules;
}

Vector<MatchingRule> StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name) const
{
Expand Down Expand Up @@ -2540,7 +2545,7 @@ void StyleComputer::collect_selector_insights(Selector const& selector, Selector
}
}

NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin, SelectorInsights& insights)
NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin, SelectorInsights& insights, Vector<MatchingRule>& hover_rules)
{
auto rule_cache = make<RuleCache>();

Expand Down Expand Up @@ -2623,6 +2628,10 @@ NonnullOwnPtr<StyleComputer::RuleCache> 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;
Expand Down Expand Up @@ -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()
Expand All @@ -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&)
Expand Down
4 changes: 3 additions & 1 deletion Libraries/LibWeb/CSS/StyleComputer.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class StyleComputer {
[[nodiscard]] GC::Ref<ComputedProperties> compute_style(DOM::Element&, Optional<CSS::Selector::PseudoElement::Type> = {}) const;
[[nodiscard]] GC::Ptr<ComputedProperties> compute_pseudo_element_style_if_needed(DOM::Element&, Optional<CSS::Selector::PseudoElement::Type>) const;

Vector<MatchingRule> const& get_hover_rules() const;
Vector<MatchingRule> collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional<CSS::Selector::PseudoElement::Type>, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name = {}) const;

void invalidate_rule_cache();
Expand Down Expand Up @@ -267,13 +268,14 @@ class StyleComputer {
HashMap<FlyString, NonnullRefPtr<Animations::KeyframeEffect::KeyFrameSet>> rules_by_animation_keyframes;
};

NonnullOwnPtr<RuleCache> make_rule_cache_for_cascade_origin(CascadeOrigin, SelectorInsights&);
NonnullOwnPtr<RuleCache> make_rule_cache_for_cascade_origin(CascadeOrigin, SelectorInsights&, Vector<MatchingRule>& hover_rules);

RuleCache const& rule_cache_for_cascade_origin(CascadeOrigin) const;

static void collect_selector_insights(Selector const&, SelectorInsights&);

OwnPtr<SelectorInsights> m_selector_insights;
Vector<MatchingRule> m_hover_rules;
OwnPtr<RuleCache> m_author_rule_cache;
OwnPtr<RuleCache> m_user_rule_cache;
OwnPtr<RuleCache> m_user_agent_rule_cache;
Expand Down
89 changes: 72 additions & 17 deletions Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/Bitmap.h>
#include <AK/CharacterTypes.h>
#include <AK/Debug.h>
#include <AK/GenericLexer.h>
Expand Down Expand Up @@ -36,6 +37,7 @@
#include <LibWeb/CSS/FontFaceSet.h>
#include <LibWeb/CSS/MediaQueryList.h>
#include <LibWeb/CSS/MediaQueryListEvent.h>
#include <LibWeb/CSS/SelectorEngine.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/CSS/SystemColor.h>
Expand Down Expand Up @@ -1485,31 +1487,84 @@ static Node* find_common_ancestor(Node* a, Node* b)
return nullptr;
}

void Document::set_hovered_node(Node* node)
void Document::invalidate_style_for_elements_affected_by_hover_change(GC::Ptr<Node> old_new_hovered_common_ancestor, GC::Ptr<Node> hovered_node)
{
if (m_hovered_node.ptr() == node)
auto const& hover_rules = style_computer().get_hover_rules();
if (hover_rules.is_empty())
return;

GC::Ptr<Node> old_hovered_node = move(m_hovered_node);
m_hovered_node = node;
auto& invalidation_root = [&] -> Node& {
if (style_computer().has_has_selectors())
return *this;
return old_new_hovered_common_ancestor ? *old_new_hovered_common_ancestor : *this;
}();

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<Element&>(node);
if (element.affected_by_hover()) {
element.set_needs_style_update(true);
} else {
element.set_needs_inherited_style_update(true);
Vector<Element&> elements;
invalidation_root.for_each_shadow_including_inclusive_descendant([&](Node& node) {
if (!node.is_element())
return TraversalDecision::Continue;
auto& element = static_cast<Element&>(node);
if (element.affected_by_hover())
elements.append(element);
return TraversalDecision::Continue;
});

auto compute_hover_selectors_match_state = [&] {
Vector<AK::Bitmap> 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<Element>([](auto& element) {
element.set_needs_inherited_style_update(true);
return TraversalDecision::Continue;
});
} else {
invalidate_style(StyleInvalidationReason::Hover);
}
}

void Document::set_hovered_node(Node* node)
{
if (m_hovered_node.ptr() == node)
return;

GC::Ptr<Node> old_hovered_node = move(m_hovered_node);
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) {
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/DOM/Document.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class Document

virtual FlyString node_name() const override { return "#document"_fly_string; }

void invalidate_style_for_elements_affected_by_hover_change(GC::Ptr<Node> old_new_hovered_common_ancestor, GC::Ptr<Node> 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(); }
Expand Down

0 comments on commit 482e5de

Please sign in to comment.