Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
27502a2
LibWeb: Repair Rust style test fixtures
awesomekling Aug 18, 2026
3b3ab8e
LibWeb: Record canonical longhand tables once in style captures
awesomekling Aug 16, 2026
e5f5259
LibWeb: Use one match answer identity
awesomekling Aug 16, 2026
5925433
LibWeb: Intern prefix states by content
awesomekling Aug 16, 2026
69a0333
LibWeb: Name selector entries and feature keys once
awesomekling Aug 16, 2026
842b325
LibWeb: Stage tree relations as before/after rows
awesomekling Aug 16, 2026
567b673
LibWeb: Stage element facts as before/after rows
awesomekling Aug 16, 2026
24fc0b4
LibWeb: Stage program fields densely with one commit point
awesomekling Aug 16, 2026
dad6c8d
LibWeb: Verify retained answers against raw selector truth
awesomekling Aug 16, 2026
ebcd9dd
LibWeb: Index fact catalogs by atom and shrink attribute facts
awesomekling Aug 16, 2026
ae9d676
LibWeb: Index primary facts by element identity
awesomekling Aug 16, 2026
1e154ec
LibWeb: Own, count and cap feature postings in the fact store
awesomekling Aug 16, 2026
d708890
LibWeb: Freeze Tier-3 admission by flush period
awesomekling Aug 16, 2026
b170481
LibWeb: Defer Tier-3 eviction to flush boundaries
awesomekling Aug 16, 2026
023ea09
LibWeb: Remove per-container memory refusal
awesomekling Aug 16, 2026
ff2d2f7
LibWeb: Memoize style atoms in the C++ bridge
awesomekling Aug 16, 2026
5d38cd1
LibWeb: Share style atoms across documents
awesomekling Aug 16, 2026
b91b813
LibWeb: Share immutable selector programs across documents
awesomekling Aug 16, 2026
a09ad87
LibWeb: Store transpose routes in compact per-entry columns
awesomekling Aug 16, 2026
38c9298
LibWeb: Cache transpose route liveness and derived indexes
awesomekling Aug 17, 2026
688453f
LibWeb: Store pseudo winner rows by element
awesomekling Aug 17, 2026
1c128c2
LibWeb: Separate winner semantics from provenance
awesomekling Aug 17, 2026
f45e717
LibWeb: Index nodes by winning rule
awesomekling Aug 17, 2026
05f40fc
LibWeb: Consolidate computed pseudo rows and pack publication columns
awesomekling Aug 17, 2026
dd8b4b9
LibWeb: Reclaim unreachable computed records
awesomekling Aug 17, 2026
00c257b
LibWeb: Batch intrinsic element arrivals
awesomekling Aug 17, 2026
4669e2e
LibWeb: Avoid redundant boundary publications
awesomekling Aug 17, 2026
154ffab
LibWeb: Publish attribute text on selector demand
awesomekling Aug 17, 2026
6d4d8fa
LibWeb: Retain style tree depth
awesomekling Aug 17, 2026
cdf4e32
LibWeb: Skip unchanged attribute origin routes
awesomekling Aug 17, 2026
9931846
LibWeb: Remove per-answer copies and sorts on the patch path
awesomekling Aug 17, 2026
b0da631
LibWeb: Gate fixed-cost flush stages on their deltas
awesomekling Aug 17, 2026
71c7d8e
LibWeb: Index dense-keyed style maps as columns
awesomekling Aug 17, 2026
6200384
LibWeb: Share and flatten rule dispatch storage
awesomekling Aug 17, 2026
d276e5b
LibWeb: Cache selector dispatch metadata in the program
awesomekling Aug 17, 2026
1214c7c
LibWeb: Own preallocated style nodes and pending inputs per document
awesomekling Aug 17, 2026
784e0be
LibWeb: Reclaim unused style atoms deterministically
awesomekling Aug 18, 2026
115b72e
LibWeb: Reconcile the StyleEngine documentation
awesomekling Aug 17, 2026
2dffe69
LibWeb: Require longhand identity for style diff shortcut
awesomekling Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 82 additions & 68 deletions Documentation/Style/StyleEngine.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Documentation/Style/StyleEngineTesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ A focused loop for style work:
./bin/test-web --verify-style -f Text/input/css/style-engine/ -f Text/input/css/style-invalidation/
```

The gates are per-mechanism checks that run at their mechanism's site. Some re-derive incremental results through the exact cold evaluator and compare (`LIBWEB_VERIFY_STYLE_ANSWER_PATCH`, `LIBWEB_VERIFY_CASCADE_WINNERS`); others assert structural properties (`LIBWEB_VERIFY_STYLE_PLAN_PROVENANCE`, `LIBWEB_VERIFY_PUBLISHED_STYLE_TRANSACTION`), and three C++-side gates cover input reuse, the computed closure, and the style-diff fast path (`LIBWEB_VERIFY_STYLE_INPUT_REUSE`, `LIBWEB_VERIFY_COMPUTED_CLOSURE`, `LIBWEB_VERIFY_STYLE_DIFF_FAST_PATH`).
The gates are per-mechanism checks that run at their mechanism's site. Some re-derive incremental results through the exact cold evaluator and compare (`LIBWEB_VERIFY_STYLE_ANSWER_PATCH`, `LIBWEB_VERIFY_SELECTOR_TRUTH_DERIVATION`, `LIBWEB_VERIFY_CASCADE_WINNERS`); others assert structural properties (`LIBWEB_VERIFY_STYLE_PLAN_PROVENANCE`, `LIBWEB_VERIFY_PUBLISHED_STYLE_TRANSACTION`), and three C++-side gates cover input reuse, the computed closure, and the style-diff fast path (`LIBWEB_VERIFY_STYLE_INPUT_REUSE`, `LIBWEB_VERIFY_COMPUTED_CLOSURE`, `LIBWEB_VERIFY_STYLE_DIFF_FAST_PATH`).

Verification is **observer-only**: checks compare against private state, cannot publish into engine caches, and are exposed as unit-returning closures, so using a gate to steer engine behavior is a compile error. A verifier must never disable or bypass the fast path it is checking, and an incomplete comparison is a failure, not a skip.
Verification is **observer-only**: structural checks receive an immutable engine view, while checks that need the exact cold evaluator receive a dedicated verifier capability whose only operations perform comparisons. Neither API exposes cache publication or general engine mutation, and every gate returns unit, so a verifier cannot steer engine behavior. A verifier must never disable or bypass the fast path it is checking, and an incomplete comparison is a failure, not a skip.

The four engine-side gates are engine inputs: recordings store their bit set, and replay refuses a capture under a different configuration. The three C++-side gates are outside the recorded surface.
The five engine-side gates are engine inputs: recordings store their bit set, including bit 4 for `LIBWEB_VERIFY_SELECTOR_TRUTH_DERIVATION`, and replay refuses a capture under a different configuration. The three C++-side gates are outside the recorded surface.

## 2. Record and replay

Expand Down
2 changes: 1 addition & 1 deletion Libraries/LibWeb/CSS/ComputedValues.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ void ComputedValues::borrow_style_record_payloads(ReadonlySpan<void const*> payl
}

ComputedStyleRecordView::ComputedStyleRecordView(StyleEngineFFI::FfiStyleRecordView const& view, StyleComputer const& style_computer, StyleRecordID style_record_identity)
: m_style_computer(view.animation_overlay_identity != 0 ? &style_computer : nullptr)
: m_style_computer(&style_computer)
, m_style_record_identity(style_record_identity)
{
VERIFY(view.present);
Expand Down
11 changes: 7 additions & 4 deletions Libraries/LibWeb/CSS/CustomPropertyData.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ class WEB_API CustomPropertyData : public RefCounted<CustomPropertyData> {

// The engine's identities for the names this environment declares, sorted and deduplicated,
// worked out once for the environment rather than once for each element handed it: a page whose
// theme declares a thousand names hands the same thousand to every element under it. Atoms are
// document-local, so the document that asked is part of what the answer is good for.
// theme declares a thousand names hands the same thousand to every element under it. Atom ids
// are process-global, but each document must acquire its own references, so the document that
// asked is part of what the answer is good for.
template<typename InternName>
[[nodiscard]] ReadonlySpan<StyleAtomID> declared_name_atoms(FlatPtr document_identity, InternName&& intern) const
[[nodiscard]] ReadonlySpan<StyleAtomID> declared_name_atoms(FlatPtr document_identity, u64 atom_generation, InternName&& intern) const
{
if (m_cached_name_atoms_document_identity == document_identity)
if (m_cached_name_atoms_document_identity == document_identity && m_cached_name_atoms_generation == atom_generation)
return m_cached_declared_name_atoms.span();
m_cached_declared_name_atoms.clear_with_capacity();
m_cached_declared_name_atoms.ensure_capacity(m_declared_count);
Expand All @@ -73,6 +74,7 @@ class WEB_API CustomPropertyData : public RefCounted<CustomPropertyData> {
}
m_cached_declared_name_atoms.shrink(unique);
m_cached_name_atoms_document_identity = document_identity;
m_cached_name_atoms_generation = atom_generation;
return m_cached_declared_name_atoms.span();
}

Expand Down Expand Up @@ -121,6 +123,7 @@ class WEB_API CustomPropertyData : public RefCounted<CustomPropertyData> {
size_t m_declared_count { 0 };
u64 m_identity { 0 };
mutable FlatPtr m_cached_name_atoms_document_identity { NumericLimits<FlatPtr>::max() };
mutable u64 m_cached_name_atoms_generation { 0 };
mutable Vector<StyleAtomID> m_cached_declared_name_atoms;
mutable FlatPtr m_cached_inheritable_document_identity { NumericLimits<FlatPtr>::max() };
mutable size_t m_cached_inheritable_generation { NumericLimits<size_t>::max() };
Expand Down
80 changes: 64 additions & 16 deletions Libraries/LibWeb/CSS/StyleComputer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,45 @@ StyleComputer::StyleComputer(DOM::Document& document)
{
}

StyleComputer::~StyleComputer() = default;
void StyleComputer::finalize()
{
Base::finalize();
clear_style_sharing_cache();
}

void StyleComputer::clear_style_sharing_cache() const
{
for (auto const& bucket : m_style_sharing_cache) {
for (auto const& entry : bucket.value) {
if (entry.explicitly_inherited_non_inherited_property && !!entry.parent_style_record_identity)
unpin_style_record(entry.parent_style_record_identity);
if (entry.style_record_identity.has_value())
unpin_style_record(*entry.style_record_identity);
}
}
m_style_sharing_cache.clear();
m_style_sharing_cache_entry_count = 0;
}

void StyleComputer::prepare_for_style_engine_transaction() const
{
++m_style_sharing_transaction_generation;
if (m_style_sharing_cache_entry_count > maximum_persistent_style_sharing_entries)
clear_style_sharing_cache();
m_computed_style_invalidation_cache.clear();
m_style_engine_cascade_input_cache.clear();
m_inherited_style_group_swaps.clear();
sweep_custom_property_environments();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void StyleComputer::drop_style_sharing_cache() const
{
clear_style_sharing_cache();
m_computed_style_invalidation_cache.clear();
m_style_engine_cascade_input_cache.clear();
m_inherited_style_group_swaps.clear();
sweep_custom_property_environments();
}

ComputedStyleRecordView StyleComputer::computed_style_record_view(StyleRecordID style_record_identity) const
{
Expand All @@ -160,8 +198,8 @@ ComputedStyleRecordView StyleComputer::computed_style_record_view(StyleRecordID
auto view = m_style_engine.style_record_view(style_record_identity);
if (!view.present)
return {};
if (view.animation_overlay_identity != 0)
pin_style_record(style_record_identity);
pin_style_record(style_record_identity);
++m_computed_style_record_view_pin_count;
return ComputedStyleRecordView { view, *this, style_record_identity };
}

Expand All @@ -175,18 +213,12 @@ void const* StyleComputer::style_record_payloads(StyleRecordID style_record_iden
void StyleComputer::pin_style_record(StyleRecordID style_record_identity) const
{
VERIFY(style_record_identity);
static constexpr u64 animation_overlay_tag = 1ull << 63;
if ((style_record_identity.value() & animation_overlay_tag) == 0)
return;
const_cast<StyleComputer&>(*this).m_style_engine.pin_style_record(style_record_identity);
}

void StyleComputer::unpin_style_record(StyleRecordID style_record_identity) const
{
VERIFY(style_record_identity);
static constexpr u64 animation_overlay_tag = 1ull << 63;
if ((style_record_identity.value() & animation_overlay_tag) == 0)
return;
const_cast<StyleComputer&>(*this).m_style_engine.unpin_style_record(style_record_identity);
}

Expand All @@ -203,6 +235,7 @@ void StyleComputer::unregister_style_node(StyleNodeID style_node_id)
{
if (style_node_id != 0 && style_node_id.value() < m_style_nodes.size()) {
m_style_nodes[style_node_id.value()] = nullptr;
m_style_engine.cancel_preallocated_style_node(style_node_id);
m_style_engine.consume_recorded_element_style_input_change(style_node_id);
}
}
Expand All @@ -228,6 +261,14 @@ void StyleComputer::prepare_elements_for_style_computation()
}
}

void StyleComputer::for_each_style_node(Function<void(DOM::Element&)> callback) const
{
for (auto element : m_style_nodes) {
if (element)
callback(*element);
}
}

void StyleComputer::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
Expand Down Expand Up @@ -2408,7 +2449,9 @@ static Vector<StyleProperty> collect_presentational_hint_properties(DOM::Abstrac
// Which properties a hint decides is a fact about the element, and this is the one place that
// knows it: mapping the attributes needs the element fully built, and for a table cell it needs
// the table's computed style, so it cannot be done when the element arrives.
record_element_presentational_hint_properties(element, properties);
if (element.presentational_hint_properties_need_publication(properties)
&& record_element_presentational_hint_properties(element, properties))
element.did_publish_presentational_hint_properties(properties);
return properties;
}

Expand Down Expand Up @@ -3625,8 +3668,8 @@ StyleEngine::StyleRecordDelta StyleComputer::publish_computed_style_inputs(DOM::
{
auto publication = record_computed_style_inputs(Optional<DOM::AbstractElement> { abstract_element }, values, abstract_element.element().style_node_id());
if (!abstract_element.pseudo_element().has_value()) {
if (auto* record = abstract_element.element().style_input_record(); record && record->bind_next_published_style) {
record->computed_style_record = publication.new_style_record;
if (auto* record = abstract_element.element().style_input_record()) {
record->computed_style_record = record->bind_next_published_style ? publication.new_style_record : StyleRecordID {};
record->bind_next_published_style = false;
}
}
Expand Down Expand Up @@ -3725,6 +3768,8 @@ NonnullRefPtr<ComputedValues const> StyleComputer::materialize_style_record(DOM:
auto& entry = bucket->last();
VERIFY(entry.values.ptr() == values.ptr());
VERIFY(entry.custom_property_data.ptr() == abstract_element.custom_property_data().ptr());
VERIFY(!entry.style_record_identity.has_value());
pin_style_record(publication.new_style_record);
entry.style_record_identity = publication.new_style_record;
}
if (style_record_delta.has_value())
Expand All @@ -3749,8 +3794,8 @@ NonnullRefPtr<ComputedValues const> StyleComputer::materialize_style_record(DOM:
inherited_group_swap_eligible);
if (!!publication.new_style_record) {
if (!abstract_element.pseudo_element().has_value()) {
if (auto* record = abstract_element.element().style_input_record(); record && record->bind_next_published_style) {
record->computed_style_record = publication.new_style_record;
if (auto* record = abstract_element.element().style_input_record()) {
record->computed_style_record = record->bind_next_published_style ? publication.new_style_record : StyleRecordID {};
record->bind_next_published_style = false;
}
}
Expand Down Expand Up @@ -3833,6 +3878,8 @@ NonnullRefPtr<ComputedValues const> StyleComputer::build_and_share_computed_valu
pinned_style_input_values = element.style_input_record()->pinned_values;
cascade_declares_custom_properties = element.style_input_record()->cascade_declares_custom_properties;
}
if (sharing.explicitly_inherited_non_inherited_property && !!sharing.parent_style_record_identity)
pin_style_record(sharing.parent_style_record_identity);
m_style_sharing_cache.ensure(key_hash).append({
.key = move(sharing.key),
.pinned_parent_groups = move(sharing.pinned_parent_groups),
Expand Down Expand Up @@ -3883,6 +3930,8 @@ RefPtr<ComputedValues const> StyleComputer::compute_pseudo_element_style_if_need
auto& entry = bucket->last();
VERIFY(entry.values.ptr() == values.ptr());
VERIFY(entry.custom_property_data.ptr() == abstract_element.custom_property_data().ptr());
VERIFY(!entry.style_record_identity.has_value());
pin_style_record(publication.new_style_record);
entry.style_record_identity = publication.new_style_record;
}
if (style_record_delta.has_value())
Expand Down Expand Up @@ -4743,8 +4792,7 @@ RefPtr<ComputedStyleWorkingSet> StyleComputer::compute_style_impl(DOM::AbstractE
auto materialize_style_record_view = [&](StyleEngine::StyleRecordView const& view, StyleRecordID identity) -> OwnPtr<ComputedStyleRecordView> {
if (!view.present)
return {};
if (view.animation_overlay_identity != 0)
pin_style_record(identity);
pin_style_record(identity);
return make<ComputedStyleRecordView>(view, *this, identity);
};
inheritance_parent_style = materialize_style_record_view(inheritance_parent_style_record, inheritance_parent_style_record_identity);
Expand Down
34 changes: 11 additions & 23 deletions Libraries/LibWeb/CSS/StyleComputer.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class WEB_API StyleComputer final : public GC::Cell {
GC_DECLARE_ALLOCATOR(StyleComputer);

public:
static constexpr bool OVERRIDES_FINALIZE = true;

static void for_each_property_expanding_shorthands(PropertyID, StyleValue const&, Function<void(PropertyID, StyleValue const&)> const& set_longhand_property);
static NonnullRefPtr<StyleValue const> get_non_animated_inherit_value(PropertyID, DOM::AbstractElement);
struct AnimatedInheritValue {
Expand All @@ -58,7 +60,7 @@ class WEB_API StyleComputer final : public GC::Cell {
static Optional<Utf16String> user_agent_style_sheet_source(Utf16View name);

explicit StyleComputer(DOM::Document&);
~StyleComputer();
virtual ~StyleComputer() override = default;

DOM::Document& document() { return m_document; }
DOM::Document const& document() const { return m_document; }
Expand Down Expand Up @@ -100,30 +102,10 @@ class WEB_API StyleComputer final : public GC::Cell {

// Drop caches whose keys contain inputs that are stable only within one engine transaction.
// Style sharing has a self-validating key and survives ordinary transaction boundaries.
void prepare_for_style_engine_transaction() const
{
++m_style_sharing_transaction_generation;
if (m_style_sharing_cache_entry_count > maximum_persistent_style_sharing_entries) {
m_style_sharing_cache.clear();
m_style_sharing_cache_entry_count = 0;
sweep_custom_property_environments();
}
m_computed_style_invalidation_cache.clear();
m_style_engine_cascade_input_cache.clear();
m_inherited_style_group_swaps.clear();
m_custom_property_environments.clear();
}
void prepare_for_style_engine_transaction() const;

// Forget every style one element computed on another's behalf. See m_style_sharing_cache.
void drop_style_sharing_cache() const
{
m_style_sharing_cache.clear();
m_style_sharing_cache_entry_count = 0;
m_computed_style_invalidation_cache.clear();
m_style_engine_cascade_input_cache.clear();
m_inherited_style_group_swaps.clear();
sweep_custom_property_environments();
}
void drop_style_sharing_cache() const;

struct ComputedStyleInvalidation {
RequiredInvalidationAfterStyleChange invalidation;
Expand All @@ -150,6 +132,7 @@ class WEB_API StyleComputer final : public GC::Cell {
[[nodiscard]] void const* style_record_payloads(StyleRecordID) const;
void pin_style_record(StyleRecordID) const;
void unpin_style_record(StyleRecordID) const;
[[nodiscard]] u64 computed_style_record_view_pin_count() const { return m_computed_style_record_view_pin_count; }

// Two elements whose cascade declares the same custom properties against the same inherited
// environment hold the same environment, so they are given one object rather than an object
Expand Down Expand Up @@ -211,6 +194,8 @@ class WEB_API StyleComputer final : public GC::Cell {
void for_each_provisional_transition_effect(DOM::AbstractElement const&, Function<void(Animations::KeyframeEffect&)> const&) const;

private:
virtual void finalize() override;

virtual void visit_edges(Visitor&) override;

[[nodiscard]] StyleEngine::StyleRecordDelta record_computed_style_inputs(Optional<DOM::AbstractElement>, ComputedValues const&, StyleNodeID style_node_id) const;
Expand Down Expand Up @@ -300,6 +285,7 @@ class WEB_API StyleComputer final : public GC::Cell {
};

private:
void clear_style_sharing_cache() const;
[[nodiscard]] NonnullRefPtr<ComputedValues const> build_and_share_computed_values(NonnullRefPtr<ComputedStyleWorkingSet>, DOM::AbstractElement, StyleScope const&, StyleSharingCandidate&) const;
[[nodiscard]] static Vector<GC::Ptr<DOM::ShadowRoot const>, 4> author_context_shadow_roots(DOM::AbstractElement);

Expand Down Expand Up @@ -352,6 +338,7 @@ class WEB_API StyleComputer final : public GC::Cell {
void unregister_style_node(StyleNodeID style_node_id);
[[nodiscard]] GC::Ptr<DOM::Element> element_for_style_node(StyleNodeID style_node_id) const;
void prepare_elements_for_style_computation();
void for_each_style_node(Function<void(DOM::Element&)>) const;

// Style scopes are numbered per document, with zero naming the document's own scope. A scope is
// never reused, so a sheet detached with an identity that has been retired detaches nothing
Expand Down Expand Up @@ -551,6 +538,7 @@ class WEB_API StyleComputer final : public GC::Cell {
CSSPixelRect m_viewport_rect;

mutable StyleEngine m_style_engine;
mutable u64 m_computed_style_record_view_pin_count { 0 };
Vector<GC::Ptr<DOM::Element>> m_style_nodes;
TreeScopeID m_next_tree_scope;
Vector<NonAuthorStyleSheet> m_non_author_style_sheets;
Expand Down
Loading