LibWeb: Store small invalidation sets inline

Most invalidation sets carry at most one selector feature. Storing
every set in a HashTable pays allocation and hashing costs while
constructing style invalidation data, even in the common case.

Store the first property inline and upgrade to HashTable only when a
second distinct property is inserted. Keep hash and equality checks
direct over the backing so those hot paths do not route through the
public callback iterator.
This commit is contained in:
Andreas Kling 2026-06-18 20:00:17 +02:00 committed by Andreas Kling
parent 13b609133d
commit 2ca74acc3d
2 changed files with 256 additions and 33 deletions

View file

@ -8,6 +8,193 @@
namespace Web::CSS {
InvalidationSet::PropertySet::PropertySet()
: m_empty()
{
}
InvalidationSet::PropertySet::PropertySet(PropertySet const& other)
: m_empty()
{
copy_from(other);
}
InvalidationSet::PropertySet::PropertySet(PropertySet&& other)
: m_empty()
{
move_from(move(other));
}
InvalidationSet::PropertySet::~PropertySet()
{
destroy_storage();
}
InvalidationSet::PropertySet& InvalidationSet::PropertySet::operator=(PropertySet const& other)
{
if (this != &other) {
destroy_storage();
copy_from(other);
}
return *this;
}
InvalidationSet::PropertySet& InvalidationSet::PropertySet::operator=(PropertySet&& other)
{
if (this != &other) {
destroy_storage();
move_from(move(other));
}
return *this;
}
void InvalidationSet::PropertySet::destroy_storage()
{
if (m_storage_type == StorageType::Single) {
m_property.~Property();
return;
}
if (m_storage_type == StorageType::HashTable)
m_properties.~HashTable<Property>();
}
void InvalidationSet::PropertySet::copy_from(PropertySet const& other)
{
m_storage_type = other.m_storage_type;
if (m_storage_type == StorageType::Empty) {
new (&m_empty) Empty();
return;
}
if (m_storage_type == StorageType::Single) {
new (&m_property) Property(other.m_property);
return;
}
new (&m_properties) HashTable<Property>(other.m_properties);
}
void InvalidationSet::PropertySet::move_from(PropertySet&& other)
{
m_storage_type = other.m_storage_type;
if (m_storage_type == StorageType::Empty) {
new (&m_empty) Empty();
return;
}
if (m_storage_type == StorageType::Single) {
new (&m_property) Property(move(other.m_property));
return;
}
new (&m_properties) HashTable<Property>(move(other.m_properties));
}
bool InvalidationSet::PropertySet::is_empty() const
{
return m_storage_type == StorageType::Empty;
}
size_t InvalidationSet::PropertySet::size() const
{
if (m_storage_type == StorageType::Empty)
return 0;
if (m_storage_type == StorageType::Single)
return 1;
return m_properties.size();
}
bool InvalidationSet::PropertySet::contains(Property const& property) const
{
if (m_storage_type == StorageType::Empty)
return false;
if (m_storage_type == StorageType::Single)
return m_property == property;
return m_properties.contains(property);
}
bool InvalidationSet::PropertySet::set(Property property)
{
if (m_storage_type == StorageType::Empty) {
new (&m_property) Property(move(property));
m_storage_type = StorageType::Single;
return true;
}
if (m_storage_type == StorageType::Single) {
if (m_property == property)
return false;
HashTable<Property> properties;
properties.set(m_property, AK::HashSetExistingEntryBehavior::Keep);
properties.set(move(property), AK::HashSetExistingEntryBehavior::Keep);
m_property.~Property();
new (&m_properties) HashTable<Property>(move(properties));
m_storage_type = StorageType::HashTable;
return true;
}
return m_properties.set(move(property), AK::HashSetExistingEntryBehavior::Keep) == AK::HashSetResult::InsertedNewEntry;
}
bool InvalidationSet::PropertySet::include_all_from(PropertySet const& other)
{
if (other.m_storage_type == StorageType::Empty)
return false;
if (other.m_storage_type == StorageType::Single)
return set(other.m_property);
bool changed = false;
for (auto const& property : other.m_properties)
changed |= set(property);
return changed;
}
bool InvalidationSet::PropertySet::operator==(PropertySet const& other) const
{
if (size() != other.size())
return false;
if (m_storage_type == StorageType::Empty)
return true;
if (m_storage_type == StorageType::Single)
return other.contains(m_property);
for (auto const& property : m_properties) {
if (!other.contains(property))
return false;
}
return true;
}
void InvalidationSet::PropertySet::accumulate_hash(u32& property_hash_sum, u32& property_hash_xor) const
{
auto add_property_hash = [&](auto const& property) {
auto property_hash = AK::Traits<Property>::hash(property);
property_hash_sum += property_hash;
property_hash_xor ^= pair_int_hash(property_hash, 0x9e3779b9);
};
if (m_storage_type == StorageType::Empty)
return;
if (m_storage_type == StorageType::Single) {
add_property_hash(m_property);
return;
}
for (auto const& property : m_properties)
add_property_hash(property);
}
IterationDecision InvalidationSet::PropertySet::for_each(Function<IterationDecision(Property const&)> const& callback) const
{
if (m_storage_type == StorageType::Empty)
return IterationDecision::Continue;
if (m_storage_type == StorageType::Single)
return callback(m_property);
for (auto const& property : m_properties) {
if (callback(property) == IterationDecision::Break)
return IterationDecision::Break;
}
return IterationDecision::Continue;
}
bool InvalidationSet::operator==(InvalidationSet const& other) const
{
if (hash() != other.hash())
@ -16,23 +203,23 @@ bool InvalidationSet::operator==(InvalidationSet const& other) const
return false;
if (m_needs_invalidate_whole_subtree != other.m_needs_invalidate_whole_subtree)
return false;
if (m_properties.size() != other.m_properties.size())
return false;
for (auto const& property : m_properties) {
if (!other.m_properties.contains(property))
return false;
}
return true;
return m_properties == other.m_properties;
}
void InvalidationSet::include_all_from(InvalidationSet const& other)
{
m_needs_invalidate_self |= other.m_needs_invalidate_self;
m_needs_invalidate_whole_subtree |= other.m_needs_invalidate_whole_subtree;
for (auto const& property : other.m_properties)
m_properties.set(property);
m_hash = {};
bool changed = false;
if (other.m_needs_invalidate_self && !m_needs_invalidate_self) {
m_needs_invalidate_self = true;
changed = true;
}
if (other.m_needs_invalidate_whole_subtree && !m_needs_invalidate_whole_subtree) {
m_needs_invalidate_whole_subtree = true;
changed = true;
}
changed |= m_properties.include_all_from(other.m_properties);
if (changed)
m_hash = {};
}
bool InvalidationSet::is_empty() const
@ -40,6 +227,12 @@ bool InvalidationSet::is_empty() const
return !m_needs_invalidate_self && !m_needs_invalidate_whole_subtree && m_properties.is_empty();
}
void InvalidationSet::add_property(Property property)
{
if (m_properties.set(move(property)))
m_hash = {};
}
void InvalidationSet::for_each_property(Function<IterationDecision(Property const&)> const& callback) const
{
if (m_needs_invalidate_self) {
@ -50,10 +243,7 @@ void InvalidationSet::for_each_property(Function<IterationDecision(Property cons
if (callback({ Property::Type::InvalidateWholeSubtree }) == IterationDecision::Break)
return;
}
for (auto const& property : m_properties) {
if (callback(property) == IterationDecision::Break)
return;
}
m_properties.for_each(callback);
}
u32 InvalidationSet::hash() const
@ -63,11 +253,7 @@ u32 InvalidationSet::hash() const
u32 property_hash_sum = 0;
u32 property_hash_xor = 0;
for (auto const& property : m_properties) {
auto property_hash = AK::Traits<Property>::hash(property);
property_hash_sum += property_hash;
property_hash_xor ^= pair_int_hash(property_hash, 0x9e3779b9);
}
m_properties.accumulate_hash(property_hash_sum, property_hash_xor);
auto hash = pair_int_hash(m_needs_invalidate_self, m_needs_invalidate_whole_subtree);
hash = pair_int_hash(hash, m_properties.size());

View file

@ -11,6 +11,7 @@
#include <AK/Forward.h>
#include <AK/HashTable.h>
#include <AK/Traits.h>
#include <AK/Variant.h>
#include <LibWeb/CSS/PseudoClass.h>
namespace Web::CSS {
@ -56,28 +57,23 @@ public:
void set_needs_invalidate_class(FlyString const& name)
{
m_properties.set({ Property::Type::Class, name });
m_hash = {};
add_property({ Property::Type::Class, name });
}
void set_needs_invalidate_id(FlyString const& name)
{
m_properties.set({ Property::Type::Id, name });
m_hash = {};
add_property({ Property::Type::Id, name });
}
void set_needs_invalidate_tag_name(FlyString const& name)
{
m_properties.set({ Property::Type::TagName, name });
m_hash = {};
add_property({ Property::Type::TagName, name });
}
void set_needs_invalidate_attribute(FlyString const& name)
{
m_properties.set({ Property::Type::Attribute, name });
m_hash = {};
add_property({ Property::Type::Attribute, name });
}
void set_needs_invalidate_pseudo_class(PseudoClass pseudo_class)
{
m_properties.set({ Property::Type::PseudoClass, pseudo_class });
m_hash = {};
add_property({ Property::Type::PseudoClass, pseudo_class });
}
bool is_empty() const;
@ -87,9 +83,50 @@ public:
u32 hash() const;
private:
class PropertySet {
public:
PropertySet();
PropertySet(PropertySet const&);
PropertySet(PropertySet&&);
~PropertySet();
PropertySet& operator=(PropertySet const&);
PropertySet& operator=(PropertySet&&);
bool is_empty() const;
size_t size() const;
bool contains(Property const&) const;
bool set(Property);
bool include_all_from(PropertySet const&);
bool operator==(PropertySet const&) const;
void accumulate_hash(u32& property_hash_sum, u32& property_hash_xor) const;
IterationDecision for_each(Function<IterationDecision(Property const&)> const&) const;
private:
enum class StorageType : u8 {
Empty,
Single,
HashTable,
};
void destroy_storage();
void copy_from(PropertySet const&);
void move_from(PropertySet&&);
StorageType m_storage_type { StorageType::Empty };
union {
Empty m_empty;
Property m_property;
HashTable<Property> m_properties;
};
};
void add_property(Property);
bool m_needs_invalidate_self { false };
bool m_needs_invalidate_whole_subtree { false };
HashTable<Property> m_properties;
PropertySet m_properties;
mutable Optional<u32> m_hash;
};