Files
ladybird/Libraries/LibWeb/DOM/StyleInvalidator.cpp
Andreas Kling 97e2b05004 LibWeb: Merge equivalent style invalidation plans
Compare invalidation sets, rules, and plans structurally so repeated
descendant and sibling invalidation entries can be merged even when
they were built as separate payload objects.

Also deduplicate pending and active descendant invalidations in the
style invalidator so equivalent rules are not re-applied as the DOM
walk descends. This reduces :has() invalidation fanout while keeping
behavior the same.
2026-04-20 13:20:41 +02:00

182 lines
6.6 KiB
C++

/*
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ScopeGuard.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/StyleInvalidator.h>
namespace Web::DOM {
GC_DEFINE_ALLOCATOR(StyleInvalidator);
static bool element_matches_invalidation_rule(Element const& element, CSS::InvalidationSet const& match_set, bool match_any)
{
return match_any || element.includes_properties_from_invalidation_set(match_set);
}
void StyleInvalidator::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
for (auto const& it : m_pending_invalidations)
visitor.visit(it.key);
}
void StyleInvalidator::invalidate(Node& node)
{
perform_pending_style_invalidations(node, false);
m_pending_invalidations.clear();
}
bool StyleInvalidator::enqueue_invalidation_plan(Node& node, StyleInvalidationReason reason, CSS::InvalidationPlan const& plan)
{
if (plan.is_empty())
return false;
if (plan.invalidate_whole_subtree) {
node.invalidate_style(reason);
return true;
}
if (plan.invalidate_self)
node.set_needs_style_update(true);
add_pending_invalidation(node, reason, plan);
if (auto* element = as_if<Element>(node)) {
for (auto const& sibling_rule : plan.sibling_rules)
apply_sibling_invalidation(*element, reason, sibling_rule);
}
return false;
}
void StyleInvalidator::add_pending_invalidation(GC::Ref<Node> node, StyleInvalidationReason reason, CSS::InvalidationPlan const& plan)
{
if (plan.descendant_rules.is_empty())
return;
auto& pending_invalidations = m_pending_invalidations.ensure(node, [] {
return Vector<PendingDescendantInvalidation> {};
});
for (auto const& descendant_rule : plan.descendant_rules) {
PendingDescendantInvalidation pending_invalidation { reason, descendant_rule };
if (!pending_invalidations.contains_slow(pending_invalidation))
pending_invalidations.append(move(pending_invalidation));
}
}
void StyleInvalidator::apply_invalidation_plan(Element& element, StyleInvalidationReason reason, CSS::InvalidationPlan const& plan, bool& invalidate_entire_subtree)
{
if (plan.is_empty())
return;
if (plan.invalidate_whole_subtree) {
element.invalidate_style(reason);
invalidate_entire_subtree = true;
element.set_needs_style_update_internal(true);
if (element.has_child_nodes())
element.set_child_needs_style_update(true);
return;
}
if (plan.invalidate_self)
element.set_needs_style_update(true);
for (auto const& descendant_rule : plan.descendant_rules) {
PendingDescendantInvalidation pending_invalidation { reason, descendant_rule };
if (!m_active_descendant_invalidations.contains_slow(pending_invalidation))
m_active_descendant_invalidations.append(move(pending_invalidation));
}
for (auto const& sibling_rule : plan.sibling_rules)
apply_sibling_invalidation(element, reason, sibling_rule);
}
void StyleInvalidator::apply_sibling_invalidation(Element& element, StyleInvalidationReason reason, CSS::SiblingInvalidationRule const& sibling_rule)
{
auto apply_if_matching = [&](Element* sibling) {
if (!sibling)
return;
if (!element_matches_invalidation_rule(*sibling, sibling_rule.match_set, sibling_rule.match_any))
return;
(void)enqueue_invalidation_plan(*sibling, reason, *sibling_rule.payload);
};
switch (sibling_rule.reach) {
case CSS::SiblingInvalidationReach::Adjacent:
apply_if_matching(element.next_element_sibling());
break;
case CSS::SiblingInvalidationReach::Subsequent:
for (auto* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling())
apply_if_matching(sibling);
break;
default:
VERIFY_NOT_REACHED();
}
}
// This function makes a full pass over the entire DOM and:
// - converts "entire subtree needs style update" into "needs style update" for each inclusive descendant where it's found.
// - applies descendant invalidation rules to matching elements
void StyleInvalidator::perform_pending_style_invalidations(Node& node, bool invalidate_entire_subtree)
{
invalidate_entire_subtree |= node.entire_subtree_needs_style_update();
if (invalidate_entire_subtree) {
node.set_needs_style_update_internal(true);
if (node.has_child_nodes())
node.set_child_needs_style_update(true);
}
auto previous_active_descendant_invalidations_size = m_active_descendant_invalidations.size();
ScopeGuard restore_state = [this, previous_active_descendant_invalidations_size] {
m_active_descendant_invalidations.shrink(previous_active_descendant_invalidations_size);
};
if (!invalidate_entire_subtree) {
if (auto pending_invalidations = m_pending_invalidations.get(node); pending_invalidations.has_value()) {
m_active_descendant_invalidations.extend(*pending_invalidations);
}
if (auto* element = as_if<Element>(node)) {
size_t invalidation_index = 0;
while (invalidation_index < m_active_descendant_invalidations.size()) {
auto const& pending_invalidation = m_active_descendant_invalidations[invalidation_index++];
if (!element_matches_invalidation_rule(*element, pending_invalidation.rule.match_set, pending_invalidation.rule.match_any))
continue;
apply_invalidation_plan(*element, pending_invalidation.reason, *pending_invalidation.rule.payload, invalidate_entire_subtree);
if (invalidate_entire_subtree)
break;
}
if (invalidate_entire_subtree) {
node.set_needs_style_update_internal(true);
if (node.has_child_nodes())
node.set_child_needs_style_update(true);
}
}
}
for (auto* child = node.first_child(); child; child = child->next_sibling())
perform_pending_style_invalidations(*child, invalidate_entire_subtree);
if (node.is_element()) {
auto& element = static_cast<Element&>(node);
if (auto shadow_root = element.shadow_root()) {
perform_pending_style_invalidations(*shadow_root, invalidate_entire_subtree);
if (invalidate_entire_subtree)
node.set_child_needs_style_update(true);
}
}
node.set_entire_subtree_needs_style_update(false);
}
}