LibWeb: Differential style resolution - #11088
Conversation
|
Important Review skippedToo many files! This PR contains 1228 files, which is 928 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1228)
You can disable this status message by setting the |
08b6849 to
cfd869f
Compare
|
|
Your pull request has conflicts that need to be resolved before it can be reviewed and merged. Make sure to rebase your branch on top of the latest |
tcl3
left a comment
There was a problem hiding this comment.
I also saw inconsistent results with these tests, so there may nor may not be an issue there:
css/css-view-transitions/only-child-group.htmlcss/css-view-transitions/only-child-new.htmlcss/css-view-transitions/only-child-old.htmlcss/css-view-transitions/only-child-view-transition.htmlcss/selectors/focus-within-004.html
|
Regarding #11003, this branch mostly fixes the issue which that branch sought to address. However, one of the included tests on that branch doesn't pass. The failing part of the test amounts to this: <!DOCTYPE html>
<!-- fails on this branch and `master`, passes on #11003 and in other engines -->
<div id="ancestor" style="--x: red">
<div id="carrier" style="--x: red"></div>
</div>
<script>
document.body.offsetHeight;
ancestor.style.setProperty("--x", "blue");
const carrierValue = getComputedStyle(carrier).getPropertyValue("--x");
document.body.textContent = `${carrierValue === "red" ? "PASS" : "FAIL"} (carrier --x: ${carrierValue})`;
</script>Given this also fails on |
|
Your pull request has conflicts that need to be resolved before it can be reviewed and merged. Make sure to rebase your branch on top of the latest |
3fdcf23 to
514ba23
Compare
|
Your pull request has conflicts that need to be resolved before it can be reviewed and merged. Make sure to rebase your branch on top of the latest |
0d70864 to
159c276
Compare
Timing-mode replay previously inspected every event header before the borrowed reader decoded it again. Fold legacy immutable-style-record skipping into the borrowed decode loop so ordinary events pay for one header decode.
Accept zero-length declaration arrays without requiring non-null data pointers. This ensures removing the final property from a CSS rule reaches the style engine and removes its old cascade contribution. Cover final-property removal through CSSOM in a style transaction test.
Monospace font-size recascade reads raw font sizes from the entire ancestor chain, which the sharing key does not identify. Mark these computations as reading beyond the key so neither another element nor a later computation can reuse their context-dependent result. Cover both document orders to prevent cache order from hiding the bug.
Publish a style recomputation when an atomic move changes the parent. The moved root can otherwise keep the same selector and cascade answers while continuing to use inherited values from its old parent. Propagate changes into light children, shadow trees, and slottables. Add coverage for all three paths.
Keep a constructed sheet's compiled program synchronized through its constructor document while the sheet has no adopters. Reattachment can then safely reuse the existing per-document sheet identity. Use the same lookup for whole-sheet replacement. Cover replacement, insertion, deletion, declaration, and selector mutations.
Root-relative units read document-global font metrics that inherited style does not identify. Clear style sharing when those metrics change and recompute descendants across ancestors that absorb inherited font changes. Cover rem and rlh updates, stale sharing entries, and the broad path for viewport-driven root font changes.
Reconstruct the qualified enclosing layer name when compiling a CSSOM rule insertion. Also retain container-query gating so standalone compilation has the same group context as a whole-sheet walk. Cover insertion into top-level and nested cascade layers.
Changing readonly or disabled can bar controls from constraint validation, but attribute invalidation only published the directly implied states. This left validity facts stale on controls, forms, and ancestor fieldsets. Publish current validity for each affected control so dependent selectors and computed styles observe the transition. Add coverage for readonly and disabled controls, disabled fieldsets, and form-associated custom elements.
Observable array indexed writes always appended their stylesheet to the scope program. Operations such as unshift and splice write displaced entries in descending index order, producing a different cascade order from the final array. Pass the written index to the observable array callback and attach each sheet before its current successor. Add coverage for document and shadow root mutations, including unshift, splice, and reverse.
Slot assignment runs after the disconnect walk has retired the removed slot identity. When no replacement slot existed, the assignment update could no longer name the old edge and discarded the only style delta. Publish assigned elements leaving the slot while its identity is still live. Normal assignment then publishes a second edge if another slot takes over. Extend coverage to the no-replacement teardown path.
Text invalidation only treated an explicit dir=auto attribute as using auto directionality. A bdi element defaults to auto without that attribute, leaving its published direction stale after a text change. Share the auto-directionality predicate between direction resolution and invalidation. Cover character data and text replacement through computed direction and :dir() selector matching.
Language and directionality resolve through an element parent chain, but the style engine stores them as published facts. Moving a subtree under a different parent updated inherited values without updating those facts, leaving :lang() and :dir() selectors stale. Invalidate the moved subtree language cache and republish both facts after a cross-parent move. Cover the moved element and its descendant.
The input reset and WebDriver clear algorithms assigned checkedness directly, bypassing checked-state and validity publication. This left :checked facts and dependent styles stale after a form reset. Route both algorithms through the checkedness setter, then restore their dirty checkedness flag. Cover a checkbox and both members of a radio group.
Heading levels were written to the fact store without a journal input, so :heading() routes never observed headingoffset mutations. Publication could also read a cached level before the DOM tree version bump. Represent heading levels as numeric local features, route changes structurally, and compute a fresh value during publication. Cover direct :heading() matching and an ancestor :has() dependency.
The transpose for a selector on the slot compound of ::slotted() walked from the slot toward its own assigned slot. Slots normally have none, so the route incorrectly produced an empty impact region. Add the inverse slot-to-assignees step and represent it as the shadow host child region, which exact matching safely narrows. Cover adding and removing a slot class with assigned and unassigned light children.
Changing a keyframes rule name only updated the CSSOM object. The style program and scope cache kept the old name, so animations could not start or stop referencing the renamed rule. Retire the compiled rule, rename it, then compile it with the new name. Invalidate its owning scope caches. Cover renames into and away from an animation reference.
Run stylesheet moving steps for style and link elements so their sheets follow their owners in cascade order without replacing their CSSOM objects. Transfer sheets between style scopes when a move crosses a shadow boundary. Cover direct, descendant, cross-scope, and linked stylesheet moves.
Recompute existing links when body link, alink, or vlink changes the document-level presentational hint. Clear the stored hint when the attribute is removed or no longer parses as a legacy color. Cover cached links across document and shadow scopes, including active link changes and attribute removal.
Return to exact matching when the Tier-3 memory controller evicts the cascade winner intern tables during a retained declaration repair. Do not index the state identifier that the eviction just invalidated. Cover declaration repair with a zero-byte Tier-3 retention budget.
Keep a pseudo-element style-sharing cache hit from replacing the originating element's retained style-input metadata. Match the guard already used when publishing a newly computed pseudo-element style.
Snapshot every dependency mark after originating and pseudo-element style computation. Restore them when the originating element reuses its retained style. Keep later attribute, custom-function, tree-counting, and container-query invalidations able to reach the element.
Walk stylesheet owner rules as well as parent rules when reconstructing the context for a dynamically arriving rule. Keep outer import layers, conditions, scopes, and container-query gates on nested imported rules. Cover a second-level import under a layer and a false media condition.
Use the recursive host predicate when deciding whether a compound behind a combinator crosses into its hosted tree. Avoid adding a second crossing to the operand that contains the host selector. Cover a host nested in :is() beside another compound feature.
Preference changes previously published style and media-query updates only to the top-level document. Walk active descendant navigables so embedded documents also observe palette, color-scheme, contrast, and motion changes. Add coverage that changes the preferred color scheme after an iframe has already computed its light-scheme style.
Typed custom-property initial values are cached after absolutization. Track whether that computation used viewport metrics, and discard the cached value when the viewport changes. Since the initial value can be consumed by any element without appearing in its computed dependency flags, restyle the document when an affected registration has been used. Cover a registered initial value while resizing an embedded viewport.
Exercise the asynchronous font-arrival path with root-relative metric units on descendants that do not use the loaded family themselves. This ensures root metric changes continue to propagate across ancestors that absorb ordinary inherited font changes.
This replaces LibWeb's selector matching, style invalidation, cascade, and computed-style retention with a new engine. The engine is built on one idea: style work should be proportional to what changed, not to what might have changed.
A conventional restyle has two phases. First, it marks everything a mutation might affect. Then it re-matches, re-cascades, and rebuilds computed style for all of it. Most of that work ends in "same answer as before." On a real page, opening a menu can invalidate thousands of elements to change the style of a few dozen.
The new engine treats styling as an incremental computation over a change stream. DOM mutations, state changes, CSSOM edits, and environment changes enter as typed deltas. The engine routes each delta to the selectors that depend on it, evaluates the affected elements, and stops as soon as an output is unchanged. In database terms, it is an incremental view maintenance engine: selectors are standing queries, mutations are the change stream, and match answers, cascade winners, and computed styles are materialized views. On real workloads, the engine keeps these views consistent for a fraction of the cost of recomputing them.
flowchart LR A[DOM / CSSOM / state /\nenvironment mutations] --> B[Typed deltas +\nnormalization journal] B --> C[Compiled delta routing] C --> D[Impact regions +\nplan selection] D --> E[Matching\nretained answers, prefix automaton,\nexact evaluator] E --> F[Cascade winners] F --> G[Interned computed records] G --> H[Reactions to C++:\nonly what changed]Each arrow is a stopping point: when a stage's output identity is unchanged, it hands nothing to the next stage. The engine may recompute more than the minimum, but never less. When it cannot cheaply prove that an output is unchanged, it recomputes conservatively; every such proof must be cheaper than the work it avoids.
The engine is written in Rust and lives inside the existing LibWeb crate. C++ remains authoritative for the DOM, the CSSOM, and computed-value construction:
flowchart TB subgraph Cpp[C++ LibWeb] DOM[DOM / CSSOM mutation] --> Input[Fact collection] React[Reaction application] --> SC[Value computation] SC --> Consumers[Layout, paint, script APIs\nread one shared record handle] end subgraph Boundary[Generated FFI boundary] Gen[Calls, recording frames, and replay\ndecoders generated from one specification] end subgraph Rust[Rust engine] Eng[Routing, matching, cascade,\nretained state, memory control] Rec[Record/replay stream] end Input --> Gen --> Eng Eng --> Gen --> React SC -->|publish groups| Eng Eng -.-> RecCorrectness is the design center. The bar for every path is exact equivalence with a full recomputation from authoritative inputs. Standing machinery enforces that bar; convention does not:
Memory is a budgeted resource. Every cache reports exact byte counts into a tiered controller. Acceleration state reserves capacity before it allocates, and a refused reservation evicts state under a benefit-driven policy. Every cached answer can be discarded, because the exact evaluator can re-derive it. A page cannot make the engine retain state proportional to DOM size times stylesheet size.
The engine takes over these responsibilities outright, so this PR also deletes their previous implementations: the invalidation sets and style invalidators, the rule caches, the :has() matching caches, the per-element computed-property objects, and the old selector matcher. Style behavior changes and their test expectations land together with the switch, because there is no fallback path to compare against.
The series is structured for review in order. Commits 1 and 2 are standalone (an AK utility extension and developer tooling). Commits 3 through 8 add the engine subsystem by subsystem as pure additions, inert until referenced, in dependency order: selector IR and compiler, core data model, matching and routing, cascade and computed records, flush and the generated FFI boundary, then record/replay and the test suites. Commit 9 is the atomic switchover that wires the engine in and deletes what it replaces. Commit 10 adds the documentation: an as-built reference for the whole design (Documentation/Style/StyleEngine.md) and a testing and debugging guide (Documentation/Style/StyleEngineTesting.md), both audited against the implementation.
The diff is large because the engine is complete and its evidence rides with it; roughly a third of the additions are tests. It has been developed and validated against the full web test suite under verify mode, the replay corpus, and weeks of daily browsing on real sites.