diff --git a/Documentation/Style/StyleEngine.md b/Documentation/Style/StyleEngine.md index dd6a436f57a8d..94b0b68c6499f 100644 --- a/Documentation/Style/StyleEngine.md +++ b/Documentation/Style/StyleEngine.md @@ -151,8 +151,8 @@ Conservative neither is executable; keep the routed region as-is * **Style read epoch**: a mutation-free interval in which style reads the authoritative live DOM and other versioned inputs. * **Style node**: the engine's compact identity for a DOM element. * **Style feature atom**: a compact identity for a selector-mentioned tag, ID, class, attribute, value, namespace, or state fact. It does not own duplicate string payload. -* **Match program**: bytecode answering whether a candidate subject matches a selector during one read epoch. -* **Transpose program**: bytecode that starts from a changed selector input and traverses inverse selector relations to enumerate a safe superset of subjects whose match truth can change. +* **Match program**: a logical-IR evaluator answering whether a candidate subject matches a selector during one read epoch. +* **Transpose program**: compiled inverse routes that start from a changed selector input and enumerate a safe superset of subjects whose match truth can change. * **Delta-routing registry**: required program state mapping a typed semantic input key to transpose entry points. * **Impact region**: the normalized union of subjects, relation ranges, and dependent scopes emitted by transpose programs for a transaction. * **Impact-region attribution**: the split between regions attributed to a specific rule and program (eligible for exact planning) and unattributed regions (evaluated conservatively). @@ -220,7 +220,7 @@ A removed style-node identity and any required relation tombstone stay valid unt Resident derived state supplies old selector truth when available. If it was evicted, discard the affected derived fragment, evaluate the final state from the live DOM, and compare with the last committed style output. -**Journal overflow.** The journal is byte-accounted against its own memory category. When growing it is refused, the most numerous input kind's fine-grained entries are replaced with a typed **complete-scope marker** covering the whole document for that kind, and later records of that kind are dropped outright. At flush a marker widens the impact region to the document, and the plan evaluates that region exactly (§3). A pathological script can force broad discovery work but cannot force unbounded journal memory. (Environment markers are exempted from disabling the retained-selector fast paths, since environment changes cannot alter selector incidence.) +**Journal overflow.** The journal is byte-accounted against its own memory category. Its capacity allows four fine-grained entries per connected element and is clamped between 4 MiB and 32 MiB. When growth would cross that capacity, the most numerous reconstructible fact, relation, program, or topology input kind's fine-grained entries are replaced with a typed **complete-scope marker** covering the whole document for that kind, and later records of that kind are dropped outright. Edge-triggered `ElementStyleInput` actions are never marker-coarsened: their reaction and inherited-group bits remain attached to every target and repeated actions on one target are unioned. At flush a marker widens the impact region to the document, and the plan evaluates that region exactly (§3). A pathological script can force broad discovery work but cannot discard an action whose target cannot be reconstructed from final facts. (Environment markers are exempted from disabling the retained-selector fast paths, since environment changes cannot alter selector incidence.) ### 4.5 Mutations during evaluation @@ -242,7 +242,8 @@ Do not blindly duplicate fields already on DOM elements. Persistent style-side s * a minimal style-data handle reachable from an element; * required evaluator-local relation-navigation state; -* sparse segmented columns for optional per-element information; +* directly indexed columns for optional per-element fact metadata; +* compact dense columns for live computed-style publication handles; * reusable temporary columnar batches for bounded-region evaluation; * indexes containing only style-relevant facts. @@ -251,14 +252,16 @@ parent required, dense first element child required, dense next element sibling required, dense previous element sibling required, dense +depth required, dense tree scope conditional, allocated only for multi-scope documents assigned slot allocated per segment shadow host allocated per segment -local feature-set identity optional, Tier 3 -style-record identity Tier 1 for observed content +fact metadata header optional payload in a dense directly indexed column +prefix local-fact identity optional, Tier 3 segmented column +computed publication handles compact dense columns, Tier 1 for observed content ``` -Optional columns are allocated per segment only when useful. A document with no shadow trees allocates no shadow relationship columns. +Shadow-relation and prefix-cache columns are allocated per segment only when useful. Fact metadata and computed publication use dense directly indexed columns whose vacant rows carry no external payload; they trade empty slots below the highest used identity for constant-time access. A document with no shadow trees allocates no shadow relationship columns. ### 5.3 Required relation navigation @@ -269,15 +272,15 @@ Concretely: for `.theme .item` and a class mutation on a container, the transpos Rules: * Dense `u32`, allocated for every connected style node, charged to **Tier 1**, counted against the mandatory per-node byte budget (§10.5). -* **Not semantically authoritative.** The exact cold evaluator can always read the live DOM. The columns are a derived projection that must agree with the live tree at every epoch boundary. -* Not evicted under ordinary memory pressure; Tier 3 is evicted first. If they cannot be allocated, evaluation stays exact through bounded live-fact batches. +* The DOM remains the Web-facing semantic owner, while these columns are the engine's committed evaluation projection. C++ must publish every relation mutation, and the columns must agree with the final live tree at every epoch boundary; the exact evaluator reads this projection rather than fetching the DOM in reverse. +* Not evicted under ordinary memory pressure; Tier 3 is evicted first. Their allocation is required engine state, and a missing committed relation row is an invariant violation rather than a cold-path signal. * Ownership follows the tree: the subsystem that mutates DOM structure publishes relation-column updates as part of the same `TreeDelta` that reports the mutation, so they cannot drift within a read epoch. Mismatch is an invariant violation, not a recoverable condition. Tree-scope navigation has the same traversal requirement once multiple scopes are supported, because scope-membership transposition works the same way. Allocate the dense tree-scope column only for documents that need it, and charge it to the conditional byte allowance (§10.5). Flat-tree operations derive from the sparse assigned-slot and shadow-host relations instead of duplicating a third parent column. ### 5.4 Temporary packing -Packing is an execution choice, not snapshot construction. Small or selective work queries the live tree directly. A large batch may gather only its requested columns and affected region into Tier-4 scratch when dense traversal, SIMD execution, or bridge amortization wins. Batches are generation checked, released after the transaction, and never authoritative. Whole-document cold evaluation streams bounded batches; it never allocates a complete flattened DOM copy. +Packing is an execution choice, not snapshot construction. Small or selective work queries the resident columns directly. A sufficiently large transaction compiles its affected regions into a preorder topology charged to Tier-4 scratch; broad matching holds an immutable shared view of the primary fact columns, while transaction-specific pre-images remain bounded overlays. Scratch is generation checked and released after the transaction. Whole-document cold evaluation never allocates a second complete flattened DOM or fact-store copy. ### 5.5 Multiple tree relations @@ -365,7 +368,7 @@ Compound operands transpose to the node whose local fact changed. Structural pse It covers tag, ID, class, attribute, namespace, state, tree relation, structural, scope/topology, and activation inputs. Selectors with no selective local atom go into a **typed** always-consulted slice for the input kinds that can affect them, not one global universal bucket. A child-list mutation must not consult programs that depend only on an environment predicate. -Construction and bytes are charged to program compilation and Tier 2. Sorted delta-coded program and entry-point IDs keep the structure proportional to selector-input incidence, not element count. +Construction and bytes are charged to program compilation and Tier 2. Each input key indexes one insertion-ordered route slice, and all slices are packed into a flat `RouteID` vector; route headers and their variable-length paths live in parallel columnar arenas. The structure is proportional to selector-input incidence, not element count. The registry is required, not optional: scanning every selector header for every ordinary mutation changes the hot-path asymptotics. Feature postings (Tier 3) may accelerate subject enumeration, but evicting them never changes which transpose entry points run. Program replacement keeps old registry entries alive through old-result removal and installs the new program and registry atomically in the next epoch. @@ -400,16 +403,16 @@ The exact batch plan consumes only a complete impact region. One selector semantics, three cooperating execution kernels. The boundaries are physical, so operators can move between them later without changing query meaning. -**Local-compound kernel**: the dominant hot path. Evaluates tag names, IDs, classes, attributes, namespaces, element states, and Boolean compound structure. Uses fixed-width interned atom IDs at the C++/Rust boundary, a rightmost-feature dispatch key, and compact fused bytecode. Candidate tests are ordered cheap/high-rejection first and short-circuit on failure. **A compound with no relational operator allocates no witness.** (Non-relational chains do participate in the retained prefix automaton below when admitted; that state is Tier-3 and evictable, never required.) +**Local-compound kernel**: the dominant hot path. Evaluates tag names, IDs, classes, attributes, namespaces, element states, and Boolean compound structure. Uses fixed-width interned atom IDs at the C++/Rust boundary, a rightmost-feature dispatch key, and an arena of fixed-width enum nodes with shared operand and literal tables. Candidate tests are ordered cheap/high-rejection first and short-circuit on failure. **A compound with no relational operator allocates no witness.** (Non-relational chains do participate in the retained prefix automaton below when admitted; that state is Tier-3 and evictable, never required.) -Selector-used features have two sparse directions with independently bounded sizes: +Selector feature keys have two sparse directions with independently bounded sizes: ```text feature atom -> candidate StyleNodeID posting Tier 3, optional feature atom -> transpose entry points Tier 2, required, program-proportional ``` -The first avoids scanning DOM facts and may be evicted. The second lets a class, attribute, tag, ID, or state mutation activate only bytecode mentioning the changed feature. If the posting is evicted, the routed transpose program reads authoritative DOM facts or uses an exact scope batch. No element-by-selector relation is ever required. +The first avoids scanning DOM facts and may be evicted. The second lets a class, attribute, tag, ID, or state mutation activate only routes mentioning the changed feature. If the posting is evicted, the routed transpose program reads authoritative DOM facts or uses an exact scope batch. No element-by-selector relation is ever required. **Directional-combinator kernel**: child, descendant, adjacent-sibling, and subsequent-sibling relationships consume the same local compound programs, under operation-specific schedules: @@ -418,19 +421,21 @@ The first avoids scanning DOM facts and may be evicted. The second lets a class, * *Mutation to an attached program*: start from changed local truths. Descendant and child effects run as a top-down prefix-state pass over the exact affected scope, warm-started from the retained prefix states of the previous transaction where those survived. * *Sibling effects*: adjacent-sibling inspects the immediately participating siblings; subsequent-sibling and structural effects use the same automaton's child-sequence machinery and stop at unchanged outputs. -These are schedules over the same bytecode. +These are schedules over the same logical IR. **The prefix automaton.** Selector-prefix truths are retained across transactions as a Tier-3 cache: interned prefix states (the "context tokens" entering each node), per-element entering states, per-element terminal match sets, and per-element positional truth bits. Registration is budgeted: an automaton carries at most 32 structural-test truth bits, and a chain that would overflow is refused whole and stays with its exact routes, so refusal degrades to exact matching, never to a wrong answer. The cache's bytes cycle through scratch during a flush and are retained at the end of a patched flush; it is prioritized above the retained answers it maintains (§10.3), and discarding it is always legal; the next flush rebuilds from exact evaluation. **Relational-query kernel**: `RelativeExists` reuses local compounds and directional traversal to answer an existential question per anchor. Witnesses belong only to this kernel. A document with no relational selector allocates no relational queues, per-node relational fields, or relational cache tables (the witness table is empty and unallocated, and relational routing early-returns on an empty route set). -Selector lists and Boolean functional pseudo-classes compile to compact branch bytecode around these kernels. Specificity and match metadata stay static program data rather than being reconstructed per candidate. +Selector lists and Boolean functional pseudo-classes compile to branch nodes in the same IR. Specificity, dispatch analysis, and match metadata stay static program data rather than being reconstructed per candidate or scope-dispatch build. ### 6.6 Local feature indexes -Index keys are semantic feature atoms, not strings: a tag/namespace pair, ID, class, attribute presence, attribute exact value with case mode, or state. Substring and token attribute operators drive from an attribute-name posting and run their exact value test in compound bytecode rather than demanding a posting per substring. +Selector posting keys are semantic feature atoms, not strings: tag name, ID, class, attribute name, directionality, part name, and custom state. Namespace, ordinary state, root, heading, and other fixed facts are read from the authoritative row when dispatch reaches a candidate. Every attribute operator drives from the attribute-name posting; exact equality can reject on its value atom before matching, while substring and token operators run their exact text test in the compound evaluator. -Representation: **chunked sorted postings**, sorted by `StyleNodeID`. The index contains only features used by an attached selector program; removing the last consumer makes it reclaimable. +Representation: **chunked sorted postings**, sorted by `StyleNodeID`. While the Tier-3 category is admitted, every published selector feature is maintained eagerly, whether or not the current program mentions it. Forgetting an element removes all of its posting memberships; memory pressure evicts the complete posting category, and later selective demand may rebuild its missing keys from authoritative facts. + +One key stops retaining a posting when its membership grows beyond the greater of 4096 elements and one quarter of the live elements. The key then reads as missing acceleration, so every consumer takes its exact fallback instead of retaining a near-document-sized secondary index. *Database counterpart:* secondary inverted indexes, and non-clustered ones: `StyleNodeID` is not a tree-order label, so proving a posting candidate lies in a region is the row lookup a non-clustered index pays when the predicate lives on the clustering dimension. @@ -442,7 +447,7 @@ Transaction-local preorder arrays make repeated relation checks constant time on The plan choice is made once, before enumeration, from candidate cardinality and region size: **the exact batch plan is chosen when the smallest safe candidate source covers more than one quarter of the region** (`SELECTIVE_SHARE_DIVISOR = 4`). A candidate source of 64 or fewer skips the cost model entirely and goes straight to direct membership checks. -The cutoff uses **exact cardinality** of normalized impact ranges. If exact cardinality is unavailable, the batch plan is chosen rather than a smaller scope guessed. Scratch or Tier-3 refusal can independently force the batch plan (or, when no batch is executable, the conservative region) before enumeration begins. +The cutoff uses **exact cardinality** of normalized impact ranges. If exact cardinality is unavailable, the batch plan is chosen rather than a smaller scope guessed. Missing Tier-3 acceleration can make the exact batch plan cheaper, but memory accounting does not change the plan after enumeration begins. ### 6.8 Query outputs @@ -476,7 +481,7 @@ The cold evaluator and the incremental evaluator consume the same static specifi ### 6.10 Ephemeral selector consumers -Selectors passed to `querySelector()`, `querySelectorAll()`, `matches()`, and `closest()` are **match-only query programs**. They compile with the same selector semantics and evaluate with a bare match evaluator over the engine's fact store, but they are not attached style programs: no transpose bytecode, no routing registry entries, no rule identities, no witness reads or writes, no cascade state. Scope roots, shadow boundaries, relative-selector anchoring, and syntax failure are explicit compiler inputs. Result ownership belongs to the DOM API, not StyleEngine. +Selectors passed to `querySelector()`, `querySelectorAll()`, `matches()`, and `closest()` are **match-only query programs**. They compile with the same selector semantics and evaluate with a bare match evaluator over the engine's fact store, but they are not attached style programs: no transpose routes, no routing registry entries, no rule identities, no witness reads or writes, no cascade state. Scope roots, shadow boundaries, relative-selector anchoring, and syntax failure are explicit compiler inputs. Result ownership belongs to the DOM API, not StyleEngine. Two mechanisms make this correct and fast against a live document: @@ -526,7 +531,7 @@ A retained witness is a proof cache, never semantic state; evicting it is always *Database counterpart:* `:has()` truth is an EXISTS aggregate, so only zero-to-nonzero transitions are deltas. The retained witness is a one-row partial materialization of the subquery; losing it is a view miss repaired by an upquery, never a semantic event. -**Bounds.** A retained witness is re-verified on read (liveness, axis membership, and a match re-check), with a fixed step cap on the following-sibling back-walk; any verification failure clears the entry and routes conservatively. The witness table itself is capped (currently 16,384 entries) and is dropped whole on a refused memory grant. Replacement after a lost witness is the ordinary forward walk of the query's axis; the routing-side cutoff (§6.7) bounds how witness-side discovery drives anchors. A Boolean result is never approximated. +**Bounds.** A retained witness is re-verified on read (liveness, axis membership, and a match re-check), with a fixed step cap on the following-sibling back-walk; any verification failure clears the entry and routes conservatively. The witness table itself is capped (currently 16,384 entries). Limit-crossing growth remains available for its current quota period, and the table is dropped whole at the following boundary. Replacement after a lost witness is the ordinary forward walk of the query's axis; the routing-side cutoff (§6.7) bounds how witness-side discovery drives anchors. A Boolean result is never approximated. ### 7.4 Negative results @@ -596,7 +601,9 @@ Attachment connects a compiled program to one or more style scopes and is distin One compiled program attaches to multiple scopes without duplicating its logical selector or declaration representation. Scope-specific indexes and materializations stay separate where tree membership requires it. -Match and transpose bytecode contains document-local `StyleAtomID` values and is therefore **compiled per document**. Routing registries, attachments, order tokens, and all result materializations are always document-local. Authoritative parsed stylesheet resources may be shared across documents, but nothing carrying one document's IDs, generations, scopes, or privacy state crosses a document boundary. +Selector programs contain process-global `StyleAtomID` values and immutable equal programs are shared across document engines on the StyleEngine thread. Each document retains references to the global atoms it uses; the final document releases the underlying interned-string identity and makes the numeric atom reusable. Entry and rule identities, routing registries, attachments, order tokens, and all result materializations remain document-local, so no document generation, tree scope, or result state crosses the sharing boundary. + +The memory lease for a shared selector payload belongs to the process-global selector-program table. Document controllers report only their local program identities and indexes; the shared table reports each immutable payload once and keeps that charge until the payload's final document reference drops. ### 8.5 Source order @@ -833,7 +840,7 @@ AND ScopeConditionTrue -> ActiveRuleMatches ``` -An environment change can toggle already-known matches without re-running selector logic. Representation: compact predicate bytecode plus one current result per attached group (Tier 3, evictable, reevaluated from typed environment inputs). Do not build a persistent inactive-rule match cache; activation evaluates the selector program through normal inputs, reusing a cached selector answer only when one is already available in the current traversal. +An environment change can toggle already-known matches without re-running selector logic. C++ evaluates media and supports conditions and publishes the resulting per-sheet or per-rule `conditions_hold` flips; container conditions are answered per element during matching instead. Rust retains the document-wide condition-program identity and committed Boolean in required program state. Do not build a persistent inactive-rule match cache; activation evaluates the selector program through normal inputs, reusing a cached selector answer only when one is already available in the current traversal. Environment inputs: viewport and page size; device pixel ratio; media features and preferences; document URL and target state; font selection and loading state; container sizes and styles; scroll and view timeline state; anchor-positioning inputs. Changes publish typed deltas to actual consumers. @@ -859,7 +866,7 @@ Memory is a primary constraint. Time improvements requiring unbounded retained s * **Tier 0: authoritative semantic inputs.** DOM, CSSOM, stylesheet programs, browser state. Reference it; do not duplicate it. * **Tier 1: minimal live style state.** Compact handles and shared payloads required to answer current observers. Not evictable without first proving the content has no observer. -* **Tier 2: shared semantic IR and intern pools.** Canonical selector, declaration, condition, cascade nodes; routing registry; transpose bytecode. Bounded relative to the compact parsed stylesheet program; cannot absorb selector-result state. +* **Tier 2: shared semantic IR and intern pools.** Canonical selector, declaration, condition, cascade nodes; routing registry; transpose route programs. Bounded relative to the compact parsed stylesheet program; cannot absorb selector-result state. * **Tier 3: acceleration materializations.** Indexes, match sets, inverse maps, witnesses, proofs, flattened environments. Strictly budgeted and fully evictable. * **Tier 4: transaction scratch.** Reusable arenas for flattened relation ranges, delta queues, batch evaluation, comparison. High-water marks monitored; shrinks after unusually large work. @@ -871,7 +878,7 @@ An interned object is not automatically Tier 2. Contexts, summaries, match answe | Active read-epoch header and generation | 1 | Reclaim after every reader retires | | Attached compact semantic rule program | 2 | Reclaim after detachment and epoch retirement | | Style feature atom mapping | 2 | Reclaim after last program/posting reference and epoch retirement | -| Delta-routing registry and transpose bytecode | 2 | Reclaim with the selector program and retired epochs | +| Delta-routing registry and transpose routes | 2 | Reclaim with the selector program and retired epochs | | Stable rule, attachment, and order nodes | 2 | Reclaim after semantic detachment and epoch retirement | | Memory controller and aggregate accounting | (uncharged) | Reclaim with the document | | `StyleNodeID` and DOM-to-style-node mapping | 1 | Reclaim after disconnection and epoch retirement | @@ -892,23 +899,23 @@ An interned object is not automatically Tier 2. Contexts, summaries, match answe Each document has a derived-style memory controller tracking exact bytes by logical operator, physical representation, scope, and tier. -**Tier-3 allocations reserve their full capacity charge before allocation or growth.** The controller evicts enough existing capacity first; if it cannot, the requesting view is not built and evaluation continues cold. Temporary over-budget allocation followed by eviction is forbidden. +At each flush boundary the controller opens a new admission period for every Tier-3 category and records its starting residency. Admission remains work-conserving while the complete Tier-3 pool has space; there are no per-category partitions. + +Owners account exact capacity at arena, slab, vector, bitmap, and table growth boundaries. The growth that crosses the global Tier-3 limit remains usable for the current quota period and closes that category to new retained entries for the rest of the period. Existing entries may continue changing so they never become stale or partial. At the following flush boundary, an over-limit category is dropped whole; there is no mid-traversal eviction, retry, or rebuild. ```text Tier3Limit = min( DeviceCap, - BaseAllowance - + NodeAllowance * ConnectedElementCount - + StylesheetAllowance * CompactStyleProgramBytes) + BaseAllowance + NodeAllowance * ConnectedElementCount) ``` -`CompactStyleProgramBytes` is the byte length of a minimal non-commoned encoding of the attached selectors, match and transpose bytecode, routing registry, declarations, and conditions, counted once for an explicitly shared constructed program. It **excludes** allocator padding, optional indexes, results, and StyleEngine's own capacity, so acceleration overhead can never inflate its own allowance. `ConnectedElementCount` counts connected styleable DOM elements in the live DOM at the read epoch, not pseudo style nodes, arena capacity, or retired generations. +`ConnectedElementCount` counts connected styleable DOM elements in the live DOM at the read epoch, not pseudo style nodes, arena capacity, or retired generations. Required selector and rule-program storage is charged honestly to Tier 2 but does not enlarge the optional Tier-3 pool; process-shared selector programs therefore need no per-document attribution rule in this budget. -| Configuration | Device cap | Base | Per connected style node | Per program byte | -| --- | ---: | ---: | ---: | ---: | -| Browser document | 64 MiB | 1 MiB | 2,048 bytes | 2.00 bytes | +| Configuration | Device cap | Base | Per connected style node | +| --- | ---: | ---: | ---: | +| Browser document | 64 MiB | 1 MiB | 2,048 bytes | -These are a ceiling for the complete set of optional views, not an expectation. Counters report actual capacity against the limit rather than treating the limit as a target. Ignoring the program term, the node coefficient reaches the device cap at 32,256 elements; above that the cap binds and larger documents run progressively colder, which is intended. +These are a ceiling for the complete set of optional views, not an expectation. Counters report actual capacity against the limit rather than treating the limit as a target. The node coefficient reaches the device cap at 32,256 elements; above that the cap binds and larger documents run progressively colder, which is intended. Tier 2 is required program state and is tracked without a cap. The cold-interpreted fallback that a cap would require does not exist; required program capacity must therefore never be refused or relabelled as Tier 3. @@ -920,7 +927,7 @@ Tier4Limit = min( max(4 MiB, Tier3Limit, TransactionNodeAllowance * ConnectedElementCount)) ``` -The scratch cap is 32 MiB and the transaction node allowance is 768 bytes. The multiplier on `Tier3Limit` is 1, not 2, so transaction scratch cannot dominate the document's total style footprint; the 4 MiB floor matters most on small documents. Tier 4 is a **reported ceiling**, not a refusable cap: required scratch charges cannot be refused (the flush must complete), so the limit exists to make an over-limit transaction visible in the pressure report rather than to stop it. The one refusable Tier-4 allocation is the optional preorder topology, whose fallback is simply planning without it. +The scratch cap is 32 MiB and the transaction node allowance is 768 bytes. The multiplier on `Tier3Limit` is 1, not 2, so transaction scratch cannot dominate the document's total style footprint; the 4 MiB floor matters most on small documents. Tier 4 is a **reported ceiling**, not a refusable cap: scratch charges cannot be refused because the flush must complete. The limit makes an over-limit transaction visible in the pressure report but changes no allocation or planning decision. During an active capture (and its replay), the memory policy pins the Tier-3 limit to the device cap so recorded eviction decisions are reproducible; ordinary builds never run that policy. @@ -930,9 +937,7 @@ Byte accounting happens at arena, slab, vector-capacity, bitmap, and hash-table Eviction removes physical acceleration state and bumps no semantic versions. A later observer reconstructs from authoritative inputs and remaining logical state. -The policy is benefit-driven with scan resistance built in. Each Tier-3 category accumulates hit/miss observations (with exponential decay); an eviction happens only when a refused requester has itself demonstrated nonzero benefit, and the victim is chosen among categories with **zero** observed benefit, in a fixed preference order (retained witnesses, feature postings, specified values, winner groups, selector incidences, retained answers, prefix caches). If evicting every zero-benefit category still cannot make room, nothing is evicted and the requester stays refused: an all-or-nothing rule that keeps one broad cold pass from flushing a working set that pays. - -One deliberate exception outranks benefit: the prefix-state cache may evict retained answers and winner groups to stay resident, because the maintenance structure outranks its derivatives. +The policy is benefit-driven with scan resistance built in. Each Tier-3 category accumulates hit/miss observations with exponential decay. At a pressured boundary, categories with no recorded hits are considered first, then the remaining categories by increasing hit ratio, with memory-category order breaking ties. It selects nothing unless the complete selection can cover the actual overage: an all-or-nothing rule that keeps one broad cold pass from flushing a working set that pays. The budget responds to document and program size. Tier-3 state remains fully evictable while compact live styles are retained. @@ -944,7 +949,7 @@ Intern pools use compact document-local handles and weak reclamation where possi ### 10.5 Per-node metadata budget -Mandatory per-node state is at most **six 32-bit words**: +Mandatory per-node state is at most **eight 32-bit words**: ```text StyleNodeID or its DOM mapping (an existing DOM identity can satisfy this without duplication) @@ -953,58 +958,68 @@ parent required relation column first element child required relation column next element sibling required relation column previous element sibling required relation column +depth required relation column +tree scope conditional relation column ``` ```text MandatoryNodeBytes(surface) = BaseMandatoryNodeBytes + ConditionalRelationBytes(surface) -BaseMandatoryNodeBytes <= 24 -ConditionalRelationBytes(surface) <= 8 +BaseMandatoryNodeBytes <= 28 +ConditionalRelationBytes(surface) <= 4 MandatoryNodeBytes(surface) <= 32 ``` -The engine asserts the relation-column budget in its own tests. The conditional allowance covers tree-scope identity, allocated only when the document requires it and no authoritative field can be exposed safely without duplication. Slot, part, pseudo, or future relation navigation must derive a compact identity or replace the physical representation; it cannot raise the 32-byte cap. (`StyleNodeID` is a `NonZeroU32`, so optional relation slots niche-pack into one word.) +The engine asserts the relation-column budget in its own tests. The conditional allowance covers tree-scope identity, allocated only when the document requires it and no authoritative field can be exposed safely without duplication. Depth rejects impossible ancestry checks immediately and bounds the remaining parent walk. Slot, part, pseudo, or future relation navigation must derive a compact identity or replace the physical representation; it cannot raise the 32-byte cap. (`StyleNodeID` is a `NonZeroU32`, so optional relation slots niche-pack into one word.) -Optional context, winner, dependency, and witness handles live in sparse Tier-3 columns and do not consume a reserved word on every node. Shared live style payloads are reported separately. +Optional context, winner, dependency, and witness handles use dense, bit-packed, or segmented Tier-3 columns according to their access and clearing patterns; they do not consume a reserved mandatory word on every node. Dense optional columns and shared live style payloads are reported separately rather than hidden in the 32-byte relation surface. **Tier 1 is required state, not free memory.** No duplicate complete computed styles. ### 10.6 Failure behavior -Memory pressure never degrades correctness; it degrades retention. The actual chain on a refused Tier-3 reservation: +Memory pressure never degrades correctness; it degrades retention. The Tier-3 chain is: ```text -refuse the reservation (never charge first) - -> evict a zero-benefit Tier-3 category and retry the reservation - -> stop requesting retainable answers for the rest of the batch - (the traversal continues with exact evaluation, uncached) +account exact coarse growth and keep it usable for this quota period + -> close that category to new retained entries + -> continue exact evaluation for entries that remain uncached + -> drop the complete over-limit category at the next flush boundary ``` Independently, a transaction the journal had to coarsen widens its impact region to the document and is evaluated exactly there. Whole-document exact evaluation runs as one pass; its scratch is charged to Tier 4, which is a reported ceiling rather than a refusable cap (§10.2). -A memory refusal can therefore make a flush slower and colder, but never rejects a synchronous style read as a Web-visible outcome. +Memory pressure can therefore make a later flush slower and colder, but never rejects a synchronous style read as a Web-visible outcome. ## 11. Scheduling Execution is sequential. Style transactions are processed as dependency-ordered stages over homogeneous delta queues, not recursive pointer-chasing calls. The flush (`take_style_transaction`, flush.rs) runs: ```text -1. Normalize the journal into one transaction. -2. Commit staged tree and local-feature deltas into the fact store - (before routing, or after planning when the plan needs the before side). -3. Decide reuse: does the transaction preserve selector incidence, reach no +1. Reclaim unreachable computed payloads when due, finish the previous + Tier-3 quota period, evict selected complete categories, and open the next + period. +2. Finalize staged sheet-rule replacements, compact unshared routing + directories, and normalize the journal into one transaction. +3. Commit staged program, tree, and fact state to the final snapshot while + retaining the fact pre-images that the transaction needs. +4. Decide reuse: does the transaction preserve selector incidence, reach no selector, or qualify for retained-answer patching or exact-cascade stops? -4. Build impact regions (with a preorder topology above a size threshold). -5. Route program and cascade-topology inputs FIRST (they set the outer +5. Build impact regions (with a preorder topology above a size threshold) + and install the transaction fact view and previous prefix-state epoch. +6. Route program and cascade-topology inputs FIRST (they set the outer envelope), then local-feature/tree/state inputs, stopping early once the region covers the document. -6. Route sibling-sequence and relational (:has()) sequence changes; converge +7. Route sibling-sequence and relational (:has()) sequence changes; converge pending prefix routes. -7. Widen for markers, normalize regions, resolve already-planned truths. -8. Traverse: patch retained answers or complete published match answers; +8. Widen for markers, normalize regions, resolve already-planned truths, and + materialize every before-side fact still needed by retained-answer patching + or confirmation. Release the normalized transaction and its staged + pre-images only after that boundary. +9. Traverse: patch retained answers or complete published match answers; matched-rule production and winner-group updates happen together here, with exact-cascade stop and confirmation checks. -9. Publish: build the reaction records and emit them in one callback. +10. Publish: build the reaction records and emit them in one callback. ``` Computed-value construction is C++-side: the reaction batch drives StyleComputer, which builds computed properties and publishes each element's computed groups back to the engine for interning (§13). Layout, paint, animation, and accessibility consequences are produced by the C++ reaction application. @@ -1055,17 +1070,17 @@ StyleEngine lives in the existing LibWeb Rust crate and is authoritative for sel * selector matching, cascade winner resolution, and reaction planning; * match-answer identities, winner groups, memory accounting, and instrumentation. -Computed-value **construction** is C++-side: the reaction batch drives StyleComputer, which computes properties and builds computed-value groups, then publishes each element's groups back to the engine, where records are interned and become the shared authoritative representation. So the split is: Rust decides *what* must recompute and *which declarations win*; C++ computes *values*; Rust retains and interns the published result. C++ holds no copy of the engine's retained state; a fact has exactly one authoritative home. +Computed-value **construction** is C++-side: the reaction batch drives StyleComputer, which computes properties and builds computed-value groups, then publishes each element's groups back to the engine, where records are interned and become the shared authoritative representation. So the split is: Rust decides *what* must recompute and *which declarations win*; C++ computes *values*; Rust retains and interns the published result. DOM and CSSOM remain the semantic sources for element and program facts; Rust owns the committed evaluation projection and all retained derived state, and C++ must publish every source mutation needed to keep that projection current. **Identity allocation.** The engine mints `StyleNodeID` values (C++ requests them through an allocation call that accepts a batch); C++ owns DOM lifecycle and the DOM-to-style-node mapping. Rust treats IDs as opaque dense indexes. Reuse safety comes from two-phase retirement: retired indexes enter a pending pool and are released for reallocation only at a later safe boundary, never while a stale handle can observe them. -The engine likewise mints document-local `StyleAtomID` values for selector-mentioned tag, ID, class, attribute, value, namespace, and state identities, keyed by the exact interned-string identity C++ passes (a `Utf16FlyString`'s one-word raw identity, kept alive by the bridge for the atom's lifetime, so identity equality is exact and never hash-approximate). Interning on the engine side is what makes selector-name atoms and fact atoms comparable: one table assigns one sequence. Rust stores only the `u32` atom in compact bytecode and postings; a small number of text classes the engine must inspect byte-wise (attribute-value text for substring operators, language tags) are pushed to engine-side storage on demand and re-pushed if evicted. +The engine likewise mints process-global `StyleAtomID` values for selector-mentioned tag, ID, class, attribute, value, namespace, and state identities, keyed by the exact interned-string identity C++ passes (a `Utf16FlyString`'s one-word raw identity, retained until the last document reference releases it, so identity equality is exact and never hash-approximate). Interning on the engine side is what makes selector-name atoms and fact atoms comparable across shared programs: one process table assigns one sequence, while each document records and releases the atoms it uses. Rust stores only the `u32` atom in selector IR and postings; text the engine must inspect byte-wise (attribute values for broad string operators and language tags) is pushed to engine-side storage on demand and re-pushed if evicted. **Relation columns** are stored on the Rust side, keyed by `StyleNodeID`, and maintained from the journalled tree deltas within the same transaction that reports the mutation, so transpose traversal, impact-region membership, and sibling-sequence scans run entirely inside the evaluator. Two deliberate exceptions bypass per-node journalling: initial bulk load links the whole arriving tree directly (safe because the root arrival forces whole-document evaluation), and shadow host/root registration is applied directly at registration time. -**Forward transfer.** Element-fact input crosses as one flat immutable transaction per flush: five pointer-free fixed-width row arrays (tree relations, local features, state, element declarations, element style inputs) submitted in a single call. Program, sheet, rule, layer, and topology changes cross as individual generated boundary calls as they happen and are journalled engine-side into the same normalization journal. A small set of per-element scalar facts (namespace, language, directionality, heading level, custom states, slot-ness) also cross as individual calls at mutation time; the design intent remains that per-element chatter is the exception, not the shape of the boundary. +**Forward transfer.** C++ mutation helpers append element facts to retained input buffers rather than crossing FFI immediately. One flat immutable transaction per flush submits six pointer-free fixed-width row arrays (tree relations, element arrivals, local features, state, element declarations, and element style inputs) plus the arrival custom-state atom arena in a single call. Namespace, language, directionality, heading level, custom states, and slot-ness ride the element-arrival rows. Program, sheet, rule, layer, and topology changes still cross as individual generated boundary calls as they happen and are journalled engine-side into the same normalization journal. -**There is no reverse fact-fetch protocol.** The engine never asks C++ for a fact it lacks: C++ pushes every fact it owns eagerly as deltas, the engine stages them, and evaluation reads old/new values through a transaction-local fact view over the pending transaction plus the resident fact store. When a broad plan needs a complete fact table, the engine clones its own resident store (counted, charged to scratch): a Rust-to-Rust copy, no boundary round trip. The one reverse direction that exists is an enumeration callback letting C++ walk flat-tree descendants the engine knows about. +**There is no reverse fact-fetch protocol.** The engine never asks C++ for a fact it lacks: C++ pushes every fact it owns eagerly as deltas, the engine stages them, and evaluation reads old/new values through a transaction-local fact view over retained pre-images plus the resident fact store. A broad plan holds an immutable shared view of the resident primary columns and allocates only the transaction-specific overlays and indexes it needs; it does not clone the complete fact store or cross the boundary. The one reverse direction that exists is an enumeration callback for flat-tree descendants not represented by an engine-owned child list. **Computed style crosses as a handle.** The boundary transfers a shared style-group handle, never a copied per-property object. C++ consumers hold `StyleRecordID` and read through the record view; layout and paint consume the same handle rather than materializing a private complete style per element (anonymous layout-derived boxes are the exception and own their values). Records are pinned while any C++ consumer references them and reclaimed per the Tier-1 rules in §10.1. @@ -1081,7 +1096,7 @@ inputs.rs applying staged tree/fact input program.rs stylesheet program, identities, order tokens program_updates.rs ProgramDelta application compiler.rs selector compilation into the engine's programs -selector.rs, selector/ logical IR, match + transpose bytecode +selector.rs, selector/ logical IR, match evaluator + transpose routes relative_selector.rs RelativeExists queries and inverse anchor enumeration input_routing.rs typed input key -> routing keys routing.rs delta routing, impact-region construction @@ -1203,15 +1218,15 @@ The engine's job is to make broad costs correspond to real semantic uncertainty ## 16. Instrumentation -The engine maintains a large counter ledger (instrumentation.rs; ~160 counters), exposed to C++ and through the internals object, alongside a separate C++-side ledger of style-update and invalidation counters. The main families: +The engine maintains a large counter ledger (169 counters in `instrumentation.rs`), exposed to C++ and through the internals object, alongside a separate C++-side ledger of style-update and invalidation counters. The main families: * Input deltas by kind; journal cancellations, replacements, and coarsened scope markers. * Executed selector primitives in five buckets: local-feature tests, state tests, combinator steps, structural tests, relational tests. * Routing: routed entry points, impact relation steps, region cardinality. * Candidate enumeration: posting entries enumerated, region-membership checks, exact checks, rejections. * Matching and stopping: retained-answer patch stops, cascade compaction counts, cold-batch rows evaluated and missing. -* Tier-3 memory decisions: refusals per category, admission retries, benefit evictions. -* Bytes by tier and category, plus the pressure snapshot (limits, refusals, evictions) exposed over FFI and printed by the replay accounting report. +* Tier-3 memory decisions: admission closures per category and benefit-guided boundary evictions. +* Bytes by tier and category, plus the pressure snapshot (limits, admission closures, evictions) exposed over FFI and printed by the replay accounting report. * Style records and interned states created, reused, reclaimed; reaction and recomputation counts on the C++ ledger (the amplification triage numbers). The doctrine over any future counter: ratios with a zero denominator report the raw numerator and `not applicable`, never an invented zero, and every plan decision made against an estimate should leave behind the observation that would have corrected it. @@ -1246,4 +1261,3 @@ The engine's correctness claim (every path produces results identical to full re * **Record/replay** captures the complete engine input stream of a real browsing session and replays it deterministically; digests over published outputs make any divergence, down to one changed identity, a hard failure. Commands, workflows, and debugging guidance for all of these are in [StyleEngineTesting.md](StyleEngineTesting.md). - diff --git a/Documentation/Style/StyleEngineTesting.md b/Documentation/Style/StyleEngineTesting.md index 62d6b87559810..654f11f3f4718 100644 --- a/Documentation/Style/StyleEngineTesting.md +++ b/Documentation/Style/StyleEngineTesting.md @@ -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 diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 218481b5f0d00..93833acfccad2 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -892,7 +892,7 @@ void ComputedValues::borrow_style_record_payloads(ReadonlySpan 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); diff --git a/Libraries/LibWeb/CSS/CustomPropertyData.h b/Libraries/LibWeb/CSS/CustomPropertyData.h index 235ca7e9eec73..7fc118a40b178 100644 --- a/Libraries/LibWeb/CSS/CustomPropertyData.h +++ b/Libraries/LibWeb/CSS/CustomPropertyData.h @@ -50,12 +50,13 @@ class WEB_API CustomPropertyData : public RefCounted { // 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 - [[nodiscard]] ReadonlySpan declared_name_atoms(FlatPtr document_identity, InternName&& intern) const + [[nodiscard]] ReadonlySpan 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); @@ -73,6 +74,7 @@ class WEB_API CustomPropertyData : public RefCounted { } 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(); } @@ -121,6 +123,7 @@ class WEB_API CustomPropertyData : public RefCounted { size_t m_declared_count { 0 }; u64 m_identity { 0 }; mutable FlatPtr m_cached_name_atoms_document_identity { NumericLimits::max() }; + mutable u64 m_cached_name_atoms_generation { 0 }; mutable Vector m_cached_declared_name_atoms; mutable FlatPtr m_cached_inheritable_document_identity { NumericLimits::max() }; mutable size_t m_cached_inheritable_generation { NumericLimits::max() }; diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 0a427b36547f5..d46c1f233a036 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -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(); +} + +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 { @@ -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 }; } @@ -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(*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(*this).m_style_engine.unpin_style_record(style_record_identity); } @@ -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); } } @@ -228,6 +261,14 @@ void StyleComputer::prepare_elements_for_style_computation() } } +void StyleComputer::for_each_style_node(Function callback) const +{ + for (auto element : m_style_nodes) { + if (element) + callback(*element); + } +} + void StyleComputer::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); @@ -2408,7 +2449,9 @@ static Vector 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; } @@ -3625,8 +3668,8 @@ StyleEngine::StyleRecordDelta StyleComputer::publish_computed_style_inputs(DOM:: { auto publication = record_computed_style_inputs(Optional { 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; } } @@ -3725,6 +3768,8 @@ NonnullRefPtr 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()) @@ -3749,8 +3794,8 @@ NonnullRefPtr 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; } } @@ -3833,6 +3878,8 @@ NonnullRefPtr 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), @@ -3883,6 +3930,8 @@ RefPtr 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()) @@ -4743,8 +4792,7 @@ RefPtr StyleComputer::compute_style_impl(DOM::AbstractE auto materialize_style_record_view = [&](StyleEngine::StyleRecordView const& view, StyleRecordID identity) -> OwnPtr { if (!view.present) return {}; - if (view.animation_overlay_identity != 0) - pin_style_record(identity); + pin_style_record(identity); return make(view, *this, identity); }; inheritance_parent_style = materialize_style_record_view(inheritance_parent_style_record, inheritance_parent_style_record_identity); diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index e8082bd91650e..e9a9c00a96e2d 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -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 const& set_longhand_property); static NonnullRefPtr get_non_animated_inherit_value(PropertyID, DOM::AbstractElement); struct AnimatedInheritValue { @@ -58,7 +60,7 @@ class WEB_API StyleComputer final : public GC::Cell { static Optional 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; } @@ -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; @@ -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 @@ -211,6 +194,8 @@ class WEB_API StyleComputer final : public GC::Cell { void for_each_provisional_transition_effect(DOM::AbstractElement const&, Function const&) const; private: + virtual void finalize() override; + virtual void visit_edges(Visitor&) override; [[nodiscard]] StyleEngine::StyleRecordDelta record_computed_style_inputs(Optional, ComputedValues const&, StyleNodeID style_node_id) const; @@ -300,6 +285,7 @@ class WEB_API StyleComputer final : public GC::Cell { }; private: + void clear_style_sharing_cache() const; [[nodiscard]] NonnullRefPtr build_and_share_computed_values(NonnullRefPtr, DOM::AbstractElement, StyleScope const&, StyleSharingCandidate&) const; [[nodiscard]] static Vector, 4> author_context_shadow_roots(DOM::AbstractElement); @@ -352,6 +338,7 @@ class WEB_API StyleComputer final : public GC::Cell { void unregister_style_node(StyleNodeID style_node_id); [[nodiscard]] GC::Ptr element_for_style_node(StyleNodeID style_node_id) const; void prepare_elements_for_style_computation(); + void for_each_style_node(Function) 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 @@ -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> m_style_nodes; TreeScopeID m_next_tree_scope; Vector m_non_author_style_sheets; diff --git a/Libraries/LibWeb/CSS/StyleEngineBridge.cpp b/Libraries/LibWeb/CSS/StyleEngineBridge.cpp index 0a19eecc8639a..3ef2ccb3d3174 100644 --- a/Libraries/LibWeb/CSS/StyleEngineBridge.cpp +++ b/Libraries/LibWeb/CSS/StyleEngineBridge.cpp @@ -15,6 +15,17 @@ namespace Web::CSS { extern "C" void ladybird_animated_properties_ref(void const*); +static void ladybird_utf16_fly_string_ref_raw(size_t raw) +{ + auto string = Utf16FlyString::from_raw(static_cast(raw)); + (void)string.to_raw_leaked(); +} + +static void ladybird_utf16_fly_string_unref_raw(size_t raw) +{ + Utf16FlyString::unref_raw(static_cast(raw)); +} + static_assert(!IsMoveConstructible); static_assert(!IsMoveAssignable); @@ -33,14 +44,15 @@ StyleEngine::StyleEngine(DeviceClass device_class, StyleComputer* style_computer : m_impl(StyleEngineFFI::style_engine_create(device_class)) , m_style_computer(style_computer) { + StyleEngineFFI::style_engine_install_raw_atom_callbacks(ladybird_utf16_fly_string_ref_raw, ladybird_utf16_fly_string_unref_raw); } StyleEngine::~StyleEngine() { if (m_impl) StyleEngineFFI::style_engine_destroy(m_impl); - for (auto raw : m_atoms) - Utf16FlyString::unref_raw(raw); + for (auto const& atom : m_atoms) + Utf16FlyString::unref_raw(atom.key); } void StyleEngine::visit_edges(GC::Cell::Visitor& visitor) @@ -215,11 +227,15 @@ StyleAtomID StyleEngine::intern_atom(Utf16FlyString const& name) // by the same word but each assigning its own sequence would compare unequal for the same name, // which fails to match silently rather than loudly. // First time seen, the leaked reference is kept so the identity cannot be reused while the - // atom is live; a duplicate releases it again. + // atom is live. Duplicates release their new reference and return without crossing the FFI. auto raw = name.to_raw_leaked(); - if (m_atoms.set(raw) != HashSetResult::InsertedNewEntry) + if (auto atom = m_atoms.get(raw); atom.has_value()) { Utf16FlyString::unref_raw(raw); - return StyleAtomID { StyleEngineFFI::style_engine_intern_atom(m_impl, raw) }; + return atom.release_value(); + } + auto atom = StyleAtomID { StyleEngineFFI::style_engine_intern_atom(m_impl, raw) }; + m_atoms.set(raw, atom); + return atom; } StyleAtomID StyleEngine::intern_text_atom(Utf16View text) @@ -227,25 +243,113 @@ StyleAtomID StyleEngine::intern_text_atom(Utf16View text) return intern_atom(Utf16FlyString::from_utf16(text).to_ascii_lowercase()); } +StyleAtomID StyleEngine::intern_language_atom(Utf16View text) +{ + auto atom = intern_text_atom(text); + if (atom == 0 || text.is_empty() || m_published_language_atoms.set(atom) != AK::HashSetResult::InsertedNewEntry) + return atom; + + Vector code_units; + code_units.ensure_capacity(text.length_in_code_units()); + for (size_t i = 0; i < text.length_in_code_units(); ++i) + code_units.unchecked_append(text.code_unit_at(i)); + StyleEngineFFI::style_engine_set_element_language(m_impl, 0, atom.value(), code_units.data(), code_units.size()); + return atom; +} + StyleAtomID StyleEngine::intern_case_sensitive_text_atom(Utf16View text) { return intern_atom(Utf16FlyString::from_utf16(text)); } -StyleAtomID StyleEngine::intern_attribute_value(Utf16View value) +// The name an attribute is published under, and the any-namespace name it shares. +// +// Three selectors ask three different questions of an attribute called `x`. `[ns|x]` reaches only +// the one in that namespace, `[x]` reaches only the one in no namespace - which is what the bare +// local name is - and `[*|x]` reaches whichever of them the element carries. The first two name +// exactly one of an element's attributes, so they are the key: an element can hold `x` in several +// namespaces at once, and each is a fact with its own value. `[*|x]` asks about all of them +// together, so the shared form is published as an identity of the name rather than as a fact of its +// own, and one entry per attribute answers all three. +StyleAtomID StyleEngine::intern_attribute_name(Utf16FlyString const& local_name, Optional const& namespace_uri) +{ + auto local = intern_atom(local_name); + auto namespace_atom = !namespace_uri.has_value() || namespace_uri->is_empty() + ? StyleAtomID {} + : intern_case_sensitive_text_atom(namespace_uri->view()); + auto& names_by_namespace = m_attribute_name_atoms.ensure(local, [] { return HashMap {}; }); + if (auto name = names_by_namespace.get(namespace_atom); name.has_value()) + return name.release_value(); + + auto in_namespace = [&](StyleAtomID name) { + if (namespace_atom == 0) + return name; + return intern_qualified_atom(namespace_atom, name); + }; + auto any_namespace = intern_qualified_atom(StyleEngine::any_namespace, local); + auto name = in_namespace(local); + + StyleAtomID folded_name; + StyleAtomID folded_local; + if (auto folded = local_name.to_ascii_lowercase(); folded != local_name) { + auto folded_atom = intern_atom(folded); + folded_name = in_namespace(folded_atom); + folded_local = intern_qualified_atom(StyleEngine::any_namespace, folded_atom); + } + + note_attribute_name_forms(name, any_namespace, folded_name, folded_local); + names_by_namespace.set(namespace_atom, name); + return name; +} + +StyleAtomID StyleEngine::intern_attribute_value(StyleAtomID name, Utf16View value) +{ + auto atom = intern_case_sensitive_text_atom(value); + if (!attribute_name_requires_value_text(name)) + return atom; + + publish_attribute_value_text(atom, value); + return atom; +} + +void StyleEngine::backfill_attribute_value_text_if_required(StyleAtomID name, Utf16View value) { + if (!attribute_name_requires_value_text(name)) + return; + auto atom = intern_case_sensitive_text_atom(value); + publish_attribute_value_text(atom, value); +} + +void StyleEngine::publish_attribute_value_text(StyleAtomID atom, Utf16View value) +{ // The engine holds one copy of the text per currently used value. Ask whether it survived // reclamation before copying it out of the attribute's representation again. if (StyleEngineFFI::style_engine_has_attribute_value_text(m_impl, atom.value())) - return atom; + return; Vector code_units; code_units.ensure_capacity(value.length_in_code_units()); for (size_t i = 0; i < value.length_in_code_units(); ++i) code_units.unchecked_append(value.code_unit_at(i)); StyleEngineFFI::style_engine_set_attribute_value_text(m_impl, atom.value(), code_units.data(), code_units.size()); - return atom; +} + +bool StyleEngine::refresh_attribute_value_text_requirements() +{ + auto version = StyleEngineFFI::style_engine_attribute_value_text_requirements_version(m_impl); + if (version == m_attribute_value_text_requirements_version) + return false; + m_attribute_value_text_requirements_version = version; + m_attribute_names_requiring_value_text.clear(); + return true; +} + +bool StyleEngine::attribute_name_requires_value_text(StyleAtomID name) +{ + return m_attribute_names_requiring_value_text.ensure(name, [&] { + return StyleEngineFFI::style_engine_attribute_name_requires_value_text(m_impl, name.value()); + }); } void StyleEngine::set_element_language(StyleNodeID node, StyleAtomID language, Utf16View tag) @@ -253,9 +357,11 @@ void StyleEngine::set_element_language(StyleNodeID node, StyleAtomID language, U // A language range is not a name, so `:lang()` compares against the tag itself rather than // against the atom. The text is recorded once per language, not once per element. Vector code_units; - code_units.ensure_capacity(tag.length_in_code_units()); - for (size_t i = 0; i < tag.length_in_code_units(); ++i) - code_units.unchecked_append(tag.code_unit_at(i)); + if (language != 0 && !tag.is_empty() && m_published_language_atoms.set(language) == AK::HashSetResult::InsertedNewEntry) { + code_units.ensure_capacity(tag.length_in_code_units()); + for (size_t i = 0; i < tag.length_in_code_units(); ++i) + code_units.unchecked_append(tag.code_unit_at(i)); + } StyleEngineFFI::style_engine_set_element_language(m_impl, node.value(), language.value(), code_units.data(), code_units.size()); } @@ -277,6 +383,19 @@ void StyleEngine::record_tree_delta(StyleEngineFFI::FfiTreeDelta const& delta) m_tree_deltas.append(delta); } +void StyleEngine::record_element_arrival(StyleEngineFFI::FfiElementArrival arrival, ReadonlySpan custom_states) +{ + request_frame_for_first_recorded_input(*this, m_style_computer); + VERIFY(m_arrival_custom_state_atoms.size() <= NumericLimits::max()); + VERIFY(custom_states.size() <= NumericLimits::max()); + VERIFY(m_arrival_custom_state_atoms.size() + custom_states.size() <= NumericLimits::max()); + arrival.custom_state_offset = static_cast(m_arrival_custom_state_atoms.size()); + arrival.custom_state_count = static_cast(custom_states.size()); + for (auto state : custom_states) + m_arrival_custom_state_atoms.append(state.value()); + m_element_arrivals.append(arrival); +} + void StyleEngine::record_local_feature_delta(StyleEngineFFI::FfiLocalFeatureDelta const& delta) { request_frame_for_first_recorded_input(*this, m_style_computer); @@ -295,11 +414,24 @@ void StyleEngine::record_element_declaration_delta(StyleEngineFFI::FfiElementDec m_element_declaration_deltas.append(delta); } +void StyleEngine::append_or_merge_element_style_input(StyleNodeID style_node, u8 reaction, u8 inherited_style_groups) +{ + if (auto existing = m_element_style_input_indices.find(style_node); existing != m_element_style_input_indices.end()) { + auto& input = m_element_style_inputs[existing->value]; + input.reaction |= reaction; + input.inherited_style_groups |= inherited_style_groups; + return; + } + + m_element_style_input_indices.set(style_node, m_element_style_inputs.size()); + m_element_style_inputs.append({ style_node.value(), reaction, inherited_style_groups }); +} + void StyleEngine::record_element_style_input_change(StyleNodeID style_node, u8 reaction, u8 inherited_style_groups) { if (style_node != 0 && reaction != 0) { request_frame_for_first_recorded_input(*this, m_style_computer); - m_element_style_inputs.append({ style_node.value(), reaction, inherited_style_groups }); + append_or_merge_element_style_input(style_node, reaction, inherited_style_groups); } } @@ -315,24 +447,29 @@ void StyleEngine::record_flat_tree_descendant_style_input_changes(StyleNodeID st request_frame_for_first_recorded_input(*this, m_style_computer); auto descendants = StyleEngineFFI::style_engine_flat_tree_descendants(m_impl, style_node.value()); for (auto descendant : ReadonlySpan { descendants.nodes, descendants.count }) - m_element_style_inputs.append({ descendant, reaction, inherited_style_groups }); + append_or_merge_element_style_input(StyleNodeID { descendant }, reaction, inherited_style_groups); StyleEngineFFI::style_engine_discard_flat_tree_descendants(m_impl); } void StyleEngine::consume_recorded_element_style_input_change(StyleNodeID style_node) { - m_element_style_inputs.remove_all_matching([&](auto const& input) { - return input.style_node == style_node.value(); - }); + auto existing = m_element_style_input_indices.find(style_node); + if (existing == m_element_style_input_indices.end()) + return; + + auto index = existing->value; + VERIFY(index < m_element_style_inputs.size()); + m_element_style_input_indices.remove(style_node); + auto last_input = m_element_style_inputs.take_last(); + if (index < m_element_style_inputs.size()) { + m_element_style_inputs[index] = last_input; + m_element_style_input_indices.set(StyleNodeID { last_input.style_node }, index); + } } bool StyleEngine::has_recorded_element_style_input_change(StyleNodeID style_node) const { - for (auto const& input : m_element_style_inputs) { - if (input.style_node == style_node.value()) - return true; - } - return false; + return m_element_style_input_indices.contains(style_node); } void StyleEngine::record_benchmark_marker(Utf16View name) { @@ -345,6 +482,7 @@ void StyleEngine::record_benchmark_marker(Utf16View name) bool StyleEngine::has_recorded_input() const { return !m_tree_deltas.is_empty() + || !m_element_arrivals.is_empty() || !m_local_feature_deltas.is_empty() || !m_state_deltas.is_empty() || !m_element_declaration_deltas.is_empty() @@ -355,12 +493,19 @@ void StyleEngine::submit_recorded_input() { if (m_style_computer) publish_pending_element_features(*this, *m_style_computer); - if (!has_recorded_input()) + if (!has_recorded_input()) { + if (refresh_attribute_value_text_requirements() && m_style_computer) + publish_required_attribute_value_texts(*this, *m_style_computer); return; + } InputTransaction transaction { .tree_deltas = m_tree_deltas.data(), .tree_delta_count = m_tree_deltas.size(), + .element_arrivals = m_element_arrivals.data(), + .element_arrival_count = m_element_arrivals.size(), + .arrival_custom_state_atoms = m_arrival_custom_state_atoms.data(), + .arrival_custom_state_atom_count = m_arrival_custom_state_atoms.size(), .local_feature_deltas = m_local_feature_deltas.data(), .local_feature_delta_count = m_local_feature_deltas.size(), .state_deltas = m_state_deltas.data(), @@ -373,10 +518,18 @@ void StyleEngine::submit_recorded_input() apply_transaction(transaction); m_tree_deltas.clear_with_capacity(); + m_element_arrivals.clear_with_capacity(); + m_arrival_custom_state_atoms.clear_with_capacity(); m_local_feature_deltas.clear_with_capacity(); m_state_deltas.clear_with_capacity(); m_element_declaration_deltas.clear_with_capacity(); m_element_style_inputs.clear_with_capacity(); + m_element_style_input_indices.clear_with_capacity(); + + // Selector demand can arrive while the program change and element facts are still staged. + // Refresh after applying the fact batch, then backfill values before matching observes it. + if (refresh_attribute_value_text_requirements() && m_style_computer) + publish_required_attribute_value_texts(*this, *m_style_computer); } void StyleEngine::apply_transaction(InputTransaction const& transaction) @@ -407,6 +560,31 @@ StyleEngine::PublishedStyleTransaction StyleEngine::take_style_transaction(Style { submit_recorded_input(); auto view = StyleEngineFFI::style_engine_take_style_transaction(m_impl, root.value()); + if (view.reclaimed_style_atom_count != 0) { + HashTable reclaimed_atoms; + reclaimed_atoms.ensure_capacity(view.reclaimed_style_atom_count); + for (auto const& reclaimed : ReadonlySpan { view.reclaimed_style_atoms, view.reclaimed_style_atom_count }) { + auto atom_id = StyleAtomID { reclaimed.atom }; + reclaimed_atoms.set(atom_id); + m_published_language_atoms.remove(atom_id); + m_attribute_names_requiring_value_text.remove(atom_id); + if (reclaimed.raw == 0) + continue; + auto atom = m_atoms.take(reclaimed.raw); + VERIFY(atom.has_value()); + VERIFY(atom.release_value() == reclaimed.atom); + Utf16FlyString::unref_raw(reclaimed.raw); + } + m_attribute_name_atoms.remove_all_matching([&](StyleAtomID local, auto& names_by_namespace) { + if (reclaimed_atoms.contains(local)) + return true; + names_by_namespace.remove_all_matching([&](StyleAtomID namespace_atom, StyleAtomID name) { + return reclaimed_atoms.contains(namespace_atom) || reclaimed_atoms.contains(name); + }); + return names_by_namespace.is_empty(); + }); + ++m_atom_generation; + } return { .version = { view.transaction_version, view.program_version }, .reactions = { view.answers, view.count }, @@ -459,7 +637,10 @@ bool StyleEngine::consume_published_match_answer(StyleNodeID node, Vector selectors) { - return StyleEngineFFI::style_engine_compile_selector_query(m_impl, selectors.data(), selectors.size()); + auto* query = StyleEngineFFI::style_engine_compile_selector_query(m_impl, selectors.data(), selectors.size()); + if (refresh_attribute_value_text_requirements() && m_style_computer) + publish_required_attribute_value_texts(*this, *m_style_computer); + return query; } void StyleEngine::destroy_selector_query(void* query) diff --git a/Libraries/LibWeb/CSS/StyleEngineBridge.h b/Libraries/LibWeb/CSS/StyleEngineBridge.h index f6afe79a044eb..770ceac5db298 100644 --- a/Libraries/LibWeb/CSS/StyleEngineBridge.h +++ b/Libraries/LibWeb/CSS/StyleEngineBridge.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -63,6 +64,9 @@ class WEB_API StyleEngine { [[nodiscard]] bool has_deferred_element_initial_features(StyleNodeID style_node) const { return m_nodes_with_pending_initial_features.contains(style_node); } Vector take_deferred_element_initial_features(); HashTable take_elements_awaiting_first_style_computation(); + void mark_style_node_preallocated(StyleNodeID style_node, TreeScopeID tree_scope) { m_preallocated_style_nodes.set(style_node, tree_scope); } + Optional consume_preallocated_style_node(StyleNodeID style_node) { return m_preallocated_style_nodes.take(style_node); } + void cancel_preallocated_style_node(StyleNodeID style_node) { m_preallocated_style_nodes.remove(style_node); } [[nodiscard]] bool resize_parsed_substitution_cache(u64 bytes); void set_element_parts(StyleNodeID node, ReadonlySpan names, ReadonlySpan hosts); @@ -131,12 +135,14 @@ class WEB_API StyleEngine { // identity on both sides would cancel in the journal and invalidate nothing. [[nodiscard]] u32 next_declaration_block_version() { return ++m_declaration_block_version; } - // Interns one selector-mentioned name and returns its document-local atom. + // Interns one selector-mentioned name and returns its process-global atom, retained by this + // document. // // Utf16FlyString is already interned, so its one-word raw form is the identity: this is a hash // lookup on that word plus one reference to keep the name alive. No string is copied, and // neither side pays an ASCII or UTF-16 conversion for a fact a u32 comparison answers. StyleAtomID intern_atom(Utf16FlyString const&); + [[nodiscard]] u64 atom_generation() const { return m_atom_generation; } // The namespace `[*|x]` names, which is any of them. No interned namespace is zero, so this // keys a form of its own in the same table. static constexpr StyleAtomID any_namespace { 0 }; @@ -144,16 +150,26 @@ class WEB_API StyleEngine { // A name both a selector and the DOM produce as text, with no interned identity on either side: // a language subtag and a `:dir()` keyword. Matched ASCII case-insensitively. StyleAtomID intern_text_atom(Utf16View); + StyleAtomID intern_language_atom(Utf16View); // The same, without the ASCII folding, for names compared literally such as namespace URIs. StyleAtomID intern_case_sensitive_text_atom(Utf16View); - // Interns an attribute value and records what it spells, so a value operator can test it - // without a DOM to ask. Values repeat heavily, so the text crosses once per distinct value. - StyleAtomID intern_attribute_value(Utf16View); + // Interns the exact identity an attribute fact uses and memoizes its namespace and folded + // forms. Demand expansion revisits every live attribute, so these forms must not cross the + // boundary again merely to recover an already published name. + StyleAtomID intern_attribute_name(Utf16FlyString const& local_name, Optional const& namespace_uri); + + // Interns an attribute value and records what it spells when a selector for this name needs + // text. Values repeat heavily, so demanded text crosses once per distinct value. + StyleAtomID intern_attribute_value(StyleAtomID name, Utf16View value); + // Demand expansion already has every value identity. Check the name before interning the text + // so attributes no selector reads do not pay another string hash. + void backfill_attribute_value_text_if_required(StyleAtomID name, Utf16View value); // Deltas accumulate here and cross in one flat batch per style flush, never one call per // element. void record_tree_delta(StyleEngineFFI::FfiTreeDelta const&); + void record_element_arrival(StyleEngineFFI::FfiElementArrival, ReadonlySpan custom_states); void record_local_feature_delta(StyleEngineFFI::FfiLocalFeatureDelta const&); void record_state_delta(StyleEngineFFI::FfiStateDelta const&); void record_element_declaration_delta(StyleEngineFFI::FfiElementDeclarationDelta const&); @@ -224,19 +240,32 @@ class WEB_API StyleEngine { using InputTransaction = StyleEngineFFI::FfiStyleInputTransaction; bool read_matches(StyleNodeID, Vector&, Optional); + void append_or_merge_element_style_input(StyleNodeID, u8 reaction, u8 inherited_style_groups); void apply_transaction(InputTransaction const&); void submit_recorded_input(); + bool refresh_attribute_value_text_requirements(); + [[nodiscard]] bool attribute_name_requires_value_text(StyleAtomID); + void publish_attribute_value_text(StyleAtomID, Utf16View); void* m_impl { nullptr }; GC::Ptr m_style_computer; - HashTable m_atoms; + HashMap m_atoms; + HashTable m_published_language_atoms; + HashMap> m_attribute_name_atoms; + HashMap m_attribute_names_requiring_value_text; + u64 m_atom_generation { 1 }; + u64 m_attribute_value_text_requirements_version { 0 }; HashTable m_nodes_with_pending_initial_features; HashTable m_nodes_awaiting_first_style_computation; + HashMap m_preallocated_style_nodes; + HashMap m_element_style_input_indices; size_t m_element_match_capacity { 64 }; u32 m_declaration_block_version { 1 }; Vector m_tree_deltas; + Vector m_element_arrivals; + Vector m_arrival_custom_state_atoms; Vector m_local_feature_deltas; Vector m_state_deltas; Vector m_element_declaration_deltas; diff --git a/Libraries/LibWeb/CSS/StyleEngineInput.cpp b/Libraries/LibWeb/CSS/StyleEngineInput.cpp index 64cefceed2ecf..102b09f516866 100644 --- a/Libraries/LibWeb/CSS/StyleEngineInput.cpp +++ b/Libraries/LibWeb/CSS/StyleEngineInput.cpp @@ -4,7 +4,6 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include #include #include #include @@ -53,12 +52,6 @@ static constexpr StyleNodeID no_style_node; // Shadow trees get their own scopes with the shadow surface; today everything names the document. static constexpr TreeScopeID document_tree_scope; -static HashTable& preallocated_style_elements() -{ - static NeverDestroyed> elements; - return *elements; -} - static bool has_pending_initial_features(DOM::Element const& element) { return element.document().style_computer().style_engine().has_deferred_element_initial_features(element.style_node_id()); @@ -128,6 +121,21 @@ static TreeScopeID tree_scope_of(DOM::Node& document_or_shadow_root) return shadow_root->style_engine_tree_scope(); } +template +static void for_each_shadow_including_inclusive_descendant_with_scope(DOM::Node& node, TreeScopeID tree_scope, Callback& callback) +{ + callback(node, tree_scope); + + if (auto* element = as_if(node); element && element->shadow_root()) { + auto& shadow_root = *element->shadow_root(); + auto shadow_scope = tree_scope_of(shadow_root); + for_each_shadow_including_inclusive_descendant_with_scope(shadow_root, shadow_scope, callback); + } + + for (auto* child = node.first_child(); child; child = child->next_sibling()) + for_each_shadow_including_inclusive_descendant_with_scope(*child, tree_scope, callback); +} + TreeScopeID style_engine_tree_scope_for(DOM::Node& document_or_shadow_root) { return tree_scope_of(document_or_shadow_root); @@ -144,7 +152,7 @@ static StyleNodeID style_tree_parent_of(DOM::Element& element, StyleEngine& styl return no_style_node; } -static StyleEngineFFI::FfiTreeRelations relations_of(DOM::Element& element, StyleEngine& style_engine) +static StyleEngineFFI::FfiTreeRelations relations_of(DOM::Element& element, StyleEngine& style_engine, TreeScopeID tree_scope) { auto assigned_slot = no_style_node; if (auto slot = element.assigned_slot_internal()) @@ -154,12 +162,17 @@ static StyleEngineFFI::FfiTreeRelations relations_of(DOM::Element& element, Styl .parent = style_tree_parent_of(element, style_engine).value(), .previous_element_sibling = identity_of(element.previous_element_sibling()).value(), .next_element_sibling = identity_of(element.next_element_sibling()).value(), - .tree_scope = tree_scope_of(element.root()).value(), + .tree_scope = tree_scope.value(), .assigned_slot = assigned_slot.value(), .reserved = 0, }; } +static StyleEngineFFI::FfiTreeRelations relations_of(DOM::Element& element, StyleEngine& style_engine) +{ + return relations_of(element, style_engine, tree_scope_of(element.root())); +} + static void record_feature( DOM::Element&, StyleEngineFFI::FfiFeatureKind, @@ -181,54 +194,22 @@ static StyleEngineFFI::FfiTreeRelations detached_relations() }; } -// The name an attribute is published under, and the any-namespace name it shares. -// -// Three selectors ask three different questions of an attribute called `x`. `[ns|x]` reaches only -// the one in that namespace, `[x]` reaches only the one in no namespace - which is what the bare -// local name is - and `[*|x]` reaches whichever of them the element carries. The first two name -// exactly one of an element's attributes, so they are the key: an element can hold `x` in several -// namespaces at once, and each is a fact with its own value. `[*|x]` asks about all of them -// together, so the shared form is published as an identity of the name rather than as a fact of its -// own, and one entry per attribute answers all three. -static StyleAtomID attribute_name_atom(StyleEngine& style_engine, Utf16FlyString const& local_name, Optional const& namespace_uri) -{ - auto local = style_engine.intern_atom(local_name); - auto any_namespace = style_engine.intern_qualified_atom(StyleEngine::any_namespace, local); - auto in_namespace = [&](StyleAtomID name) { - if (!namespace_uri.has_value() || namespace_uri->is_empty()) - return name; - return style_engine.intern_qualified_atom(style_engine.intern_case_sensitive_text_atom(namespace_uri->view()), name); - }; - auto name = in_namespace(local); - - // An attribute name is matched ASCII case-insensitively against an HTML element in an HTML - // document, and a selector dispatches on the folded form, so an attribute whose own name is not - // already lowercase has to answer to that form as well. Only a non-HTML element can hold one: - // both the parser and `setAttribute` fold the name for an HTML element in an HTML document. - StyleAtomID folded_name; - StyleAtomID folded_local; - if (auto folded = local_name.to_ascii_lowercase(); folded != local_name) { - auto folded_atom = style_engine.intern_atom(folded); - folded_name = in_namespace(folded_atom); - folded_local = style_engine.intern_qualified_atom(StyleEngine::any_namespace, folded_atom); - } - - style_engine.note_attribute_name_forms(name, any_namespace, folded_name, folded_local); - return name; -} - void record_element_connected(DOM::Element& element) { auto* style_engine = style_engine_for(element); if (!style_engine) return; + Optional preallocated_tree_scope; if (element.style_node_id() == no_style_node) { element.set_style_node_id(style_engine->allocate_style_node()); element.document().style_computer().register_style_node(element.style_node_id(), element); - } else if (!preallocated_style_elements().remove(&element)) { - // Already connected as far as the engine is concerned. Re-recording an insertion would - // double-link the element into its sibling sequence. - return; + } else { + preallocated_tree_scope = style_engine->consume_preallocated_style_node(element.style_node_id()); + if (!preallocated_tree_scope.has_value()) { + // Already connected as far as the engine is concerned. Re-recording an insertion would + // double-link the element into its sibling sequence. + return; + } } // A shadow root that took its identity before its host had one is still waiting to be linked to // it. A sheet adopted into a shadow tree names that root, so the root can be identified first, @@ -241,7 +222,9 @@ void record_element_connected(DOM::Element& element) .old_connected = false, .new_connected = true, .old_relations = detached_relations(), - .new_relations = relations_of(element, *style_engine), + .new_relations = preallocated_tree_scope.has_value() + ? relations_of(element, *style_engine, *preallocated_tree_scope) + : relations_of(element, *style_engine), }); style_engine->defer_element_initial_features(element.style_node_id()); @@ -272,14 +255,20 @@ void prepare_style_nodes_for_subtree(DOM::Node& root) auto& style_computer = root.document().style_computer(); auto& style_engine = style_computer.style_engine(); Vector> elements; + Vector element_tree_scopes; Vector> shadow_roots; - root.for_each_shadow_including_inclusive_descendant([&](DOM::Node& node) { - if (auto* element = as_if(node); element && element->style_node_id() == no_style_node) + Vector shadow_root_tree_scopes; + auto collect = [&](DOM::Node& node, TreeScopeID tree_scope) { + if (auto* element = as_if(node); element && element->style_node_id() == no_style_node) { elements.append(element); - else if (auto* shadow_root = as_if(node); shadow_root && shadow_root->style_node_id() == no_style_node) + element_tree_scopes.append(tree_scope); + } else if (auto* shadow_root = as_if(node); shadow_root && shadow_root->style_node_id() == no_style_node) { shadow_roots.append(shadow_root); - return TraversalDecision::Continue; - }); + shadow_root_tree_scopes.append(tree_scope); + } + }; + auto root_tree_scope = tree_scope_of(root.root()); + for_each_shadow_including_inclusive_descendant_with_scope(root, root_tree_scope, collect); Vector identities; identities.resize(elements.size() + shadow_roots.size()); style_engine.allocate_style_nodes(identities.span()); @@ -287,13 +276,13 @@ void prepare_style_nodes_for_subtree(DOM::Node& root) auto& element = *elements[index]; element.set_style_node_id(identities[index]); style_computer.register_style_node(identities[index], element); - preallocated_style_elements().set(&element); + style_engine.mark_style_node_preallocated(element.style_node_id(), element_tree_scopes[index]); } for (size_t index = 0; index < shadow_roots.size(); ++index) { auto& shadow_root = *shadow_roots[index]; auto identity = identities[elements.size() + index]; shadow_root.set_style_node_id(identity); - style_engine.set_tree_scope_root(tree_scope_of(shadow_root), identity); + style_engine.set_tree_scope_root(shadow_root_tree_scopes[index], identity); } } @@ -308,10 +297,10 @@ template static void publish_element_selector_features(StyleEngine& style_engine, DOM::Element& element, StyleNodeID node, PublishFeature publish_feature, PublishEmptiness publish_emptiness, InvalidateLanguageCache invalidate_language_cache) { // Slot identity and namespace never change during an element's lifetime. - if (is(element)) - style_engine.set_element_is_slot(node, true); + auto is_slot = is(element); + StyleAtomID namespace_atom; if (auto const& namespace_uri = element.namespace_uri(); namespace_uri.has_value() && !namespace_uri->is_empty()) - style_engine.set_element_namespace(node, style_engine.intern_case_sensitive_text_atom(namespace_uri->view())); + namespace_atom = style_engine.intern_case_sensitive_text_atom(namespace_uri->view()); publish_feature(StyleEngineFFI::FfiFeatureKind::TagName, StyleAtomID {}, StyleEngineFFI::FfiFeatureValueKind::Atom, style_engine.intern_atom(element.local_name())); if (auto folded_name = element.local_name().to_ascii_lowercase(); folded_name != element.local_name()) @@ -321,7 +310,8 @@ static void publish_element_selector_features(StyleEngine& style_engine, DOM::El for (auto const& class_name : element.class_names()) publish_feature(StyleEngineFFI::FfiFeatureKind::Class, intern_id_or_class_atom(style_engine, element, class_name), StyleEngineFFI::FfiFeatureValueKind::Present, StyleAtomID {}); element.for_each_attribute([&](DOM::QualifiedName const& name, Utf16View value) { - publish_feature(StyleEngineFFI::FfiFeatureKind::Attribute, attribute_name_atom(style_engine, name.local_name(), name.namespace_()), StyleEngineFFI::FfiFeatureValueKind::Atom, style_engine.intern_attribute_value(value)); + auto name_atom = style_engine.intern_attribute_name(name.local_name(), name.namespace_()); + publish_feature(StyleEngineFFI::FfiFeatureKind::Attribute, name_atom, StyleEngineFFI::FfiFeatureValueKind::Atom, style_engine.intern_attribute_value(name_atom, value)); }); bool has_nonempty_text_child = false; @@ -342,31 +332,55 @@ static void publish_element_selector_features(StyleEngine& style_engine, DOM::El } auto const language = element.lang_view(); - style_engine.set_element_language(node, language.has_value() ? style_engine.intern_text_atom(*language) : 0, language.value_or({})); + auto language_atom = language.has_value() ? style_engine.intern_language_atom(*language) : StyleAtomID {}; auto const directionality = element.directionality() == DOM::Element::Directionality::Rtl ? "rtl"sv : "ltr"sv; - style_engine.set_element_directionality(node, style_engine.intern_text_atom(Utf16View { directionality })); + auto directionality_atom = style_engine.intern_text_atom(Utf16View { directionality }); if (invalidate_language_cache == InvalidateLanguageCache::Yes) element.invalidate_lang_value(); GC::Ptr heading = as_if(element); - style_engine.set_element_heading_level(node, static_cast(min(heading ? heading->heading_level() : 0, 255u))); + auto heading_level = static_cast(min(heading ? heading->heading_level() : 0, 255u)); Vector custom_states; if (auto states = element.custom_state_set()) { for (auto const& state : states->states()) custom_states.append(style_engine.intern_atom(state)); } - style_engine.set_element_custom_states(node, custom_states); + style_engine.record_element_arrival({ + .node = node.value(), + .namespace_atom = namespace_atom.value(), + .language_atom = language_atom.value(), + .directionality_atom = directionality_atom.value(), + .custom_state_offset = 0, + .custom_state_count = 0, + .heading_level = heading_level, + .is_slot = is_slot, + .reserved = 0, + }, + custom_states); +} + +void publish_required_attribute_value_texts(StyleEngine& style_engine, StyleComputer& style_computer) +{ + style_computer.for_each_style_node([&](DOM::Element& element) { + element.for_each_attribute([&](DOM::QualifiedName const& name, Utf16View value) { + auto name_atom = style_engine.intern_attribute_name(name.local_name(), name.namespace_()); + style_engine.backfill_attribute_value_text_if_required(name_atom, value); + }); + }); } -void populate_isolated_selector_query_engine(StyleEngine& style_engine, DOM::ParentNode& root, Function, StyleNodeID)> const& publish_identity) +void configure_isolated_selector_query_engine(StyleEngine& style_engine, DOM::Document& document) { - style_engine.set_fold_id_and_class_name_case(root.document().in_quirks_mode()); + style_engine.set_fold_id_and_class_name_case(document.in_quirks_mode()); style_engine.set_html_element_namespace( - root.document().document_type() == DOM::Document::Type::HTML + document.document_type() == DOM::Document::Type::HTML ? style_engine.intern_case_sensitive_text_atom(Namespace::HTML.view()) : 0); +} +void populate_isolated_selector_query_engine(StyleEngine& style_engine, DOM::ParentNode& root, Function, StyleNodeID)> const& publish_identity) +{ Optional non_element_root_identity; if (!is(root) && !is(root)) { non_element_root_identity = style_engine.allocate_style_node(); @@ -517,7 +531,8 @@ static void record_element_initial_features(DOM::Element& element) if (!element.part_names().is_empty()) record_element_parts_changed(element); - record_element_inline_style_properties(element); + if (auto const inline_style = element.inline_style(); inline_style && (!inline_style->properties().is_empty() || !inline_style->custom_properties().is_empty())) + record_element_inline_style_properties(element); } void record_element_moved(DOM::Element& element, DOM::Node* old_parent, DOM::Element* old_previous_sibling, DOM::Element* old_next_sibling) @@ -606,7 +621,7 @@ void record_element_assigned_slot_changed(DOM::Element& element, DOM::Element* o }); } -void record_element_disconnecting(DOM::Element& element) +static void record_element_disconnecting(DOM::Element& element, TreeScopeID tree_scope) { auto* style_engine = style_engine_for(element); if (!style_engine) @@ -643,7 +658,7 @@ void record_element_disconnecting(DOM::Element& element) .node = node.value(), .old_connected = true, .new_connected = false, - .old_relations = relations_of(element, *style_engine), + .old_relations = relations_of(element, *style_engine, tree_scope), .new_relations = detached_relations(), }); @@ -682,7 +697,7 @@ void record_element_custom_property_names(DOM::Element& element, CustomPropertyD return; auto atoms = data - ? data->declared_name_atoms(bit_cast(&element.document()), [&](Utf16FlyString const& name) { return style_engine->intern_atom(name); }) + ? data->declared_name_atoms(bit_cast(&element.document()), style_engine->atom_generation(), [&](Utf16FlyString const& name) { return style_engine->intern_atom(name); }) : ReadonlySpan {}; style_engine->set_element_custom_property_names(element.style_node_id(), atoms, uses_unnamed, uses_custom_functions); } @@ -951,12 +966,11 @@ struct DeclaredPropertyColumns { bool declarations_are_complete; }; -static void publish_element_declared_properties(DOM::Element& element, StyleEngineFFI::FfiElementDeclarationKind kind, ReadonlySpan style_properties, bool declarations_are_complete = true) +static bool publish_element_declared_properties(DOM::Element& element, StyleEngineFFI::FfiElementDeclarationKind kind, ReadonlySpan style_properties, bool declarations_are_complete = true) { auto* style_engine = style_engine_for(element); - if (!style_engine || element.style_node_id() == no_style_node || has_pending_initial_features(element)) { - return; - } + if (!style_engine || element.style_node_id() == no_style_node || has_pending_initial_features(element)) + return false; DeclaredPropertyColumns columns(style_properties.size(), declarations_are_complete); for (auto const& property : style_properties) { @@ -966,6 +980,7 @@ static void publish_element_declared_properties(DOM::Element& element, StyleEngi columns.append(property, ExpandShorthands::Yes); } style_engine->set_element_declared_properties(element.style_node_id(), kind, columns.properties, columns.important, columns.operators, columns.values, columns.original_values, columns.declarations_are_complete); + return true; } // An element can arrive with a style attribute already written, so this is published on arrival as @@ -987,9 +1002,9 @@ static void record_element_inline_style_properties(DOM::Element& element) // bordered table for that reason. The cascade builds the block anyway, so this costs the call. // // SVG presentation attributes map through the same hook, so they are published under this kind too. -void record_element_presentational_hint_properties(DOM::Element& element, ReadonlySpan hints) +bool record_element_presentational_hint_properties(DOM::Element& element, ReadonlySpan hints) { - publish_element_declared_properties(element, StyleEngineFFI::FfiElementDeclarationKind::PresentationalHint, hints); + return publish_element_declared_properties(element, StyleEngineFFI::FfiElementDeclarationKind::PresentationalHint, hints); } void record_element_declarations_changed(DOM::Element& element, ElementDeclarationKind kind, bool had_declarations, bool has_declarations) @@ -1051,6 +1066,24 @@ void record_shadow_root_disconnecting(DOM::ShadowRoot& shadow_root) shadow_root.set_style_node_id(no_style_node); } +void record_subtree_disconnecting(DOM::Node& root) +{ + auto root_tree_scope = tree_scope_of(root.root()); + auto disconnect_element = [](DOM::Node& node, TreeScopeID tree_scope) { + if (auto* element = as_if(node)) + record_element_disconnecting(*element, tree_scope); + }; + for_each_shadow_including_inclusive_descendant_with_scope(root, root_tree_scope, disconnect_element); + + // Only once no element still names a shadow root as its parent can the root give up its own + // identity. + auto disconnect_shadow_root = [](DOM::Node& node, TreeScopeID) { + if (auto* shadow_root = as_if(node)) + record_shadow_root_disconnecting(*shadow_root); + }; + for_each_shadow_including_inclusive_descendant_with_scope(root, root_tree_scope, disconnect_shadow_root); +} + void record_shadow_root_connected(DOM::ShadowRoot& shadow_root) { auto* style_engine = style_engine_for(shadow_root); @@ -2205,17 +2238,19 @@ void record_element_attribute_changed(DOM::Element& element, Utf16FlyString cons if (name == HTML::AttributeNames::headingoffset || name == HTML::AttributeNames::headingreset) record_heading_levels_in_subtree(element); - // Both values cross as atoms, with their text recorded once per distinct value. This lets the - // match evaluator reconstruct either side of a transaction without asking the DOM, and two - // different values cannot cancel in the journal merely because both are present. + // Both values cross as atoms. Their text is recorded once per distinct value only when a + // compiled selector for this attribute uses an operator that cannot compare atom identities. + // This lets the match evaluator reconstruct either side of such a transaction without asking + // the DOM, and two different values cannot cancel in the journal merely because both are + // present. // The same name an arriving attribute publishes, with the same other forms noted alongside it. - // See `attribute_name_atom`. - auto atom = attribute_name_atom(*style_engine, name, namespace_uri); + // See `StyleEngine::intern_attribute_name`. + auto atom = style_engine->intern_attribute_name(name, namespace_uri); auto kind_of = [](Optional const& value) { return value.has_value() ? StyleEngineFFI::FfiFeatureValueKind::Atom : StyleEngineFFI::FfiFeatureValueKind::Absent; }; auto atom_of = [&](Optional const& value) { - return value.has_value() ? style_engine->intern_attribute_value(*value) : 0; + return value.has_value() ? style_engine->intern_attribute_value(atom, *value) : 0; }; auto old_kind = kind_of(old_value); auto old_atom = atom_of(old_value); diff --git a/Libraries/LibWeb/CSS/StyleEngineInput.h b/Libraries/LibWeb/CSS/StyleEngineInput.h index f0fe7fd81ed7b..7109d32273ea0 100644 --- a/Libraries/LibWeb/CSS/StyleEngineInput.h +++ b/Libraries/LibWeb/CSS/StyleEngineInput.h @@ -28,10 +28,12 @@ class StyleEngine; WEB_API void record_element_connected(DOM::Element&); WEB_API void prepare_style_nodes_for_subtree(DOM::Node&); WEB_API void publish_pending_element_features(StyleEngine&, StyleComputer&); +WEB_API void publish_required_attribute_value_texts(StyleEngine&, StyleComputer&); // Populate an isolated engine with the current facts of a DOM tree. The callback receives the temporary identity // assigned to each element; no identity or transaction in the document's // resident engine is changed. +WEB_API void configure_isolated_selector_query_engine(StyleEngine&, DOM::Document&); WEB_API void populate_isolated_selector_query_engine(StyleEngine&, DOM::ParentNode&, Function, StyleNodeID)> const&); // Tell the document's engine whether this is an HTML document. Selectors compile against that fact, @@ -39,8 +41,8 @@ WEB_API void populate_isolated_selector_query_engine(StyleEngine&, DOM::ParentNo // before the first sheet attaches, and has to say it itself. WEB_API void record_document_kind(DOM::Document&); -// Called while the node is still linked, so its old relations are still readable. -WEB_API void record_element_disconnecting(DOM::Element&); +// Called while the subtree is still linked, so its old relations are still readable. +WEB_API void record_subtree_disconnecting(DOM::Node&); // Report that an element moved without leaving the tree. `moveBefore()` keeps the element's state // and its identity, so nothing disconnects and nothing connects, and only its relations move. @@ -69,7 +71,7 @@ WEB_API void record_element_custom_states_changed(DOM::Element&); // publishes the value it now resolves to. WEB_API void record_element_language_and_directionality(DOM::Element&); WEB_API void record_element_directionality(DOM::Element&); -WEB_API void record_element_presentational_hint_properties(DOM::Element&, ReadonlySpan); +WEB_API bool record_element_presentational_hint_properties(DOM::Element&, ReadonlySpan); WEB_API void record_element_animation_names(DOM::Element&, ReadonlySpan); WEB_API void record_element_custom_property_names(DOM::Element&, ReadonlySpan, bool uses_unnamed, bool uses_custom_functions); diff --git a/Libraries/LibWeb/CSS/StyleInputRecord.h b/Libraries/LibWeb/CSS/StyleInputRecord.h index fb1f6e203ef07..657e6d4c00678 100644 --- a/Libraries/LibWeb/CSS/StyleInputRecord.h +++ b/Libraries/LibWeb/CSS/StyleInputRecord.h @@ -43,8 +43,9 @@ struct StyleInputRecord { // Such a computation cannot be answered from the record, because what it read can move without // any word of it moving. bool read_beyond_the_record { true }; - // What that computation did besides producing values. A computation that is skipped still has - // to leave these marks, since nothing else will leave them for it. + // A snapshot of the element's authoritative dependency marks. The element clears its marks + // before deriving a replacement, so a computation skipped through this record must restore the + // previous marks that nothing else will leave. bool style_uses_attr_css_function { false }; bool style_uses_var_css_function { false }; bool style_uses_if_css_function { false }; diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index 725d307c7a20e..b7655345883c8 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -1322,6 +1322,27 @@ void Element::apply_presentational_hints(Vector& properties) } } +bool Element::presentational_hint_properties_need_publication(ReadonlySpan properties) const +{ + if (m_published_presentational_hint_properties.size() == properties.size()) { + bool properties_are_unchanged = true; + for (size_t index = 0; index < properties.size(); ++index) + properties_are_unchanged &= m_published_presentational_hint_properties[index] == properties[index]; + if (properties_are_unchanged) + return false; + } + + return true; +} + +void Element::did_publish_presentational_hint_properties(ReadonlySpan properties) +{ + m_published_presentational_hint_properties.clear(); + m_published_presentational_hint_properties.ensure_capacity(properties.size()); + for (auto const& property : properties) + m_published_presentational_hint_properties.unchecked_append(property); +} + void Element::run_attribute_change_steps(Utf16FlyString const& local_name, Optional const& old_value, Optional const& value, Optional const& namespace_) { attribute_changed(local_name, old_value, value, namespace_); @@ -1364,10 +1385,12 @@ static CSS::StyleComputer::ComputedStyleInvalidation compute_required_invalidati // NB: The adoption also makes an unchanged element keep sharing group storage with its // previous style generation, which future diffs turn into pure pointer compares. bool const all_group_payloads_shared = new_computed_values.adopt_identical_group_payloads(old_computed_values); - // The inheritance-dependent specified values live outside the group payloads, and swapping - // one for a concrete value with the same used color changes what descendants inherit, so - // equal payloads alone cannot prove the diff away. + // The computed longhand table, resolved font list and inheritance-dependent specified values + // live outside the group payloads, so equal payloads alone cannot prove the diff away. When + // all longhands are equal, adopting identical group payloads also adopts the previous table. bool const property_diff_can_be_skipped = all_group_payloads_shared + && old_computed_values.computed_longhand_values().data() == new_computed_values.computed_longhand_values().data() + && old_computed_values.font_list().equals(new_computed_values.font_list()) && !CSS::ComputedValues::either_carries_animated_overlay(old_computed_values, new_computed_values) && old_computed_values.inheritance_dependent_specified_values_equal(new_computed_values); static bool const verify_fast_path = getenv("LIBWEB_VERIFY_STYLE_DIFF_FAST_PATH") != nullptr; @@ -1986,14 +2009,17 @@ CSS::RequiredInvalidationAfterStyleChange Element::apply_style_engine_reaction(b // Which animations an element references is an index StyleEngine keeps, in the same shape as the // anchor-name registry above: nothing about selector matching can say it, and without it a // `@keyframes` rule cannot find the elements running the animation it describes. - { + auto indexable_animation_names = [](CSS::ComputedValues const& style) { Vector animation_names; - for (auto const& animation_name : new_style->animation_names()) { + for (auto const& animation_name : style.animation_names()) { if (animation_name.syntax != CSS::ComputedAnimationNameSyntax::None) animation_names.append(animation_name.name); } + return animation_names; + }; + auto animation_names = indexable_animation_names(*new_style); + if (old_computed_values ? indexable_animation_names(*old_computed_values) != animation_names : !animation_names.is_empty()) CSS::record_element_animation_names(*this, animation_names); - } // Which custom properties this element declares or references decides which `@property` // registrations reach it. Declaring one matters because registration changes how it computes; // referencing one matters because registration gives it a value where it had none. @@ -2387,18 +2413,8 @@ void Element::set_shadow_root(GC::Ptr shadow_root) if (m_shadow_root == shadow_root) return; if (m_shadow_root) { - if (is_connected()) { - m_shadow_root->for_each_shadow_including_inclusive_descendant([](DOM::Node& descendant) { - if (auto* element = as_if(descendant)) - CSS::record_element_disconnecting(*element); - return TraversalDecision::Continue; - }); - m_shadow_root->for_each_shadow_including_inclusive_descendant([](DOM::Node& descendant) { - if (auto* shadow_root = as_if(descendant)) - CSS::record_shadow_root_disconnecting(*shadow_root); - return TraversalDecision::Continue; - }); - } + if (is_connected()) + CSS::record_subtree_disconnecting(*m_shadow_root); m_shadow_root->set_host(nullptr); m_shadow_root->set_is_connected(false); // NB: We don't need to run the removed steps if the children have already been disconnected (or were never diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 9ac192acc9dc9..f7c785e59536d 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -314,13 +314,20 @@ class WEB_API Element // The element's StyleEngine identity, or 0 while it has none. Disconnected and never-styled // elements keep 0, which is what makes them free. [[nodiscard]] CSS::StyleNodeID style_node_id() const { return m_style_node_id; } - void set_style_node_id(CSS::StyleNodeID style_node_id) { m_style_node_id = style_node_id; } + void set_style_node_id(CSS::StyleNodeID style_node_id) + { + if (m_style_node_id != style_node_id) + m_published_presentational_hint_properties.clear(); + m_style_node_id = style_node_id; + } // https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes virtual bool supports_dimension_attributes() const { return false; } virtual bool is_presentational_hint(Utf16FlyString const&) const { return false; } virtual void apply_presentational_hints(Vector&) const; + bool presentational_hint_properties_need_publication(ReadonlySpan) const; + void did_publish_presentational_hint_properties(ReadonlySpan); void run_attribute_change_steps(Utf16FlyString const& local_name, Optional const& old_value, Optional const& value, Optional const& namespace_); @@ -875,12 +882,14 @@ class WEB_API Element GC::Ptr m_inline_style; GC::Ptr m_shadow_root; - // The authoritative StyleEngine record. C++ compatibility consumers borrow the record-owned - // computed-values view rather than retaining one complete style per element. + // A consumer handle mirroring StyleEngine's authoritative style-record column. C++ consumers + // borrow the record-owned computed-values view rather than retaining one complete style per + // element. CSS::StyleRecordID m_style_record_identity; RefPtr m_custom_property_data; OwnPtr m_style_input_record; PublishedCustomPropertyNames m_published_custom_property_names; + Vector m_published_presentational_hint_properties; void register_element_reference_pseudo_element(CSS::PseudoElement type, GC::Ref element); SyntheticPseudoElement& ensure_synthetic_pseudo_element(CSS::PseudoElement) const; @@ -897,6 +906,7 @@ class WEB_API Element bool m_is_being_activated : 1 { false }; bool m_in_top_layer : 1 { false }; bool m_rendered_in_top_layer : 1 { false }; + // Authoritative dependency marks left by this element's latest style computation. bool m_style_uses_attr_css_function : 1 { false }; bool m_style_uses_var_css_function : 1 { false }; bool m_style_uses_if_css_function : 1 { false }; diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index 5572e0a5540d6..ec22873884ad4 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -1106,18 +1106,7 @@ void Node::remove(bool suppress_observers) // NB: Recorded here rather than in removed_from(), because StyleEngine's tree delta carries // the old relations and this is the last point at which they are still readable. - for_each_shadow_including_inclusive_descendant([](Node& node) { - if (auto* element = as_if(node)) - CSS::record_element_disconnecting(*element); - return TraversalDecision::Continue; - }); - // Only once no element still names a shadow root as its parent can the root give up its - // own identity. - for_each_shadow_including_inclusive_descendant([](Node& node) { - if (auto* shadow_root = as_if(node)) - CSS::record_shadow_root_disconnecting(*shadow_root); - return TraversalDecision::Continue; - }); + CSS::record_subtree_disconnecting(*this); // A display: contents element has no principal layout node of its own, but removing it also removes // all of its children's boxes from the parent's layout subtree. diff --git a/Libraries/LibWeb/DOM/PseudoElement.cpp b/Libraries/LibWeb/DOM/PseudoElement.cpp index 30ad8c722e416..ab46fce3c721b 100644 --- a/Libraries/LibWeb/DOM/PseudoElement.cpp +++ b/Libraries/LibWeb/DOM/PseudoElement.cpp @@ -83,7 +83,7 @@ void SyntheticPseudoElement::clear_computed_style(RefPtrpin_style_record_for_detachment(); } - replace_style_record(0); + m_style_record_identity = 0; } void SyntheticPseudoElement::refresh_computed_style(CSS::StyleRecordID style_record_identity) diff --git a/Libraries/LibWeb/DOM/SelectorQuery.cpp b/Libraries/LibWeb/DOM/SelectorQuery.cpp index 5ba0f720b45e1..2bc5ce3b1ed0d 100644 --- a/Libraries/LibWeb/DOM/SelectorQuery.cpp +++ b/Libraries/LibWeb/DOM/SelectorQuery.cpp @@ -24,15 +24,18 @@ class IsolatedSelectorQueryEngine { : m_engine(CSS::StyleEngine::DeviceClass::ForegroundDesktop) , m_has_document_root(is(root)) { - CSS::populate_isolated_selector_query_engine(m_engine, root, [&](GC::Ref element, CSS::StyleNodeID identity) { - m_identities.set(element, identity); - }); - + // Attribute-name and default value case behavior are compiled into the query, so publish + // the document kind before compiling rather than while facts are populated afterward. + CSS::configure_isolated_selector_query_engine(m_engine, root.document()); Vector selector_handles; selector_handles.ensure_capacity(selectors.size()); for (auto const& selector : selectors) selector_handles.unchecked_append(&selector->rust_selector()); m_query = m_engine.compile_selector_query(selector_handles); + + CSS::populate_isolated_selector_query_engine(m_engine, root, [&](GC::Ref element, CSS::StyleNodeID identity) { + m_identities.set(element, identity); + }); } ~IsolatedSelectorQueryEngine() diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index b0cd833bc0eb1..60f8e7a2b93fb 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -1177,6 +1177,10 @@ GC::Ref Internals::style_engine_counters() JS::Value(static_cast(value)), JS::default_attributes); } + object->define_direct_property( + "computedStyleRecordViewPins"_utf16_fly_string, + JS::Value(static_cast(window().associated_document().style_computer().computed_style_record_view_pin_count())), + JS::default_attributes); return object; } @@ -1192,6 +1196,18 @@ u64 Internals::layout_style_record_identity(DOM::Element& element) return layout_node ? layout_node->style_record_identity().value() : 0; } +u64 Internals::before_style_record_identity(DOM::Element& element) +{ + return element.style_record_identity(CSS::PseudoElement::Before).value(); +} + +u64 Internals::before_layout_style_record_identity(DOM::Element& element) +{ + element.document().update_layout(DOM::UpdateLayoutReason::Debugging); + auto const* layout_node = element.pseudo_element_unsafe_layout_node(CSS::PseudoElement::Before); + return layout_node ? layout_node->style_record_identity().value() : 0; +} + u64 Internals::paint_style_record_identity(DOM::Element& element) { element.document().update_layout(DOM::UpdateLayoutReason::Debugging); diff --git a/Libraries/LibWeb/Internals/Internals.h b/Libraries/LibWeb/Internals/Internals.h index 3c0f5f6e3ee34..a894f97153aa5 100644 --- a/Libraries/LibWeb/Internals/Internals.h +++ b/Libraries/LibWeb/Internals/Internals.h @@ -179,6 +179,8 @@ class WEB_API Internals final : public InternalsBase { GC::Ref style_engine_counters(); u64 style_record_identity(DOM::Element&); u64 layout_style_record_identity(DOM::Element&); + u64 before_style_record_identity(DOM::Element&); + u64 before_layout_style_record_identity(DOM::Element&); u64 paint_style_record_identity(DOM::Element&); u64 layout_node_identity(DOM::Node&); double style_engine_match_document(); diff --git a/Libraries/LibWeb/Internals/Internals.idl b/Libraries/LibWeb/Internals/Internals.idl index 3dfd6593cab32..eb58e7934af94 100644 --- a/Libraries/LibWeb/Internals/Internals.idl +++ b/Libraries/LibWeb/Internals/Internals.idl @@ -174,6 +174,8 @@ interface Internals { object styleEngineCounters(); unsigned long long styleRecordIdentity(Element element); unsigned long long layoutStyleRecordIdentity(Element element); + unsigned long long beforeStyleRecordIdentity(Element element); + unsigned long long beforeLayoutStyleRecordIdentity(Element element); unsigned long long paintStyleRecordIdentity(Element element); // Stable for the lifetime of a layout node, and zero when the DOM node has no principal // layout node. Useful for asserting that incremental tree builds preserve unaffected nodes. diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 3f441a4b06bd7..6970948734b1c 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -844,6 +844,8 @@ NodeWithStyle::NodeWithStyle(DOM::Document& document, GC::Ptr node, C publish_style_record_to_node_data(); synchronize_table_span_data(); enroll_for_arena_replaced_content_facts_sync_if_eligible(); + if (m_owned_computed_values) + pin_style_record_for_cxx_consumers(); } CSS::ComputedValues const& NodeWithStyle::owned_computed_values() const @@ -956,6 +958,11 @@ void NodeWithStyle::apply_style(CSS::StyleRecordID style_record_identity) enroll_for_arena_replaced_content_facts_sync_if_eligible(); propagate_style_to_anonymous_wrappers(); attach_style_resources(); + // A pseudo layout node can outlive replacement of the DOM pseudo's record until the layout + // tree is rebuilt. Root its record across that gap, including metadata-only style changes that + // keep the existing layout node. + if (is_generated_for_pseudo_element()) + pin_style_record_for_cxx_consumers(); } void NodeWithStyle::attach_style_resources() @@ -1287,6 +1294,8 @@ void NodeWithStyle::set_computed_values(NonnullRefPtr set_flag(RustFFI::NodeFlag::InsetsUseAnchorFunctions, computed_values->inset_properties_contain_anchor_functions()); publish_style_record_to_node_data(); enroll_for_arena_replaced_content_facts_sync_if_eligible(); + if (m_owned_computed_values) + pin_style_record_for_cxx_consumers(); if (changes_layout_affecting_style) bump_fragment_cache_epoch_of_self_and_ancestors(); @@ -1301,13 +1310,14 @@ void NodeWithStyle::set_style_record_identity(CSS::StyleRecordID style_record_id { // A detached or layout-derived record is independent of its DOM target's record. A // rendering consequence replaces and re-derives it explicitly through apply_style(). - if (m_style_record_owner || m_owned_computed_values) + if (m_owned_computed_values) return; if (m_style_record_identity == style_record_identity) { publish_style_record_to_node_data(); return; } + bool should_repin_style_record = m_style_record_owner; auto new_record_view = document().style_computer().computed_style_record_view(style_record_identity); VERIFY(new_record_view); bool changes_layout_affecting_style = false; @@ -1330,6 +1340,8 @@ void NodeWithStyle::set_style_record_identity(CSS::StyleRecordID style_record_id set_flag(RustFFI::NodeFlag::InsetsUseAnchorFunctions, new_record_view->inset_properties_contain_anchor_functions()); publish_style_record_to_node_data(); enroll_for_arena_replaced_content_facts_sync_if_eligible(); + if (should_repin_style_record) + pin_style_record_for_cxx_consumers(); if (changes_layout_affecting_style) bump_fragment_cache_epoch_of_self_and_ancestors(); @@ -1337,7 +1349,7 @@ void NodeWithStyle::set_style_record_identity(CSS::StyleRecordID style_record_id void NodeWithStyle::pin_style_record_for_cxx_consumers() { - if (m_owned_computed_values || m_style_record_owner) + if (m_style_record_owner) return; VERIFY(m_style_record_identity); diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index 6698ede2784cd..dfba4158cefe9 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -848,6 +848,10 @@ class WEB_API NodeWithStyle : public Node { CSS::ComputedValues const& owned_computed_values() const; RefPtr m_owned_computed_values; CSS::StyleRecordID m_style_record_identity; + // Layout nodes are ref-counted rather than GC cells, so this owner cannot be a traced GC::Ptr. + // Document::tear_down_layout_tree() must drop the layout root, which destroys these nodes and + // unpins their records before this root is cleared. Every document destruction path goes + // through that teardown. GC::Root m_style_record_owner; Vector> m_image_observers; Vector> m_cursor_style_values; diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index 291057482b614..4cf7de27f5fee 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -485,6 +485,7 @@ static RustFFI::FfiComputedContentType ffi_computed_content_type(CSS::ComputedCo struct PseudoElementFrame { CSS::StyleRecordID style_record_identity; + GC::Ptr style_record_owner; CSS::Display display; RefPtr replacement_image; BlockContainer* originating_list_box { nullptr }; @@ -528,6 +529,13 @@ RustFFI::FfiPseudoTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_pseudo_tr auto& storage = *static_cast(builder_pointer)->m_pseudo_element_frames; VERIFY(storage.active_frame_count > 0); VERIFY(storage.frames[storage.active_frame_count - 1].ptr() == frame_pointer); + auto& frame = *storage.frames[storage.active_frame_count - 1]; + if (!!frame.style_record_identity) { + VERIFY(frame.style_record_owner); + frame.style_record_owner->unpin_style_record(frame.style_record_identity); + frame.style_record_identity = {}; + frame.style_record_owner = nullptr; + } --storage.active_frame_count; }, .initialize = [](void* frame_pointer, void* element_pointer, RustFFI::FfiPseudoElement ffi_pseudo) -> RustFFI::FfiPseudoElementFacts { VERIFY(frame_pointer); @@ -535,9 +543,15 @@ RustFFI::FfiPseudoTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_pseudo_tr auto& frame = *static_cast(frame_pointer); auto& element = *static_cast(element_pointer); auto pseudo_element = css_pseudo_element(ffi_pseudo); + VERIFY(!frame.style_record_identity); + VERIFY(!frame.style_record_owner); if (auto existing_pseudo = element.get_synthetic_pseudo_element(pseudo_element); existing_pseudo.has_value() && existing_pseudo->layout_node()) existing_pseudo->set_layout_node(nullptr); frame.style_record_identity = element.style_record_identity(pseudo_element); + if (!!frame.style_record_identity) { + frame.style_record_owner = &element.document().style_computer(); + frame.style_record_owner->pin_style_record(frame.style_record_identity); + } frame.replacement_image = nullptr; frame.originating_list_box = nullptr; frame.layout_node = nullptr; @@ -798,6 +812,7 @@ struct PrincipalNodeFrame { RefPtr layout_node; RefPtr anonymous_computed_values; CSS::StyleRecordID style_record_identity; + GC::Ptr style_record_owner; }; struct LayoutTreeBuildBridge::PrincipalNodeFrameStorage { @@ -1016,6 +1031,7 @@ RustFFI::FfiDomTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_dom_tree_bui frame.old_layout_node = node.unsafe_layout_node(); frame.layout_node = nullptr; frame.anonymous_computed_values = nullptr; + VERIFY(!frame.style_record_owner); frame.style_record_identity = 0; return { .frame = &frame, @@ -1030,6 +1046,11 @@ RustFFI::FfiDomTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_dom_tree_bui VERIFY(storage.active_frame_count > 0); VERIFY(storage.frames[storage.active_frame_count - 1].ptr() == frame_pointer); auto& frame = *storage.frames[storage.active_frame_count - 1]; + if (!!frame.style_record_identity) { + VERIFY(frame.style_record_owner); + frame.style_record_owner->unpin_style_record(frame.style_record_identity); + frame.style_record_owner = nullptr; + } frame.old_layout_node = nullptr; frame.layout_node = nullptr; frame.anonymous_computed_values = nullptr; @@ -1053,6 +1074,9 @@ RustFFI::FfiDomTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_dom_tree_bui update_style_if_needed_for_layout_tree_bypass_path(element); } frame.style_record_identity = element.style_record_identity(); + VERIFY(frame.style_record_identity); + frame.style_record_owner = &element.document().style_computer(); + frame.style_record_owner->pin_style_record(frame.style_record_identity); auto computed_values = element.computed_style(); VERIFY(computed_values); return { diff --git a/Libraries/LibWeb/Rust/StyleEngineBoundary.json b/Libraries/LibWeb/Rust/StyleEngineBoundary.json index d68e43d095ba1..80cd17bd26a6e 100644 --- a/Libraries/LibWeb/Rust/StyleEngineBoundary.json +++ b/Libraries/LibWeb/Rust/StyleEngineBoundary.json @@ -80,7 +80,9 @@ [77, "StyleDeltaBatch"], [78, "SelectorQueryAtomMappings"], [79, "PrepareSelectorQuery"], - [80, "AddCounterStyleRule"] + [80, "AddCounterStyleRule"], + [81, "AttributeValueTextRequirementsVersion"], + [82, "AttributeNameRequiresValueText"] ], "operations": [ { "event": "SetFoldIdAndClassNameCase", "ffi": "style_engine_set_fold_id_and_class_name_case", "cpp": "set_fold_id_and_class_name_case", "return": "void", "args": [["fold", "bool"]], "body": "engine.set_fold_id_and_class_name_case(fold);" }, @@ -125,6 +127,8 @@ { "event": "SetRuleLayer", "ffi": "style_engine_set_rule_layer", "cpp": "set_rule_layer", "return": "void", "args": [["rule", "style_rule_id"], ["layer", "u32"]], "body": "if rule == 0 { return; } engine.set_rule_layer(RuleID(rule - 1), CascadeLayerID(layer));" }, { "event": "SetAttributeValueText", "ffi": "style_engine_set_attribute_value_text", "return": "void", "args": [["value", "style_atom"], ["text", "u16_slice"]], "body": "if value == 0 { return; } engine.set_attribute_value_text(StyleAtomID(value), text);" }, { "event": "HasAttributeValueText", "ffi": "style_engine_has_attribute_value_text", "return": "bool", "receiver": "const", "args": [["value", "style_atom"]], "body": "let result = engine.has_attribute_value_text(StyleAtomID(value));" }, + { "event": "AttributeValueTextRequirementsVersion", "ffi": "style_engine_attribute_value_text_requirements_version", "return": "u64", "receiver": "const", "replay": false, "args": [], "body": "let result = engine.attribute_value_text_requirements_version();" }, + { "event": "AttributeNameRequiresValueText", "ffi": "style_engine_attribute_name_requires_value_text", "return": "bool", "receiver": "const", "replay": false, "args": [["name", "style_atom"]], "body": "let result = engine.attribute_name_requires_value_text(StyleAtomID(name));" }, { "event": "DiscardStyleTransactionOutputs", "ffi": "style_engine_discard_style_transaction_outputs", "return": "void", "args": [], "body": "engine.discard_style_transaction_outputs();" }, { "event": "RecordLayerStatement", "ffi": "style_engine_record_layer_statement", "cpp": "record_layer_statement", "return": "void", "args": [["sheet", "sheet_id"]], "body": "if sheet != 0 { engine.record_layer_statement(SheetID(sheet - 1)); }" }, { "event": "AddPropertyRule", "ffi": "style_engine_add_property_rule", "cpp": "add_property_rule", "return": "style_rule_id", "args": [["sheet", "sheet_id"], ["before_rule", "style_rule_id"], ["name_atom", "style_atom"], ["has_initial_value", "bool"]], "body": "let result = if sheet == 0 { 0 } else { let before = (before_rule != 0).then(|| RuleID(before_rule - 1)); engine.add_property_rule(SheetID(sheet - 1), before, StyleAtomID(name_atom), has_initial_value).0 + 1 };" }, diff --git a/Libraries/LibWeb/Rust/src/bin/style_replay.rs b/Libraries/LibWeb/Rust/src/bin/style_replay.rs index f22112562e20e..e0aa9791ee058 100644 --- a/Libraries/LibWeb/Rust/src/bin/style_replay.rs +++ b/Libraries/LibWeb/Rust/src/bin/style_replay.rs @@ -9,9 +9,13 @@ use std::alloc::Layout; use std::alloc::System; use std::collections::BTreeMap; use std::collections::BTreeSet; +use std::collections::HashSet; use std::collections::VecDeque; +use std::collections::hash_map::DefaultHasher; use std::env; use std::ffi::c_void; +use std::hash::Hash; +use std::hash::Hasher; use std::path::PathBuf; use std::process::ExitCode; use std::time::Duration; @@ -19,6 +23,7 @@ use std::time::Instant; use libweb_rust::css::style::bridge; use libweb_rust::css::style::bridge::FfiCascadeOrigin; +use libweb_rust::css::style::bridge::FfiElementArrival; use libweb_rust::css::style::bridge::FfiElementDeclarationDelta; use libweb_rust::css::style::bridge::FfiElementDeclarationKind; use libweb_rust::css::style::bridge::FfiElementStyleInput; @@ -47,6 +52,7 @@ use libweb_rust::css::style::record_replay::EventKind; use libweb_rust::css::style::record_replay::LogReader; use libweb_rust::css::style::record_replay::PayloadReader; use libweb_rust::css::style::record_replay::PayloadWriter; +use libweb_rust::css::style::selector::SelectorProgram; include!(concat!(env!("OUT_DIR"), "/style_engine_replay_generated.rs")); @@ -80,12 +86,21 @@ fn run() -> Result<(), Box> { let mut match_answer_identity_mappings = Vec::::new(); let mut engine_count = 0_u64; let mut event_count = 0_u64; + let mut intern_atom_boundary_calls = 0_u64; + let mut element_arrivals = 0_u64; + let mut element_fact_calls = 0_u64; + let mut element_declaration_calls = 0_u64; + let mut element_animation_name_calls = 0_u64; + let mut attribute_value_text_queries = 0_u64; + let mut attribute_value_text_publications = 0_u64; + let mut selector_program_sharing = SelectorProgramSharing::default(); let mut flush_count = 0_u64; let mut presence_degraded_publication_comparisons = 0_u64; let mut boundary_time = Duration::ZERO; let mut selected_flush_count = 0_u64; let mut selected_boundary_time = Duration::ZERO; let mut active_phase = None; + let mut last_benchmark_marker = String::new(); let mut phase_times = BTreeMap::::new(); let mut pending_changed_rows = FastMap::::default(); let mut amplification = AmplificationLedger::default(); @@ -94,6 +109,8 @@ fn run() -> Result<(), Box> { let mut detailed_counter_reader = None; let mut recorded_style_record_payloads = Vec::>>>>::new(); let mut recorded_style_record_views = Vec::>>>>::new(); + let mut computed_longhand_tables = + Vec::>>::new(); let mut computed_group_payloads = Vec::new(); let mut animation_overlay_payloads = Vec::new(); let mut inheritance_dependent_properties = Vec::new(); @@ -109,6 +126,19 @@ fn run() -> Result<(), Box> { break; }; event_count += 1; + match event.kind { + EventKind::SetElementNamespace + | EventKind::SetElementLanguage + | EventKind::SetElementDirectionality + | EventKind::SetElementCustomStates + | EventKind::SetElementIsSlot + | EventKind::SetElementHeadingLevel => element_fact_calls += 1, + EventKind::SetElementDeclaredProperties => element_declaration_calls += 1, + EventKind::SetElementAnimationNames => element_animation_name_calls += 1, + EventKind::HasAttributeValueText => attribute_value_text_queries += 1, + EventKind::SetAttributeValueText => attribute_value_text_publications += 1, + _ => {} + } match event.kind { kind if replay_generated_boundary_event(kind, &mut event.payload, &live_engines)? => {} EventKind::CreateGraph => { @@ -130,14 +160,17 @@ fn run() -> Result<(), Box> { if engine_id == 0 || live_engines.get(index).is_some_and(Option::is_some) { return Err(format!("engine {engine_id} was created more than once").into()); } - let engine = bridge::style_engine_create(bridge::FfiDeviceClass::ForegroundDesktop); + let engine = bridge::style_engine_create_for_replay(bridge::FfiDeviceClass::ForegroundDesktop); unsafe { bridge::style_engine_use_recording_memory_policy(engine) }; if live_engines.len() <= index { live_engines.resize(index + 1, None); + selector_program_sharing.resize_engines(index + 1); match_answer_identity_mappings.resize_with(index + 1, MatchAnswerIdentityMapping::default); + computed_longhand_tables.resize_with(index + 1, Vec::new); } live_engines[index] = Some(engine); match_answer_identity_mappings[index] = MatchAnswerIdentityMapping::default(); + computed_longhand_tables[index].clear(); engine_count += 1; } EventKind::SetComputedGroupDependencyMasks => { @@ -155,11 +188,12 @@ fn run() -> Result<(), Box> { } EventKind::DestroyGraph => { let engine_id = event.payload.read_u64()?; - let engine = usize::try_from(engine_id) - .ok() - .and_then(|index| live_engines.get_mut(index)) + let engine_index = usize::try_from(engine_id)?; + let engine = live_engines + .get_mut(engine_index) .and_then(Option::take) .ok_or_else(|| format!("engine {engine_id} was destroyed without being live"))?; + release_computed_longhand_tables(&mut computed_longhand_tables[engine_index]); accumulate_memory_pressure(&mut memory_pressure, unsafe { bridge::replay_memory_pressure_snapshot(engine) }); @@ -180,6 +214,12 @@ fn run() -> Result<(), Box> { let engine = read_engine(&mut event.payload, &live_engines)?; // The row arrays are consumed in place from the mapped log; nothing is copied. let tree = event.payload.read_raw_slice::()?; + element_arrivals += tree + .iter() + .filter(|delta| !delta.old_connected && delta.new_connected) + .count() as u64; + let arrivals = event.payload.read_raw_slice::()?; + let arrival_custom_state_atoms = event.payload.read_u32_vec()?; let features = event.payload.read_raw_slice::()?; let states = event.payload.read_raw_slice::()?; let declarations = event.payload.read_raw_slice::()?; @@ -191,6 +231,10 @@ fn run() -> Result<(), Box> { let transaction = FfiStyleInputTransaction { tree_deltas: tree.as_ptr(), tree_delta_count: tree.len(), + element_arrivals: arrivals.as_ptr(), + element_arrival_count: arrivals.len(), + arrival_custom_state_atoms: arrival_custom_state_atoms.as_ptr(), + arrival_custom_state_atom_count: arrival_custom_state_atoms.len(), local_feature_deltas: features.as_ptr(), local_feature_delta_count: features.len(), state_deltas: states.as_ptr(), @@ -238,7 +282,7 @@ fn run() -> Result<(), Box> { } } EventKind::AddStyleRule => { - let engine = read_engine(&mut event.payload, &live_engines)?; + let (engine_index, engine) = read_engine_indexed(&mut event.payload, &live_engines)?; let sheet = event.payload.read_u32()?; let before_rule = event.payload.read_u32()?; let expected = event.payload.read_u32()?; @@ -247,6 +291,7 @@ fn run() -> Result<(), Box> { _ => { replay_atom_mappings(engine, &mut event.payload)?; let program = libweb_rust::css::style::selector::replay::read(&mut event.payload)?; + selector_program_sharing.record(engine_index, &program); unsafe { bridge::replay_add_style_rule(engine, sheet, before_rule, program) } } }; @@ -255,6 +300,7 @@ fn run() -> Result<(), Box> { } } EventKind::InternAtom => { + intern_atom_boundary_calls += 1; let engine = read_engine(&mut event.payload, &live_engines)?; let token = usize::try_from(event.payload.read_u64()?)?; let expected = event.payload.read_u32()?; @@ -266,10 +312,11 @@ fn run() -> Result<(), Box> { replay_atom_mappings(engine, &mut event.payload)?; } EventKind::ReplaceStyleRuleSelectors => { - let engine = read_engine(&mut event.payload, &live_engines)?; + let (engine_index, engine) = read_engine_indexed(&mut event.payload, &live_engines)?; let rule = event.payload.read_u32()?; replay_atom_mappings(engine, &mut event.payload)?; let program = libweb_rust::css::style::selector::replay::read(&mut event.payload)?; + selector_program_sharing.record(engine_index, &program); unsafe { bridge::replay_replace_style_rule_selectors(engine, rule, program) }; } EventKind::SetElementDeclaredProperties => { @@ -323,8 +370,44 @@ fn run() -> Result<(), Box> { .get_or_insert_with(|| DetailedCounterReader::new(engine)) .read(engine) }); + if expected.style_atoms_swept { + unsafe { + bridge::style_engine_set_replay_reclaimed_style_atoms( + engine, + expected.reclaimed_atoms.as_ptr(), + expected.reclaimed_atoms.len(), + ); + } + } let start = Instant::now(); let actual_view = unsafe { bridge::style_engine_take_style_transaction(engine, root) }; + let actual_reclaimed_atoms = if actual_view.reclaimed_style_atom_count == 0 { + Vec::new() + } else { + unsafe { + std::slice::from_raw_parts( + actual_view.reclaimed_style_atoms, + actual_view.reclaimed_style_atom_count, + ) + } + .iter() + .map(|reclaimed| reclaimed.atom) + .collect::>() + }; + if actual_view.style_atoms_swept != expected.style_atoms_swept { + return Err(format!( + "style atom sweep diverged: expected {}, got {}", + expected.style_atoms_swept, actual_view.style_atoms_swept + ) + .into()); + } + if actual_reclaimed_atoms != expected.reclaimed_atoms { + return Err(format!( + "style atom reclamation diverged: expected {:?}, got {:?}", + expected.reclaimed_atoms, actual_reclaimed_atoms + ) + .into()); + } if actual_view.count != 0 { let answers = unsafe { std::slice::from_raw_parts(actual_view.answers, actual_view.count) }; replay_style_transaction_output( @@ -367,6 +450,8 @@ fn run() -> Result<(), Box> { result: actual_view.scoped, filter_calls: Vec::new(), emissions: context.actual_emissions, + style_atoms_swept: actual_view.style_atoms_swept, + reclaimed_atoms: actual_reclaimed_atoms, }; let mut actual_payload = PayloadWriter::default(); write_style_transaction_outputs(&actual, &mut actual_payload); @@ -481,6 +566,7 @@ fn run() -> Result<(), Box> { EventKind::BenchmarkMarker => { let _engine = read_engine(&mut event.payload, &live_engines)?; let name = String::from_utf16(&event.payload.read_u16_vec()?)?; + last_benchmark_marker.clone_from(&name); if let Some(marker) = BenchmarkMarker::parse(&name) { encountered_subtests.insert(format!("{}/{}", marker.suite, marker.test)); active_phase = marker.next_phase(); @@ -711,7 +797,7 @@ fn run() -> Result<(), Box> { } } EventKind::PublishComputedGroups => { - let engine = read_engine(&mut event.payload, &live_engines)?; + let (engine_index, engine) = read_engine_indexed(&mut event.payload, &live_engines)?; let node = event.payload.read_u32()?; let pseudo_kind = event.payload.read_u8()?; let group_count = event.payload.read_length()?; @@ -759,38 +845,45 @@ fn run() -> Result<(), Box> { let longhand_table = match event.payload.read_bool()? { false => std::ptr::null_mut(), true => { - let table = - libweb_rust::css::computed_longhand_table::rust_computed_longhand_table_create(); - let stored_value_count = event.payload.read_length()?; - let mut stored_values = Vec::with_capacity(stored_value_count); - for _ in 0..stored_value_count { - let property = event.payload.read_u16()?; - let token = event.payload.read_u64()?; - let flags = event.payload.read_u8()?; - stored_values.push((property, bridge::replay_style_value(token, flags))); - } - let source_slot_count = event.payload.read_length()?; - let mut source_slots = std::collections::HashMap::new(); - for _ in 0..source_slot_count { - let property = event.payload.read_u16()?; - let slot = event.payload.read_u32()?; - source_slots.insert(property, slot); - } - for (property, value) in stored_values { - let slot = source_slots.get(&property).map_or(-1, |&slot| i64::from(slot)); + let identity = usize::try_from(event.payload.read_u32()?)?; + let record_definition = event.payload.read_bool()?; + let tables = &mut computed_longhand_tables[engine_index]; + if record_definition { + if tables.len() <= identity { + tables.resize(identity + 1, None); + } + if tables[identity].is_some() { + return Err(format!("computed longhand table {identity} was defined twice").into()); + } + let table = + libweb_rust::css::computed_longhand_table::rust_computed_longhand_table_create(); + let stored_value_count = event.payload.read_length()?; + for _ in 0..stored_value_count { + let property = event.payload.read_u16()?; + let token = event.payload.read_u64()?; + let flags = event.payload.read_u8()?; + let value = bridge::replay_style_value(token, flags); + unsafe { + libweb_rust::css::computed_longhand_table::rust_computed_longhand_table_set( + table, + property, + value.cast(), + -1, + ); + } + } unsafe { - libweb_rust::css::computed_longhand_table::rust_computed_longhand_table_set( + libweb_rust::css::computed_longhand_table::rust_computed_longhand_table_freeze( table, - property, - value.cast(), - slot, ); } + tables[identity] = Some(table); } - unsafe { - libweb_rust::css::computed_longhand_table::rust_computed_longhand_table_freeze(table); - } - table + tables + .get(identity) + .copied() + .flatten() + .ok_or_else(|| format!("computed longhand table {identity} was not defined"))? } }; let expected = bridge::FfiStyleRecordDelta { @@ -824,16 +917,9 @@ fn run() -> Result<(), Box> { longhand_table.cast_const().cast(), ) }; - if !longhand_table.is_null() { - unsafe { - libweb_rust::css::computed_longhand_table::rust_computed_longhand_table_release( - longhand_table, - ); - } - } if actual != expected { return Err(format!( - "computed style publication diverged for node {node}: expected {expected:?}, got {actual:?}" + "computed style publication diverged at event {event_count} after {last_benchmark_marker:?} in {active_phase:?} for node {node}: expected {expected:?}, got {actual:?}" ) .into()); } @@ -925,11 +1011,23 @@ fn run() -> Result<(), Box> { } } } + EventKind::AttributeValueTextRequirementsVersion => { + let _engine = read_engine(&mut event.payload, &live_engines)?; + let _recorded_version = event.payload.read_u64()?; + } + EventKind::AttributeNameRequiresValueText => { + let _engine = read_engine(&mut event.payload, &live_engines)?; + let _name = event.payload.read_u32()?; + let _recorded_result = event.payload.read_bool()?; + } _ => unreachable!("all boundary events are generated or handled explicitly"), } event.payload.finish()?; } let live_at_process_exit = live_engines.iter().flatten().count(); + for tables in &mut computed_longhand_tables { + release_computed_longhand_tables(tables); + } for engine in live_engines.into_iter().flatten() { accumulate_memory_pressure(&mut memory_pressure, unsafe { bridge::replay_memory_pressure_snapshot(engine) @@ -976,6 +1074,33 @@ fn run() -> Result<(), Box> { println!(" {phase}: {:.3} ms", elapsed.as_secs_f64() * 1000.0); } amplification.print(); + if options.detailed_counters { + println!("boundary counters:"); + println!(" internAtomCalls: {intern_atom_boundary_calls}"); + println!(" elementArrivals: {element_arrivals}"); + println!( + " elementFactCalls: {} ({:.2}/arrival)", + element_fact_calls, + element_fact_calls as f64 / element_arrivals.max(1) as f64 + ); + println!(" elementDeclarationCalls: {element_declaration_calls}"); + println!(" elementAnimationNameCalls: {element_animation_name_calls}"); + println!(" attributeValueTextQueries: {attribute_value_text_queries}"); + println!(" attributeValueTextPublications: {attribute_value_text_publications}"); + println!( + " selectorProgramCompilations: {}", + selector_program_sharing.compilations + ); + println!( + " selectorProgramsDistinct: documents={}, process={}", + selector_program_sharing.document_distinct_count(), + selector_program_sharing.process_hashes.len() + ); + println!( + " selectorProgramBytesDistinct: documents={}, process={}", + selector_program_sharing.document_distinct_bytes, selector_program_sharing.process_distinct_bytes + ); + } detailed_counters.print(detailed_counter_reader.as_ref()); reports.push(serde_json::json!({ "path": path, @@ -984,6 +1109,21 @@ fn run() -> Result<(), Box> { "document_engines": engine_count, "live_at_capture_exit": live_at_process_exit, "presence_degraded_exact_cascade_publication_comparisons": presence_degraded_publication_comparisons, + "boundary_counters": { + "intern_atom_calls": intern_atom_boundary_calls, + "element_arrivals": element_arrivals, + "element_fact_calls": element_fact_calls, + "element_fact_calls_per_arrival": element_fact_calls as f64 / element_arrivals.max(1) as f64, + "element_declaration_calls": element_declaration_calls, + "element_animation_name_calls": element_animation_name_calls, + "attribute_value_text_queries": attribute_value_text_queries, + "attribute_value_text_publications": attribute_value_text_publications, + "selector_program_compilations": selector_program_sharing.compilations, + "selector_programs_distinct_documents": selector_program_sharing.document_distinct_count(), + "selector_programs_distinct_process": selector_program_sharing.process_hashes.len(), + "selector_program_bytes_distinct_documents": selector_program_sharing.document_distinct_bytes, + "selector_program_bytes_distinct_process": selector_program_sharing.process_distinct_bytes, + }, "timing": { "boundary_ms": duration_ms(boundary_time), "selected_flushes": selected_flush_count, @@ -1017,6 +1157,39 @@ fn run() -> Result<(), Box> { Ok(()) } +#[derive(Default)] +struct SelectorProgramSharing { + compilations: u64, + document_hashes: Vec>, + process_hashes: HashSet, + document_distinct_bytes: u64, + process_distinct_bytes: u64, +} + +impl SelectorProgramSharing { + fn resize_engines(&mut self, length: usize) { + self.document_hashes.resize_with(length, HashSet::new); + } + + fn record(&mut self, engine: usize, program: &SelectorProgram) { + let mut hasher = DefaultHasher::new(); + program.hash(&mut hasher); + let hash = hasher.finish(); + let bytes = program.capacity_bytes(); + self.compilations += 1; + if self.document_hashes[engine].insert(hash) { + self.document_distinct_bytes += bytes; + } + if self.process_hashes.insert(hash) { + self.process_distinct_bytes += bytes; + } + } + + fn document_distinct_count(&self) -> usize { + self.document_hashes.iter().map(HashSet::len).sum() + } +} + #[derive(Clone)] enum Selection { Full, @@ -1119,7 +1292,7 @@ impl Options { } } -#[derive(Clone)] +#[derive(Clone, Debug)] struct ActivePhase { suite: String, test: String, @@ -1576,10 +1749,19 @@ fn read_engine_indexed( Ok((index, pointer)) } +fn release_computed_longhand_tables( + tables: &mut Vec>, +) { + for table in tables.drain(..).flatten() { + unsafe { libweb_rust::css::computed_longhand_table::rust_computed_longhand_table_release(table) }; + } +} + fn style_record_replay_index(style_record: u64) -> Result { const ANIMATION_OVERLAY_TAG: u64 = 1 << 63; + const BASE_IDENTITY_MASK: u64 = u32::MAX as u64; - let identity = style_record & !ANIMATION_OVERLAY_TAG; + let identity = style_record & BASE_IDENTITY_MASK; let namespace = u64::from(style_record & ANIMATION_OVERLAY_TAG != 0); usize::try_from(identity * 2 + namespace) } @@ -1656,6 +1838,8 @@ struct StyleTransactionOutputs { result: bool, filter_calls: Vec<(Vec, Vec)>, emissions: Vec, + style_atoms_swept: bool, + reclaimed_atoms: Vec, } struct ReplayStyleTransactionContext { @@ -1799,11 +1983,15 @@ fn read_style_transaction_outputs( answers, }); } + let style_atoms_swept = payload.read_bool()?; + let reclaimed_atoms = payload.read_u32_vec()?; payload.finish()?; Ok(StyleTransactionOutputs { result, filter_calls, emissions, + style_atoms_swept, + reclaimed_atoms, }) } @@ -1831,6 +2019,8 @@ fn write_style_transaction_outputs(outputs: &StyleTransactionOutputs, payload: & payload.write_u8(answer.gap as u8); } } + payload.write_bool(outputs.style_atoms_swept); + payload.write_u32_slice(&outputs.reclaimed_atoms); } fn replay_atom_mappings(engine: *mut c_void, payload: &mut PayloadReader) -> Result<(), Box> { @@ -2387,6 +2577,39 @@ mod tests { assert_eq!(amplification["stages"]["selector"]["touched_rows"], 10); assert_eq!(amplification["stages"]["selector"]["amplification"], 2.5); } + + #[test] + fn style_transaction_outputs_round_trip_a_sweep_without_reclaims() { + let expected = StyleTransactionOutputs { + result: true, + filter_calls: Vec::new(), + emissions: Vec::new(), + style_atoms_swept: true, + reclaimed_atoms: Vec::new(), + }; + let mut payload = PayloadWriter::default(); + write_style_transaction_outputs(&expected, &mut payload); + + let actual = read_style_transaction_outputs(PayloadReader::new(payload.as_bytes())).unwrap(); + + assert!(actual.result); + assert!(actual.filter_calls.is_empty()); + assert!(actual.emissions.is_empty()); + assert!(actual.style_atoms_swept); + assert_eq!(actual.reclaimed_atoms, expected.reclaimed_atoms); + } + + #[test] + fn style_record_replay_indices_ignore_base_generations() { + let identity = 17; + let first_generation = identity; + let later_generation = (29_u64 << 32) | identity; + let overlay = (1_u64 << 63) | identity; + + assert_eq!(style_record_replay_index(first_generation).unwrap(), 34); + assert_eq!(style_record_replay_index(later_generation).unwrap(), 34); + assert_eq!(style_record_replay_index(overlay).unwrap(), 35); + } } /// A read-only memory mapping of one capture file. diff --git a/Libraries/LibWeb/Rust/src/css/computed_longhand_table.rs b/Libraries/LibWeb/Rust/src/css/computed_longhand_table.rs index cccf32a976209..1f64e3ae0a891 100644 --- a/Libraries/LibWeb/Rust/src/css/computed_longhand_table.rs +++ b/Libraries/LibWeb/Rust/src/css/computed_longhand_table.rs @@ -405,15 +405,6 @@ impl ComputedLonghandTable { &self.value_view } - /// The complete style-sheet-context sidecar, in property order. - pub(crate) fn source_slot_entries(&self) -> impl Iterator + '_ { - self.source_slots.iter().enumerate().filter_map(|(index, &slot)| { - u32::try_from(slot) - .ok() - .map(|slot| (FIRST_LONGHAND_PROPERTY_ID + index as u16, slot)) - }) - } - pub(crate) fn is_frozen(&self) -> bool { self.frozen } diff --git a/Libraries/LibWeb/Rust/src/css/style/atoms.rs b/Libraries/LibWeb/Rust/src/css/style/atoms.rs new file mode 100644 index 0000000000000..70db4734b444f --- /dev/null +++ b/Libraries/LibWeb/Rust/src/css/style/atoms.rs @@ -0,0 +1,637 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +use std::cell::Cell; +use std::cell::RefCell; +use std::collections::BTreeSet; +use std::collections::HashMap; +use std::collections::HashSet; +use std::hash::BuildHasher; +use std::rc::Rc; +use std::sync::Mutex; +use std::sync::OnceLock; + +use super::index::StyleAtomID; + +type RawAtomCallback = unsafe extern "C" fn(usize); + +const TEXT_KEY_MASK: u64 = 0x0000_ffff_ffff_ffff; +const TEXT_KEY_FLOOR: usize = usize::MAX - TEXT_KEY_MASK as usize; + +#[must_use] +pub(super) fn synthetic_text_atom_key(hash: u64) -> usize { + usize::MAX - (hash & TEXT_KEY_MASK) as usize +} + +struct RawAtomCallbacks { + retain: RawAtomCallback, + release: RawAtomCallback, +} + +fn raw_atom_callbacks() -> &'static OnceLock { + static CALLBACKS: OnceLock = OnceLock::new(); + &CALLBACKS +} + +pub(super) fn install_raw_atom_callbacks(retain: RawAtomCallback, release: RawAtomCallback) { + raw_atom_callbacks().get_or_init(|| RawAtomCallbacks { retain, release }); +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RawAtomLifetime { + RetainedFlyString, + SyntheticTextKey, + OpaqueReplayToken, +} + +impl RawAtomLifetime { + fn for_raw(self, raw: usize) -> Self { + match self { + Self::RetainedFlyString if raw >= TEXT_KEY_FLOOR => Self::SyntheticTextKey, + lifetime => lifetime, + } + } +} + +#[derive(Clone, Copy)] +struct GlobalAtomEntry { + atom: StyleAtomID, + document_references: u64, + raw_lifetime: Option, +} + +#[derive(Default)] +struct GlobalAtoms { + raw: HashMap, + qualified: HashMap<(u32, u32), GlobalAtomEntry>, + available: BTreeSet, + next: u32, +} + +impl GlobalAtoms { + fn allocate(&mut self) -> StyleAtomID { + if let Some(atom) = self.available.pop_first() { + return StyleAtomID(atom); + } + self.next = self + .next + .checked_add(1) + .expect("process-global style atom space exhausted"); + StyleAtomID(self.next) + } + + fn acquire_raw(&mut self, raw: usize, lifetime: RawAtomLifetime) -> StyleAtomID { + let lifetime = lifetime.for_raw(raw); + if let Some(entry) = self.raw.get_mut(&raw) { + assert_eq!( + entry.raw_lifetime, + Some(lifetime), + "one raw atom cannot mix live and replay identity" + ); + entry.document_references += 1; + return entry.atom; + } + let atom = self.allocate(); + if lifetime == RawAtomLifetime::RetainedFlyString { + // SAFETY: Live StyleEngine callers pass the raw identity of a referenced Utf16FlyString. + let callbacks = raw_atom_callbacks() + .get() + .expect("live atom callbacks must be installed"); + unsafe { (callbacks.retain)(raw) }; + } + self.raw.insert( + raw, + GlobalAtomEntry { + atom, + document_references: 1, + raw_lifetime: Some(lifetime), + }, + ); + atom + } + + fn acquire_qualified(&mut self, namespace: StyleAtomID, name: StyleAtomID) -> StyleAtomID { + let key = (namespace.0, name.0); + if let Some(entry) = self.qualified.get_mut(&key) { + entry.document_references += 1; + return entry.atom; + } + let atom = self.allocate(); + self.qualified.insert( + key, + GlobalAtomEntry { + atom, + document_references: 1, + raw_lifetime: None, + }, + ); + atom + } + + fn release_raw(&mut self, raw: usize, expected: StyleAtomID) { + let entry = self + .raw + .get_mut(&raw) + .expect("a document must release a live global atom"); + assert_eq!(entry.atom, expected); + entry.document_references -= 1; + if entry.document_references != 0 { + return; + } + let entry = self.raw.remove(&raw).unwrap(); + self.available.insert(entry.atom.0); + if entry.raw_lifetime == Some(RawAtomLifetime::RetainedFlyString) { + // SAFETY: acquire_raw retained exactly one global reference for this entry. + let callbacks = raw_atom_callbacks() + .get() + .expect("live atom callbacks must be installed"); + unsafe { (callbacks.release)(raw) }; + } + } + + fn release_qualified(&mut self, key: (u32, u32), expected: StyleAtomID) { + let entry = self + .qualified + .get_mut(&key) + .expect("a document must release a live global qualified atom"); + assert_eq!(entry.atom, expected); + entry.document_references -= 1; + if entry.document_references != 0 { + return; + } + let entry = self.qualified.remove(&key).unwrap(); + self.available.insert(entry.atom.0); + } +} + +fn global_atoms() -> &'static Mutex { + // The mutex supplies the `Sync` required by a process-global static. It does not make a + // `DocumentAtoms` owner, or the StyleEngine containing it, safe to use from multiple threads. + static GLOBAL_ATOMS: OnceLock> = OnceLock::new(); + GLOBAL_ATOMS.get_or_init(|| Mutex::new(GlobalAtoms::default())) +} + +#[derive(Clone, Copy)] +enum AtomScope { + #[cfg(test)] + Document, + Process(RawAtomLifetime), +} + +pub(super) struct DocumentAtoms { + raw: HashMap, + cpp_memoized_raws: HashSet, + qualified: HashMap<(u32, u32), StyleAtomID>, + scope: AtomScope, + pins: Rc, + #[cfg(test)] + available: BTreeSet, + #[cfg(test)] + next: u32, + sweep_at: usize, + reported_pin_releases: Cell, +} + +pub(super) struct PinnedAtoms { + atoms: Vec, + pins: Rc, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct ReclaimedStyleAtom { + pub raw: usize, + pub atom: StyleAtomID, +} + +#[derive(Default)] +struct AtomPins { + counts: RefCell>, + releases: Cell, +} + +pub(super) struct AtomSweepDecision { + pub should_sweep: bool, + pub skipped_pin_releases: u64, +} + +pub(super) const PIN_RELEASES_PER_SWEEP: u64 = 256; + +impl Drop for PinnedAtoms { + fn drop(&mut self) { + let mut pinned = self.pins.counts.borrow_mut(); + for atom in &self.atoms { + let count = pinned.get_mut(atom).expect("a pinned atom must have a live count"); + *count = count.checked_sub(1).expect("pinned atom count underflow"); + if *count == 0 { + pinned.remove(atom); + } + } + if !self.atoms.is_empty() { + self.pins.releases.set( + self.pins + .releases + .get() + .checked_add(1) + .expect("atom pin release count overflow"), + ); + } + } +} + +impl DocumentAtoms { + pub(super) fn for_live_engine() -> Self { + Self { + raw: HashMap::new(), + cpp_memoized_raws: HashSet::new(), + qualified: HashMap::new(), + #[cfg(test)] + scope: AtomScope::Document, + #[cfg(not(test))] + scope: AtomScope::Process(RawAtomLifetime::RetainedFlyString), + pins: Rc::new(AtomPins::default()), + #[cfg(test)] + available: BTreeSet::new(), + #[cfg(test)] + next: 0, + sweep_at: 256, + reported_pin_releases: Cell::new(0), + } + } + + pub(super) fn for_replay() -> Self { + Self { + raw: HashMap::new(), + cpp_memoized_raws: HashSet::new(), + qualified: HashMap::new(), + scope: AtomScope::Process(RawAtomLifetime::OpaqueReplayToken), + pins: Rc::new(AtomPins::default()), + #[cfg(test)] + available: BTreeSet::new(), + #[cfg(test)] + next: 0, + sweep_at: 256, + reported_pin_releases: Cell::new(0), + } + } + + pub(super) fn intern_raw(&mut self, raw: usize) -> StyleAtomID { + if let Some(&atom) = self.raw.get(&raw) { + return atom; + } + let atom = match self.scope { + #[cfg(test)] + AtomScope::Document => self.allocate_document_atom(), + AtomScope::Process(lifetime) => global_atoms() + .lock() + .expect("process-global style atom lock is poisoned") + .acquire_raw(raw, lifetime), + }; + self.raw.insert(raw, atom); + atom + } + + pub(super) fn intern_cpp_raw(&mut self, raw: usize) -> StyleAtomID { + self.cpp_memoized_raws.insert(raw); + self.intern_raw(raw) + } + + pub(super) fn intern_qualified(&mut self, namespace: StyleAtomID, name: StyleAtomID) -> StyleAtomID { + let key = (namespace.0, name.0); + if let Some(&atom) = self.qualified.get(&key) { + return atom; + } + let atom = match self.scope { + #[cfg(test)] + AtomScope::Document => self.allocate_document_atom(), + AtomScope::Process(_) => global_atoms() + .lock() + .expect("process-global style atom lock is poisoned") + .acquire_qualified(namespace, name), + }; + self.qualified.insert(key, atom); + atom + } + + #[cfg(test)] + fn allocate_document_atom(&mut self) -> StyleAtomID { + if let Some(atom) = self.available.pop_first() { + return StyleAtomID(atom); + } + self.next = self.next.checked_add(1).expect("document style atom space exhausted"); + StyleAtomID(self.next) + } + + pub(super) fn pin(&self, atoms: impl IntoIterator) -> PinnedAtoms { + let atoms = atoms.into_iter().filter(|atom| !atom.is_none()).collect::>(); + let mut pinned = self.pins.counts.borrow_mut(); + for &atom in &atoms { + *pinned.entry(atom).or_default() += 1; + } + drop(pinned); + PinnedAtoms { + atoms, + pins: Rc::clone(&self.pins), + } + } + + pub(super) fn sweep_decision(&self) -> AtomSweepDecision { + let growth_requires_sweep = self.raw.len() + self.qualified.len() >= self.sweep_at; + let pin_releases = self.pins.releases.get(); + let pin_releases_require_sweep = pin_releases >= PIN_RELEASES_PER_SWEEP; + let skipped_pin_releases = if growth_requires_sweep || pin_releases_require_sweep { + 0 + } else { + pin_releases + .checked_sub(self.reported_pin_releases.get()) + .expect("reported atom pin releases exceed releases") + }; + self.reported_pin_releases.set(pin_releases); + AtomSweepDecision { + should_sweep: growth_requires_sweep || pin_releases_require_sweep, + skipped_pin_releases, + } + } + + /// Add transient pins and the raw components of every live qualified name. + pub(super) fn mark_sweep_dependencies(&self, live: &mut HashSet) + where + S: BuildHasher, + { + live.extend(self.pins.counts.borrow().keys().copied()); + for (&(namespace, name), &qualified) in &self.qualified { + if live.contains(&qualified) { + if namespace != 0 { + live.insert(StyleAtomID(namespace)); + } + if name != 0 { + live.insert(StyleAtomID(name)); + } + } + } + } + + /// Return atoms not owned by semantic state after all derived dependencies have been marked. + /// Derived atom-keyed catalogs must forget these identities before finish_sweep makes their + /// integers available for reuse. + pub(super) fn reclaimable_for_sweep(&self, live: &HashSet) -> Vec + where + S: BuildHasher, + { + let mut reclaimable = self + .raw + .values() + .chain(self.qualified.values()) + .copied() + .filter(|atom| !live.contains(atom)) + .collect::>(); + reclaimable.sort_unstable_by_key(|atom| atom.0); + reclaimable + } + + pub(super) fn finish_sweep(&mut self, reclaimable: &[StyleAtomID]) -> Vec { + let reclaimable = reclaimable.iter().copied().collect::>(); + let mut raw = Vec::new(); + self.raw.retain(|&identity, &mut atom| { + if !reclaimable.contains(&atom) { + return true; + } + raw.push((identity, atom)); + false + }); + let mut qualified = Vec::new(); + self.qualified.retain(|&key, &mut atom| { + if !reclaimable.contains(&atom) { + return true; + } + qualified.push((key, atom)); + false + }); + + match self.scope { + #[cfg(test)] + AtomScope::Document => { + self.available.extend(reclaimable.iter().map(|atom| atom.0)); + } + AtomScope::Process(_) => { + let mut global = global_atoms() + .lock() + .expect("process-global style atom lock is poisoned"); + for &(key, atom) in &qualified { + global.release_qualified(key, atom); + } + for &(identity, atom) in &raw { + global.release_raw(identity, atom); + } + } + } + + self.sweep_at = self.raw.len() + self.qualified.len() + 256; + self.pins.releases.set(0); + self.reported_pin_releases.set(0); + let mut reclaimed = raw + .into_iter() + .map(|(raw, atom)| ReclaimedStyleAtom { + raw: if self.cpp_memoized_raws.remove(&raw) { raw } else { 0 }, + atom, + }) + .chain( + qualified + .into_iter() + .map(|(_, atom)| ReclaimedStyleAtom { raw: 0, atom }), + ) + .collect::>(); + reclaimed.sort_unstable_by_key(|entry| entry.atom.0); + reclaimed + } + + #[cfg(feature = "style-recording")] + pub(super) fn raw(&self) -> &HashMap { + &self.raw + } + + #[cfg(feature = "style-recording")] + pub(super) fn qualified(&self) -> &HashMap<(u32, u32), StyleAtomID> { + &self.qualified + } +} + +impl Drop for DocumentAtoms { + fn drop(&mut self) { + if !matches!(self.scope, AtomScope::Process(_)) { + return; + } + let mut global = global_atoms() + .lock() + .expect("process-global style atom lock is poisoned"); + for (&key, &atom) in &self.qualified { + global.release_qualified(key, atom); + } + for (&raw, &atom) in &self.raw { + global.release_raw(raw, atom); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + static GLOBAL_ATOM_TEST_LOCK: Mutex<()> = Mutex::new(()); + + fn prepare_sweep(atoms: &DocumentAtoms, live: &mut HashSet) -> Vec { + atoms.mark_sweep_dependencies(live); + atoms.reclaimable_for_sweep(live) + } + + #[test] + fn synthetic_text_keys_do_not_retain_fly_strings() { + assert_eq!( + RawAtomLifetime::RetainedFlyString.for_raw(TEXT_KEY_FLOOR.saturating_sub(1)), + RawAtomLifetime::RetainedFlyString + ); + assert_eq!( + RawAtomLifetime::RetainedFlyString.for_raw(TEXT_KEY_FLOOR), + RawAtomLifetime::SyntheticTextKey + ); + } + + #[test] + fn process_atoms_share_and_recycle_after_the_last_document() { + let _test_lock = GLOBAL_ATOM_TEST_LOCK.lock().unwrap(); + let first_atom; + { + let mut first = DocumentAtoms::for_replay(); + let mut second = DocumentAtoms::for_replay(); + first_atom = first.intern_raw(0x1234); + assert_eq!(second.intern_raw(0x1234), first_atom); + assert_ne!(second.intern_raw(0x5678), first_atom); + } + let mut later = DocumentAtoms::for_replay(); + assert_eq!(later.intern_raw(0x9abc), first_atom); + } + + #[test] + fn process_atoms_wait_for_every_raw_and_qualified_owner() { + let _test_lock = GLOBAL_ATOM_TEST_LOCK.lock().unwrap(); + let mut first = DocumentAtoms::for_replay(); + let mut second = DocumentAtoms::for_replay(); + let namespace = first.intern_raw(0x1000); + let name = first.intern_raw(0x2000); + let qualified = first.intern_qualified(namespace, name); + assert_eq!(second.intern_raw(0x1000), namespace); + assert_eq!(second.intern_raw(0x2000), name); + assert_eq!(second.intern_qualified(namespace, name), qualified); + + let first_reclaimable = prepare_sweep(&first, &mut HashSet::new()); + first.finish_sweep(&first_reclaimable); + assert_eq!(second.intern_raw(0x1000), namespace); + assert_eq!(second.intern_qualified(namespace, name), qualified); + + let second_reclaimable = prepare_sweep(&second, &mut HashSet::new()); + second.finish_sweep(&second_reclaimable); + let mut later = DocumentAtoms::for_replay(); + assert_eq!(later.intern_raw(0x3000), namespace); + assert_eq!(later.intern_raw(0x4000), name); + assert_eq!(later.intern_qualified(namespace, name), qualified); + } + + #[test] + fn qualified_atoms_share_the_global_identity_space() { + let _test_lock = GLOBAL_ATOM_TEST_LOCK.lock().unwrap(); + let mut first = DocumentAtoms::for_replay(); + let mut second = DocumentAtoms::for_replay(); + let namespace = first.intern_raw(0x1000); + let name = first.intern_raw(0x2000); + assert_eq!(second.intern_raw(0x1000), namespace); + assert_eq!(second.intern_raw(0x2000), name); + assert_eq!( + first.intern_qualified(namespace, name), + second.intern_qualified(namespace, name) + ); + } + + #[test] + fn qualified_atoms_keep_their_component_atoms_live() { + let mut atoms = DocumentAtoms::for_live_engine(); + let namespace = atoms.intern_raw(0x1000); + let name = atoms.intern_raw(0x2000); + let qualified = atoms.intern_qualified(namespace, name); + let mut live = HashSet::from([qualified]); + assert!(prepare_sweep(&atoms, &mut live).is_empty()); + assert_eq!(live, HashSet::from([namespace, name, qualified])); + } + + #[test] + fn pins_delay_reclamation_until_their_owner_is_destroyed() { + let mut atoms = DocumentAtoms::for_live_engine(); + let atom = atoms.intern_cpp_raw(0x1000); + let pin = atoms.pin([atom]); + assert!(prepare_sweep(&atoms, &mut HashSet::new()).is_empty()); + drop(pin); + let decision = atoms.sweep_decision(); + assert!(!decision.should_sweep); + assert_eq!(decision.skipped_pin_releases, 1); + assert_eq!(atoms.sweep_decision().skipped_pin_releases, 0); + for _ in 1..PIN_RELEASES_PER_SWEEP { + drop(atoms.pin([atom])); + } + assert!(atoms.sweep_decision().should_sweep); + let reclaimable = prepare_sweep(&atoms, &mut HashSet::new()); + assert_eq!(reclaimable, [atom]); + assert_eq!( + atoms.finish_sweep(&reclaimable), + [ReclaimedStyleAtom { raw: 0x1000, atom }] + ); + } + + #[test] + fn document_atoms_reuse_reclaimed_identities_in_sorted_order() { + let mut atoms = DocumentAtoms::for_live_engine(); + let first = atoms.intern_cpp_raw(0x1000); + let second = atoms.intern_raw(0x2000); + let third = atoms.intern_cpp_raw(0x3000); + let reclaimable = prepare_sweep(&atoms, &mut HashSet::from([second])); + assert_eq!(reclaimable, [first, third]); + assert_eq!( + atoms.finish_sweep(&reclaimable), + [ + ReclaimedStyleAtom { + raw: 0x1000, + atom: first, + }, + ReclaimedStyleAtom { + raw: 0x3000, + atom: third, + }, + ] + ); + assert_eq!(atoms.intern_raw(0x4000), first); + assert_eq!(atoms.intern_raw(0x5000), third); + } + + #[test] + fn compiler_only_atoms_do_not_claim_a_cpp_memo_entry() { + let mut atoms = DocumentAtoms::for_live_engine(); + let atom = atoms.intern_raw(0x1000); + let reclaimable = prepare_sweep(&atoms, &mut HashSet::new()); + assert_eq!(atoms.finish_sweep(&reclaimable), [ReclaimedStyleAtom { raw: 0, atom }]); + } + + #[test] + fn repeated_churn_keeps_the_document_identity_space_bounded() { + let mut atoms = DocumentAtoms::for_live_engine(); + let mut highest = 0; + for round in 0..8 { + for offset in 0..256 { + highest = highest.max(atoms.intern_raw(0x1000 + round * 256 + offset).0); + } + let reclaimable = prepare_sweep(&atoms, &mut HashSet::new()); + assert_eq!(reclaimable.len(), 256); + atoms.finish_sweep(&reclaimable); + } + assert_eq!(highest, 256); + } +} diff --git a/Libraries/LibWeb/Rust/src/css/style/batch_matcher.rs b/Libraries/LibWeb/Rust/src/css/style/batch_matcher.rs index c462bace3006a..dd475c0e662d4 100644 --- a/Libraries/LibWeb/Rust/src/css/style/batch_matcher.rs +++ b/Libraries/LibWeb/Rust/src/css/style/batch_matcher.rs @@ -24,7 +24,6 @@ use super::index::DispatchEntry; use super::index::DispatchKey; use super::index::ParentDispatchFacts; use super::index::RuleDispatch; -use super::index::StyleAtomID; use super::index::StyleNodeFacts; use super::instrumentation::Counter; use super::instrumentation::Counters; @@ -36,6 +35,7 @@ use super::prefix::PrefixStates; use super::prefix::PrefixTransitionGap; use super::prefix::PrefixTransitionLookup; use super::program::CascadeOrigin; +use super::program::EntryID; use super::program::RuleID; use super::program::SelectorProgramID; use super::program::StyleSheetProgram; @@ -87,6 +87,7 @@ pub struct RuleMatch { #[derive(Default)] pub struct RuleMatches { matches: Vec, + selector_truth: Option>, charged_bytes: u64, } @@ -126,11 +127,50 @@ impl RuleMatches { pub fn clear(&mut self) { self.matches.clear(); + if let Some(truth) = self.selector_truth.as_mut() { + truth.clear(); + } + } + + pub(super) fn enable_selector_truth(&mut self) { + self.selector_truth.get_or_insert_default(); + } + + fn selector_truth_len(&self) -> usize { + self.selector_truth.as_ref().map_or(0, Vec::len) + } + + fn record_selector_truth(&mut self, entry: EntryID, tree_scope: TreeScopeID, scope_proximity: u32) { + if let Some(truth) = self.selector_truth.as_mut() { + truth.push(super::SelectorTruth { + entry, + tree_scope, + scope_proximity, + }); + } + } + + fn truncate(&mut self, matches: usize, selector_truth: usize) { + self.matches.truncate(matches); + if let Some(truth) = self.selector_truth.as_mut() { + truth.truncate(selector_truth); + } + } + + pub(super) fn prepared_selector_truth(&self) -> Option> { + let mut truth = self.selector_truth.clone()?; + truth.sort_unstable(); + truth.dedup(); + Some(truth) } /// Reconcile the scratch charge after a pass. pub fn settle_memory(&mut self, memory: &mut MemoryController) { - let current = (self.matches.capacity() * size_of::()) as u64; + let current = (self.matches.capacity() * size_of::() + + self + .selector_truth + .as_ref() + .map_or(0, |truth| truth.capacity() * size_of::())) as u64; if current > self.charged_bytes { memory.reserve_required(MemoryCategory::BatchScratch, current - self.charged_bytes); } else if self.charged_bytes > current { @@ -142,15 +182,16 @@ impl RuleMatches { #[must_use] pub fn take(&mut self, memory: &mut MemoryController) -> Vec { self.settle_memory(memory); - memory.release(MemoryCategory::BatchScratch, self.charged_bytes); - self.charged_bytes = 0; - std::mem::take(&mut self.matches) + let matches = std::mem::take(&mut self.matches); + self.settle_memory(memory); + matches } pub fn release(&mut self, memory: &mut MemoryController) { memory.release(MemoryCategory::BatchScratch, self.charged_bytes); self.charged_bytes = 0; self.matches = Vec::new(); + self.selector_truth = None; } } @@ -200,10 +241,10 @@ pub(super) fn scope_dispatch_shape_and_rules( for_each_scope_rule(program, tree_scope, take, |sheet, rule, selector_program| { shape.push((selector_program, program.sheet_origin(sheet) == CascadeOrigin::Author)); let compiled = programs.get(selector_program); - for (index, entry) in compiled.entries().iter().enumerate() { - let key = compiled.dispatch_key(entry); - let copies = if key == DispatchKey::Universal { - let mut branch_keys = compiled.subject_dispatch_keys(index).to_vec(); + for index in 0..compiled.entries().len() { + let metadata = compiled.dispatch_metadata(index); + let copies = if metadata.key == DispatchKey::Universal { + let mut branch_keys = metadata.subject_dispatch_keys().to_vec(); branch_keys.sort_unstable(); branch_keys.dedup(); branch_keys.len().max(1) @@ -259,28 +300,24 @@ pub(super) fn insert_scope_rule( author: bool, ) { let compiled = programs.get(selector_program); - for (index, entry) in compiled.entries().iter().enumerate() { - let key = compiled.dispatch_key(entry); - let bloom_of = |keys: &[DispatchKey]| { - keys.iter() - .copied() - .fold(0_u64, |bloom, key| bloom | super::index::dispatch_bloom_bit(key)) - }; - let subject_dispatch = compiled.subject_dispatch_keys(index); - let required_attribute_value = match key { - DispatchKey::AttributeName(name) => compiled.required_attribute_value(entry, name), - _ => StyleAtomID::NONE, - }; + for index in 0..compiled.entries().len() { + let metadata = compiled.dispatch_metadata(index); + let key = metadata.key; + let subject_dispatch = metadata.subject_dispatch_keys(); let template = super::index::DispatchEntry { + identity: programs.entry_id( + selector_program, + u32::try_from(index).expect("selector entry space exhausted"), + ), rule, program: selector_program, entry: u32::try_from(index).expect("selector entry space exhausted"), cascade_order: 0, - required_attribute_value, - required_parent: compiled.subject_parent_dispatch_key(index), - required_ancestor: compiled.subject_ancestor_dispatch_key(index), + required_attribute_value: metadata.required_attribute_value, + required_parent: metadata.required_parent, + required_ancestor: metadata.required_ancestor, required_ancestor_index: None, - required_subject_bloom: bloom_of(compiled.subject_required_keys(index)), + required_subject_bloom: metadata.required_subject_bloom, prefix_matched: false, multi_key: false, }; @@ -300,21 +337,14 @@ pub(super) fn insert_scope_rule( }; if branch_keys.is_empty() { let dispatch_entry = dispatch.insert(key, template); - if let Some(chain) = compiled.unified_chain(entry) { + if let Some(chain) = metadata.prefix_chain() { // NB: Structural truth bits are admitted for author rules only. The // convergence walk consumes only author routes, so a user-agent // positional chain would put its tests into every document's // automaton, taxing each transition and widening every tree // flush's re-compare frontier, without any route ever being // subsumed in return. - dispatch.add_prefix_entry( - programs, - selector_program, - u32::try_from(index).expect("selector entry space exhausted"), - &chain, - dispatch_entry, - author, - ); + dispatch.add_prefix_entry(programs, selector_program, chain, dispatch_entry, author); } } else { // NB: A branch copy carries no required attribute value: a disjunction branch's @@ -419,6 +449,11 @@ impl AncestorRequirements { // Rows normally arrive in tree order. Following parents until an already-built row also // keeps this exact for a differently ordered batch without making deep DOM trees recurse. for start in 0..row_count { + if !facts.has_row(u32::try_from(start).expect("fact batch row space exhausted")) { + computed[start] = true; + unbounded[start] = true; + continue; + } if computed[start] { continue; } @@ -713,6 +748,7 @@ fn append_matched_entry( counters: &mut Counters, count_emission: CountRuleMatchEmission, ) { + out.record_selector_truth(candidate.identity, scope, scope_proximity); let candidate_index = candidate.cascade_order as usize; // A selector list can match through several entries. Only its greatest matching specificity // contributes, independently for the element and each pseudo-element target. @@ -775,31 +811,76 @@ pub(super) fn append_prefix_matches( dispatch: &RuleDispatch, program: &StyleSheetProgram, programs: &SelectorPrograms, - matches: &[super::index::DispatchEntryID], + matches: &[EntryID], counters: &mut Counters, count_emission: CountRuleMatchEmission, ) { let mut completed = None; for &matched in matches { - let candidate = dispatch.entry(matched); - if !program.rule_can_decide(candidate.rule) { - continue; + for candidate in dispatch.entries_for_identity(matched) { + if !candidate.prefix_matched { + continue; + } + if !program.rule_can_decide(candidate.rule) { + continue; + } + let compiled = programs.get(candidate.program); + let entry = &compiled.entries()[candidate.entry as usize]; + append_matched_entry( + out, + 0, + node, + scope, + candidate, + compiled, + entry, + u32::MAX, + &mut completed, + counters, + count_emission, + ); + } + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn append_selector_truth_matches( + out: &mut RuleMatches, + node: StyleNodeID, + program: &StyleSheetProgram, + programs: &SelectorPrograms, + dispatch: &RuleDispatch, + truth: &[super::SelectorTruth], + counters: &mut Counters, +) { + let mut completed = None; + for &matched in truth { + let mut previous = None; + for candidate in dispatch.entries_for_identity(matched.entry) { + let candidate_key = (candidate.rule, candidate.program, candidate.entry); + if previous == Some(candidate_key) { + continue; + } + previous = Some(candidate_key); + if !program.rule_can_decide(candidate.rule) { + continue; + } + let compiled = programs.get(candidate.program); + let entry = &compiled.entries()[candidate.entry as usize]; + append_matched_entry( + out, + 0, + node, + matched.tree_scope, + candidate, + compiled, + entry, + matched.scope_proximity, + &mut completed, + counters, + CountRuleMatchEmission::No, + ); } - let compiled = programs.get(candidate.program); - let entry = &compiled.entries()[candidate.entry as usize]; - append_matched_entry( - out, - 0, - node, - scope, - candidate, - compiled, - entry, - u32::MAX, - &mut completed, - counters, - count_emission, - ); } } @@ -978,6 +1059,7 @@ impl<'a> BatchMatcher<'a> { counters: &mut Counters, ) -> Result<(), Incomplete> { let start = out.matches.len(); + let selector_truth_start = out.selector_truth_len(); let is_document_root = self.tree.parent(node).is_none(); for &(rule, program) in rules { if !self.program.rule_can_decide(rule) || self.program.rule_version(rule).selector_program != Some(program) @@ -1018,7 +1100,7 @@ impl<'a> BatchMatcher<'a> { let matches = match evaluator.matches_entry_for_program(program, compiled, entry, node, counters) { Ok(matches) => matches, Err(incomplete) => { - out.matches.truncate(start); + out.truncate(start, selector_truth_start); return Err(incomplete); } }; @@ -1028,7 +1110,7 @@ impl<'a> BatchMatcher<'a> { let scope_proximity = match evaluator.scope_proximity_of(compiled, entry, node, counters) { Ok(scope_proximity) => scope_proximity, Err(incomplete) => { - out.matches.truncate(start); + out.truncate(start, selector_truth_start); return Err(incomplete); } }; @@ -1043,6 +1125,11 @@ impl<'a> BatchMatcher<'a> { specificity: entry.specificity, scope_proximity, }; + out.record_selector_truth( + self.programs.entry_id(program, entry_index), + self.scope, + scope_proximity, + ); if let Some(existing) = out.matches[start..] .iter_mut() .find(|existing| existing.rule == rule && existing.pseudo_element == entry.pseudo_element) @@ -1070,6 +1157,7 @@ impl<'a> BatchMatcher<'a> { counters: &mut Counters, ) -> Result<(), Incomplete> { let start = out.matches.len(); + let selector_truth_start = out.selector_truth_len(); let mut dispatch_workspace = DispatchCandidateWorkspace::default(); let mut prefix_states = PrefixStates::new(self.facts.row_count()); for node in self.tree.preorder(root) { @@ -1085,7 +1173,7 @@ impl<'a> BatchMatcher<'a> { deferred_prefix_matches: None, }, ) { - out.matches.truncate(start); + out.truncate(start, selector_truth_start); return Err(incomplete); } } @@ -1163,6 +1251,7 @@ impl<'a> BatchMatcher<'a> { return self.match_filtered_rules_directly(node, row, rules, &evaluator, out, counters); } let start = out.matches.len(); + let selector_truth_start = out.selector_truth_len(); let mut used_prefixes = false; let mut cascade_pruning_blocked = false; if !self.node_is_slotted_in && !self.node_is_a_part_exposed_here && !self.node_is_the_host_of_this_tree { @@ -1215,41 +1304,50 @@ impl<'a> BatchMatcher<'a> { }; if let Some((states, prefix_matches)) = prefix_matches { used_prefixes = true; + for &matched in states.matches_in(prefix_matches) { + out.record_selector_truth(matched, self.scope, u32::MAX); + } cascade_pruning_blocked = self.cascade_only - && states - .matches_in(prefix_matches) - .iter() - .any(|&matched| self.dispatch.cascade_pruning_blocker(self.dispatch.entry(matched))); + && states.matches_in(prefix_matches).iter().any(|&matched| { + self.dispatch + .entries_for_identity(matched) + .filter(|candidate| candidate.prefix_matched) + .any(|candidate| self.dispatch.cascade_pruning_blocker(candidate)) + }); if let Some(deferred) = deferred_prefix_matches { *deferred = Some(prefix_matches); } else { for &matched in states.matches_in(prefix_matches) { - let candidate = self.dispatch.entry(matched); - if !self.rule_filter_admits(candidate.rule, candidate.program) { - continue; - } - if !self.program.rule_can_decide(candidate.rule) { - continue; - } - let candidate_index = candidate.cascade_order as usize; - if completed.as_deref().is_some_and(|completed| completed[candidate_index]) { - continue; + for candidate in self.dispatch.entries_for_identity(matched) { + if !candidate.prefix_matched { + continue; + } + if !self.rule_filter_admits(candidate.rule, candidate.program) { + continue; + } + if !self.program.rule_can_decide(candidate.rule) { + continue; + } + let candidate_index = candidate.cascade_order as usize; + if completed.as_deref().is_some_and(|completed| completed[candidate_index]) { + continue; + } + let compiled = self.programs.get(candidate.program); + let entry = &compiled.entries()[candidate.entry as usize]; + append_matched_entry( + out, + start, + node, + self.scope, + candidate, + compiled, + entry, + u32::MAX, + &mut completed, + counters, + self.count_rule_match_emission(), + ); } - let compiled = self.programs.get(candidate.program); - let entry = &compiled.entries()[candidate.entry as usize]; - append_matched_entry( - out, - start, - node, - self.scope, - candidate, - compiled, - entry, - u32::MAX, - &mut completed, - counters, - self.count_rule_match_emission(), - ); } } } @@ -1442,7 +1540,7 @@ impl<'a> BatchMatcher<'a> { } continue; } - out.matches.truncate(start); + out.truncate(start, selector_truth_start); return Err(incomplete); } } @@ -1457,7 +1555,7 @@ impl<'a> BatchMatcher<'a> { } continue; } - out.matches.truncate(start); + out.truncate(start, selector_truth_start); return Err(incomplete); } }; @@ -1493,7 +1591,7 @@ impl<'a> BatchMatcher<'a> { } if let Some(incomplete) = first_incomplete { if completed.is_none() { - out.matches.truncate(start); + out.truncate(start, selector_truth_start); } return Err(incomplete); } @@ -1507,6 +1605,8 @@ mod tests { use super::super::index::StateSet; use super::super::index::StyleAtomID; use super::super::memory::DeviceClass; + use super::super::planning::SelectorTruthChanges; + use super::super::planning::record_match_set_difference; use super::super::program::CascadeOrigin; use super::super::program::RuleKind; use super::super::program::StyleSheetObjectID; @@ -1658,6 +1758,76 @@ mod tests { assert_eq!(document.matched_nodes(&matches, descendant), vec![2]); } + #[test] + fn shared_selector_entries_join_every_rule_after_prefix_matching() { + let mut document = Document::new(); + let mut builder = SelectorProgramBuilder::new(); + class_rule(CLASS_ITEM)(&mut builder); + let selector_program = document.programs.add(builder.finish()); + let mut rules = Vec::new(); + for _ in 0..2 { + let rule = document.program.append_rule(document.sheet(), None, RuleKind::Style); + let mut version = document.program.rule_version(rule); + version.selector_program = Some(selector_program); + document.program.replace_rule_version(rule, version); + rules.push(rule); + } + + let matches = document.run(); + assert_eq!(document.matched_nodes(&matches, rules[0]), vec![1, 3]); + assert_eq!(document.matched_nodes(&matches, rules[1]), vec![1, 3]); + assert_eq!(document.counters.get(Counter::CandidateChecks), 0); + } + + #[test] + fn shared_prefix_identity_emits_only_rows_admitted_to_the_automaton() { + let mut document = Document::new(); + let user_agent_sheet = document + .program + .add_sheet(StyleSheetObjectID(2), CascadeOrigin::UserAgent); + document.program.attach_sheet(user_agent_sheet, TreeScopeID::DOCUMENT); + + let mut builder = SelectorProgramBuilder::new(); + let any = builder.push_feature(FeatureTest::AnyElement); + let first_of_type = builder.push(SelectorOp::NthPosition(NthPosition { + step: 0, + offset: 1, + from_end: false, + of_selector: None, + of_type: true, + })); + let root = builder.push_compound(&[any, first_of_type]); + builder.push_entry(root); + let selector_program = document.programs.add(builder.finish()); + + let author_sheet = document.sheet(); + let mut add_rule = |sheet| { + let rule = document.program.append_rule(sheet, None, RuleKind::Style); + let mut version = document.program.rule_version(rule); + version.selector_program = Some(selector_program); + document.program.replace_rule_version(rule, version); + rule + }; + let user_agent_rule = add_rule(user_agent_sheet); + let author_rule = add_rule(author_sheet); + + let matches = document.run(); + assert_eq!(document.matched_nodes(&matches, user_agent_rule), vec![0, 1, 2]); + assert_eq!(document.matched_nodes(&matches, author_rule), vec![0, 1, 2]); + + let dispatch = build_scope_dispatch(&document.program, &document.programs, TreeScopeID::DOCUMENT); + let entry = document.programs.entry_id(selector_program, 0); + let rows: Vec<_> = dispatch.entries_for_identity(entry).collect(); + assert_eq!(rows.len(), 2); + assert_eq!(rows.iter().filter(|row| row.prefix_matched).count(), 1); + + let mut changes = SelectorTruthChanges::default(); + record_match_set_difference(&mut changes, true, document.nodes[0], &[], &[entry], &dispatch); + let mut changed_rules: Vec<_> = changes.deltas.as_slice().iter().map(|delta| delta.rule).collect(); + changed_rules.sort_unstable(); + assert_eq!(changed_rules, vec![user_agent_rule, author_rule]); + } + #[test] fn prefix_states_keep_descendants_but_expire_children() { let mut document = Document::new(); @@ -2286,9 +2456,6 @@ mod tests { StateSet::default(), &[], &[AttributeFact { - folded_local: StyleAtomID::NONE, - folded_name: StyleAtomID(30), - local: StyleAtomID::NONE, name: StyleAtomID(30), value: StyleAtomID(31), text_offset: u32::MAX, diff --git a/Libraries/LibWeb/Rust/src/css/style/bridge.rs b/Libraries/LibWeb/Rust/src/css/style/bridge.rs index 8db7add7bb271..2416ad291ae03 100644 --- a/Libraries/LibWeb/Rust/src/css/style/bridge.rs +++ b/Libraries/LibWeb/Rust/src/css/style/bridge.rs @@ -30,14 +30,16 @@ use crate::abort_on_panic as abort_on_boundary_panic; use crate::css::selector::CompiledSelector; use crate::css::selector::RustSelector; +use super::HashSet; +use super::PinnedAtoms; use super::StyleEngine; use super::batch_matcher::RuleMatch; use super::cascade::CascadeOperator; use super::compiler::ImplicitScopeRoot; use super::compiler::NamespaceScope; use super::compiler::ScopeChain; -use super::index::FeatureKey; use super::index::FeatureValue; +use super::index::LocalFeatureKey; use super::index::StyleAtomID; use super::memory::DeviceClass; #[cfg(feature = "style-recording")] @@ -105,9 +107,18 @@ pub struct FfiStyleDelta { #[derive(Default)] pub(super) struct FfiStyleTransactionOutput { scoped: bool, + style_atoms_swept: bool, transaction_version: u64, program_version: u64, answers: Vec, + reclaimed_style_atoms: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct FfiReclaimedStyleAtom { + pub raw: usize, + pub atom: u32, } #[derive(Clone, Copy)] @@ -117,7 +128,10 @@ pub struct FfiStyleTransactionView { pub program_version: u64, pub answers: *const FfiStyleDelta, pub count: usize, + pub reclaimed_style_atoms: *const FfiReclaimedStyleAtom, + pub reclaimed_style_atom_count: usize, pub scoped: bool, + pub style_atoms_swept: bool, } #[derive(Clone, Copy)] @@ -159,7 +173,10 @@ impl Default for FfiStyleTransactionView { program_version: 0, answers: std::ptr::null(), count: 0, + reclaimed_style_atoms: std::ptr::null(), + reclaimed_style_atom_count: 0, scoped: false, + style_atoms_swept: false, } } } @@ -187,6 +204,11 @@ fn write_style_transaction_outputs( payload.write_u8(answer.gap as u8); } } + payload.write_bool(output.style_atoms_swept); + payload.write_length(output.reclaimed_style_atoms.len()); + for reclaimed in &output.reclaimed_style_atoms { + payload.write_u32(reclaimed.atom); + } } /// One final base-style assignment. Zero names the absent side of an insertion or removal. @@ -248,6 +270,7 @@ pub struct FfiStyleRecordView { struct SelectorQuery { program: SelectorProgram, + _atoms: PinnedAtoms, _memory: MemoryLease, } @@ -353,6 +376,22 @@ pub struct FfiTreeDelta { pub new_relations: FfiTreeRelations, } +/// Selector-visible facts which exist when one style node first joins the tree. Variable-width +/// custom states occupy one shared atom column and are named by this row's offset and count. +#[derive(Clone, Copy)] +#[repr(C)] +pub struct FfiElementArrival { + pub node: u32, + pub namespace_atom: u32, + pub language_atom: u32, + pub directionality_atom: u32, + pub custom_state_offset: u32, + pub custom_state_count: u32, + pub heading_level: u8, + pub is_slot: bool, + pub reserved: u16, +} + /// Which local fact a feature delta describes. #[derive(Clone, Copy, PartialEq, Eq)] #[repr(u8)] @@ -466,10 +505,11 @@ pub struct FfiElementStyleInput { pub inherited_style_groups: u8, } -// SAFETY: the five transaction row types are pointer-free repr(C) with alignment four, and their +// SAFETY: the six transaction row types are pointer-free repr(C) with alignment four, and their // enum fields are recorded only from live FFI values, so raw bytes round-trip on the capturing // host (the RawRecord contract). unsafe impl super::record_replay::RawRecord for FfiTreeDelta {} +unsafe impl super::record_replay::RawRecord for FfiElementArrival {} unsafe impl super::record_replay::RawRecord for FfiLocalFeatureDelta {} unsafe impl super::record_replay::RawRecord for FfiStateDelta {} unsafe impl super::record_replay::RawRecord for FfiElementDeclarationDelta {} @@ -481,6 +521,10 @@ unsafe impl super::record_replay::RawRecord for FfiElementStyleInput {} pub struct FfiStyleInputTransaction { pub tree_deltas: *const FfiTreeDelta, pub tree_delta_count: usize, + pub element_arrivals: *const FfiElementArrival, + pub element_arrival_count: usize, + pub arrival_custom_state_atoms: *const u32, + pub arrival_custom_state_atom_count: usize, pub local_feature_deltas: *const FfiLocalFeatureDelta, pub local_feature_delta_count: usize, pub state_deltas: *const FfiStateDelta, @@ -527,14 +571,14 @@ impl FfiCascadeOrigin { } } -fn decode_feature_key(delta: &FfiLocalFeatureDelta) -> FeatureKey { +fn decode_feature_key(delta: &FfiLocalFeatureDelta) -> LocalFeatureKey { match delta.feature_kind { - FfiFeatureKind::TagName => FeatureKey::TagName, - FfiFeatureKind::FoldedTagName => FeatureKey::FoldedTagName, - FfiFeatureKind::Emptiness => FeatureKey::Emptiness, - FfiFeatureKind::Id => FeatureKey::Id, - FfiFeatureKind::Class => FeatureKey::Class(StyleAtomID(delta.name_atom)), - FfiFeatureKind::Attribute => FeatureKey::Attribute(StyleAtomID(delta.name_atom)), + FfiFeatureKind::TagName => LocalFeatureKey::TagName, + FfiFeatureKind::FoldedTagName => LocalFeatureKey::FoldedTagName, + FfiFeatureKind::Emptiness => LocalFeatureKey::Emptiness, + FfiFeatureKind::Id => LocalFeatureKey::Id, + FfiFeatureKind::Class => LocalFeatureKey::Class(StyleAtomID(delta.name_atom)), + FfiFeatureKind::Attribute => LocalFeatureKey::Attribute(StyleAtomID(delta.name_atom)), } } @@ -663,7 +707,8 @@ impl StyleEngine { } fn install_ffi_style_transaction_output(&mut self, output: FfiStyleTransactionOutput) { - let bytes = (output.answers.capacity() * size_of::()) as u64; + let bytes = (output.answers.capacity() * size_of::() + + output.reclaimed_style_atoms.capacity() * size_of::()) as u64; self.ffi_style_transaction_output = output; self.ffi_style_transaction_output_memory .resize_required_to(&mut self.memory, bytes); @@ -679,11 +724,13 @@ impl StyleEngine { pub fn apply_transaction_batch( &mut self, tree_deltas: &[FfiTreeDelta], + arrival_columns: (&[FfiElementArrival], &[u32]), local_feature_deltas: &[FfiLocalFeatureDelta], state_deltas: &[FfiStateDelta], element_declaration_deltas: &[FfiElementDeclarationDelta], element_style_inputs: &[FfiElementStyleInput], ) { + let (element_arrivals, arrival_custom_state_atoms) = arrival_columns; let largest_element_index = tree_deltas .iter() .filter_map(|delta| StyleNodeID::from_raw(delta.node)?.element_index()) @@ -747,6 +794,35 @@ impl StyleEngine { .unwrap_or(false) }; + if !element_arrivals.is_empty() { + for arrival in element_arrivals { + let Some(node) = StyleNodeID::from_raw(arrival.node) else { + debug_assert!(false, "an element arrival named an invalid style node"); + continue; + }; + let Some(custom_state_end) = arrival.custom_state_offset.checked_add(arrival.custom_state_count) else { + debug_assert!(false, "an element arrival custom-state range overflowed"); + continue; + }; + let Ok(custom_state_range) = usize::try_from(arrival.custom_state_offset) + .and_then(|start| usize::try_from(custom_state_end).map(|end| start..end)) + else { + debug_assert!(false, "an element arrival custom-state range exceeded usize"); + continue; + }; + let Some(custom_states) = arrival_custom_state_atoms.get(custom_state_range) else { + debug_assert!( + false, + "an element arrival named custom states outside the shared atom column" + ); + continue; + }; + let custom_states = custom_states.iter().copied().map(StyleAtomID).collect::>(); + self.record_element_arrival(node, arrival, &custom_states, node_is_arriving(node)); + } + self.settle_batched_inputs(); + } + for delta in local_feature_deltas { let Some(node) = StyleNodeID::from_raw(delta.node) else { continue; @@ -824,6 +900,20 @@ pub extern "C" fn style_engine_create(device_class: FfiDeviceClass) -> *mut c_vo }) } +/// Installs the C++ ownership callbacks used by live process-global raw atoms. +#[unsafe(no_mangle)] +pub extern "C" fn style_engine_install_raw_atom_callbacks( + retain: unsafe extern "C" fn(usize), + release: unsafe extern "C" fn(usize), +) { + super::atoms::install_raw_atom_callbacks(retain, release); +} + +/// Creates a replay engine whose atom keys are opaque capture tokens rather than live fly strings. +pub fn style_engine_create_for_replay(device_class: FfiDeviceClass) -> *mut c_void { + abort_on_panic(|| Box::into_raw(Box::new(StyleEngine::new_for_replay(device_class.decode()))).cast()) +} + /// Applies the memory policy used while producing a replay recording. /// /// # Safety @@ -930,6 +1020,13 @@ pub unsafe extern "C" fn style_engine_apply_transaction(engine: *mut c_void, tra let engine = unsafe { &mut *engine.cast::() }; // SAFETY: the caller vouches that each pointer covers its stated count for this call. let tree = unsafe { borrow(transaction.tree_deltas, transaction.tree_delta_count) }; + let arrivals = unsafe { borrow(transaction.element_arrivals, transaction.element_arrival_count) }; + let arrival_custom_state_atoms = unsafe { + borrow( + transaction.arrival_custom_state_atoms, + transaction.arrival_custom_state_atom_count, + ) + }; let features = unsafe { borrow(transaction.local_feature_deltas, transaction.local_feature_delta_count) }; let states = unsafe { borrow(transaction.state_deltas, transaction.state_delta_count) }; let declarations = unsafe { @@ -940,9 +1037,18 @@ pub unsafe extern "C" fn style_engine_apply_transaction(engine: *mut c_void, tra }; let element_style_inputs = unsafe { borrow(transaction.element_style_inputs, transaction.element_style_input_count) }; - engine.apply_transaction_batch(tree, features, states, declarations, element_style_inputs); + engine.apply_transaction_batch( + tree, + (arrivals, arrival_custom_state_atoms), + features, + states, + declarations, + element_style_inputs, + ); engine.record_boundary_call(EventKind::ApplyTransaction, |payload| { write_recording_tree_deltas(tree, payload); + payload.write_raw_slice(arrivals); + payload.write_u32_slice(arrival_custom_state_atoms); payload.write_raw_slice(features); write_recording_state_deltas(states, payload); payload.write_raw_slice(declarations); @@ -1268,9 +1374,6 @@ pub unsafe extern "C" fn style_engine_set_element_language( text_length: usize, ) { abort_on_panic(|| { - let Some(node) = StyleNodeID::from_raw(node) else { - return; - }; let engine = unsafe { &mut *engine.cast::() }; let text = match language != 0 && !text.is_null() { true => unsafe { std::slice::from_raw_parts(text, text_length) }, @@ -1281,14 +1384,17 @@ pub unsafe extern "C" fn style_engine_set_element_language( // once per language rather than once per element. engine.set_element_language_text(StyleAtomID(language), text); } - engine.set_element_language(node, StyleAtomID(language)); + if let Some(node) = StyleNodeID::from_raw(node) { + engine.set_element_language(node, StyleAtomID(language)); + } engine.record_boundary_call(EventKind::SetElementLanguage, |payload| { - payload.write_u32(node.raw()); + payload.write_u32(node); payload.write_u32(language); payload.write_u16_slice(text); }); }); } + /// # Safety /// `pointers` must be null or point to `count` live `RustSelector` pointers. /// How many scope roots and limits each enclosing `@scope` contributed, outermost first. @@ -1348,6 +1454,9 @@ pub unsafe extern "C" fn style_engine_compile_selector_query( let engine = unsafe { &mut *engine.cast::() }; let selectors = unsafe { borrow_selectors(selectors, count) }; let program = engine.compile_selector_query(&selectors); + let mut atoms = HashSet::default(); + program.collect_atoms(&mut atoms); + let atoms = engine.atoms.pin(atoms); engine.record_boundary_call(EventKind::SelectorQueryAtomMappings, |payload| { write_recording_atom_mappings(engine, payload); }); @@ -1358,6 +1467,7 @@ pub unsafe extern "C" fn style_engine_compile_selector_query( memory.resize_required_to(&mut engine.memory, bytes); Box::into_raw(Box::new(SelectorQuery { program, + _atoms: atoms, _memory: memory, })) .cast() @@ -2059,24 +2169,25 @@ pub unsafe extern "C" fn style_engine_publish_computed_groups( .as_ref() }; payload.write_bool(longhand_table.is_some()); - if let Some(table) = longhand_table { - let stored_values = table - .value_pointers() - .iter() - .enumerate() - .filter(|(_, value)| !value.is_null()) - .collect::>(); - payload.write_length(stored_values.len()); - for (index, &value) in stored_values { - payload.write_u16(crate::css::property_metadata::FIRST_LONGHAND_PROPERTY_ID + index as u16); - payload.write_u64(pointer_token(value)); - payload.write_u8(crate::css::style_value::style_value_dependency_flags(value.cast())); - } - let source_slots = table.source_slot_entries().collect::>(); - payload.write_length(source_slots.len()); - for (property, slot) in source_slots { - payload.write_u16(property); - payload.write_u32(slot); + if longhand_table.is_some() { + let (identity, canonical_values) = engine + .recording_computed_longhand_table(result.new_style_record) + .expect("a published style record must retain its longhand table"); + payload.write_u32(identity); + let record_definition = engine.recording_first_response(2, u64::from(identity)); + payload.write_bool(record_definition); + if record_definition { + let stored_values = canonical_values + .iter() + .enumerate() + .filter(|(_, value)| !value.is_null()) + .collect::>(); + payload.write_length(stored_values.len()); + for (index, &value) in stored_values { + payload.write_u16(crate::css::property_metadata::FIRST_LONGHAND_PROPERTY_ID + index as u16); + payload.write_u64(pointer_token(value)); + payload.write_u8(crate::css::style_value::style_value_dependency_flags(value.cast())); + } } } payload.write_u64(result.old_style_record); @@ -2394,7 +2505,7 @@ pub unsafe extern "C" fn style_engine_intern_atom(engine: *mut c_void, raw: usiz abort_on_panic(|| { let engine = unsafe { &mut *engine.cast::() }; let result = engine.intern_atom(raw).0; - let token = engine.recording_pointer_token(raw); + let token = engine.recording_atom_pointer_token(raw); engine.record_boundary_call(EventKind::InternAtom, |payload| { payload.write_u64(token.expect("an enabled recorder must tokenize the pointer")); payload.write_u32(result); @@ -2429,6 +2540,14 @@ pub unsafe extern "C" fn style_engine_take_style_transaction( output.program_version = program_version.0; output.answers.extend_from_slice(answers); }); + output.reclaimed_style_atoms = std::mem::take(&mut engine.reclaimed_style_atoms) + .into_iter() + .map(|reclaimed| FfiReclaimedStyleAtom { + raw: reclaimed.raw, + atom: reclaimed.atom.0, + }) + .collect(); + output.style_atoms_swept = std::mem::take(&mut engine.style_atoms_swept); if engine.recording_id().is_some() { engine.record_boundary_call(EventKind::StyleDeltaBatch, |payload| { payload.write_u32(root.raw()); @@ -2437,6 +2556,7 @@ pub unsafe extern "C" fn style_engine_take_style_transaction( payload.write_bytes(outputs.as_bytes()); payload.write_u64(outputs.stable_digest()); }); + engine.forget_recording_atom_mappings(output.reclaimed_style_atoms.iter().map(|reclaimed| reclaimed.atom)); } engine.install_ffi_style_transaction_output(output); let output = &engine.ffi_style_transaction_output; @@ -2445,10 +2565,36 @@ pub unsafe extern "C" fn style_engine_take_style_transaction( program_version: output.program_version, answers: output.answers.as_ptr(), count: output.answers.len(), + reclaimed_style_atoms: output.reclaimed_style_atoms.as_ptr(), + reclaimed_style_atom_count: output.reclaimed_style_atoms.len(), scoped: output.scoped, + style_atoms_swept: output.style_atoms_swept, } }) } + +/// Installs the authoritative release order recorded for the next replay transaction. +/// +/// # Safety +/// `engine` must be live and `atoms` must name `count` readable atom identities. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn style_engine_set_replay_reclaimed_style_atoms( + engine: *mut c_void, + atoms: *const u32, + count: usize, +) { + abort_on_panic(|| { + let engine = unsafe { &mut *engine.cast::() }; + assert!(engine.replay_reclaimed_style_atoms.is_none()); + let atoms = if count == 0 { + &[] + } else { + assert!(!atoms.is_null()); + unsafe { std::slice::from_raw_parts(atoms, count) } + }; + engine.replay_reclaimed_style_atoms = Some(atoms.iter().copied().map(StyleAtomID).collect()); + }); +} /// Reads one counter by index, returning its stable name and writing its value and name length, or /// null once the index is past the end. C++ enumerates the counters this way rather than /// duplicating the list. The name is borrowed static UTF-8 and is not nul-terminated. @@ -2648,7 +2794,7 @@ mod tests { }, }, ]; - engine.apply_transaction_batch(&initial_tree, &[], &[], &[], &[]); + engine.apply_transaction_batch(&initial_tree, (&[], &[]), &[], &[], &[], &[]); let root = StyleNodeID::from_raw(nodes[0]).unwrap(); let child = StyleNodeID::from_raw(nodes[1]).unwrap(); @@ -2662,7 +2808,7 @@ mod tests { assert_eq!(transaction.inputs.len(), 2); assert!(transaction.inputs.iter().all(|input| matches!( input.key, - InputKey::TreeRelations(node) | InputKey::LocalFeature(node, FeatureKey::ArrivingFacts) if node == root + InputKey::TreeRelations(node) | InputKey::LocalFeature(node, LocalFeatureKey::ArrivingFacts) if node == root ))); engine.release_transaction(transaction); @@ -2682,7 +2828,7 @@ mod tests { ..no_relations() }, }]; - engine.apply_transaction_batch(&later_arrival, &[], &[], &[], &[]); + engine.apply_transaction_batch(&later_arrival, (&[], &[]), &[], &[], &[], &[]); let transaction = engine.take_transaction(); assert_eq!(transaction.inputs.len(), 3); let later = StyleNodeID::from_raw(nodes[3]).unwrap(); @@ -2696,7 +2842,7 @@ mod tests { .iter() .filter(|input| matches!( input.key, - InputKey::TreeRelations(node) | InputKey::LocalFeature(node, FeatureKey::ArrivingFacts) if node == later + InputKey::TreeRelations(node) | InputKey::LocalFeature(node, LocalFeatureKey::ArrivingFacts) if node == later )) .count(), 2 @@ -2706,6 +2852,110 @@ mod tests { assert_eq!(engine.counters().get(Counter::InitialBulkTreeRows), 3); } + #[test] + fn element_arrival_rows_install_intrinsic_facts() { + assert_eq!(size_of::(), 28); + let mut engine = StyleEngine::new(DeviceClass::ForegroundDesktop); + let mut nodes = [0_u32; 2]; + engine.allocate_style_nodes(&mut nodes); + let tree = [ + FfiTreeDelta { + node: nodes[0], + old_connected: false, + new_connected: true, + old_relations: no_relations(), + new_relations: no_relations(), + }, + FfiTreeDelta { + node: nodes[1], + old_connected: false, + new_connected: true, + old_relations: no_relations(), + new_relations: FfiTreeRelations { + parent: nodes[0], + ..no_relations() + }, + }, + ]; + let arrivals = [ + FfiElementArrival { + node: nodes[0], + namespace_atom: 11, + language_atom: 12, + directionality_atom: 13, + custom_state_offset: 0, + custom_state_count: 2, + heading_level: 4, + is_slot: true, + reserved: 0, + }, + FfiElementArrival { + node: nodes[1], + namespace_atom: 21, + language_atom: 22, + directionality_atom: 23, + custom_state_offset: 2, + custom_state_count: 1, + heading_level: 0, + is_slot: false, + reserved: 0, + }, + ]; + engine.apply_transaction_batch(&tree, (&arrivals, &[31, 32, 33]), &[], &[], &[], &[]); + let transaction = engine.take_transaction(); + + let root = StyleNodeID::from_raw(nodes[0]).unwrap(); + let child = StyleNodeID::from_raw(nodes[1]).unwrap(); + assert_eq!(engine.facts.namespace_of(root), StyleAtomID(11)); + assert_eq!(engine.facts.language_of(root), StyleAtomID(12)); + assert_eq!(engine.facts.directionality_of(root), StyleAtomID(13)); + assert_eq!(engine.facts.heading_level_of(root), 4); + assert!(engine.facts.is_slot(root)); + assert_eq!(engine.facts.custom_states_of(root), &[StyleAtomID(31), StyleAtomID(32)]); + assert_eq!(engine.facts.namespace_of(child), StyleAtomID(21)); + assert_eq!(engine.facts.custom_states_of(child), &[StyleAtomID(33)]); + engine.release_transaction(transaction); + } + + fn arrival_for(node: u32, custom_state_offset: u32, custom_state_count: u32) -> FfiElementArrival { + FfiElementArrival { + node, + namespace_atom: 1, + language_atom: 2, + directionality_atom: 3, + custom_state_offset, + custom_state_count, + heading_level: 0, + is_slot: false, + reserved: 0, + } + } + + #[test] + #[should_panic(expected = "an element arrival named an invalid style node")] + fn malformed_element_arrival_rejects_an_invalid_node() { + let mut engine = StyleEngine::new(DeviceClass::ForegroundDesktop); + engine.apply_transaction_batch(&[], (&[arrival_for(0, 0, 0)], &[]), &[], &[], &[], &[]); + } + + #[test] + #[should_panic(expected = "an element arrival custom-state range overflowed")] + fn malformed_element_arrival_rejects_an_overflowing_custom_state_range() { + let mut engine = StyleEngine::new(DeviceClass::ForegroundDesktop); + let mut nodes = [0]; + engine.allocate_style_nodes(&mut nodes); + engine.apply_transaction_batch(&[], (&[arrival_for(nodes[0], u32::MAX, 1)], &[]), &[], &[], &[], &[]); + } + + #[test] + #[should_panic(expected = "an element arrival named custom states outside the shared atom column")] + fn malformed_element_arrival_rejects_an_out_of_bounds_custom_state_range() { + let mut engine = StyleEngine::new(DeviceClass::ForegroundDesktop); + let mut nodes = [0]; + engine.allocate_style_nodes(&mut nodes); + engine.apply_transaction_batch(&[], (&[arrival_for(nodes[0], 0, 2)], &[1]), &[], &[], &[], &[]); + } + #[test] fn initial_tree_bulk_load_publishes_match_answers_before_traversal() { let mut engine = StyleEngine::new(DeviceClass::ForegroundDesktop); @@ -2749,7 +2999,7 @@ mod tests { new_kind: FfiFeatureValueKind::Atom, new_atom: 1, }); - engine.apply_transaction_batch(&initial_tree, &initial_features, &[], &[], &[]); + engine.apply_transaction_batch(&initial_tree, (&[], &[]), &initial_features, &[], &[], &[]); let root = StyleNodeID::from_raw(nodes[0]).unwrap(); let mut published = Vec::new(); @@ -2827,7 +3077,7 @@ mod tests { }, }, ]; - engine.apply_transaction_batch(&arrival, &[], &[], &[], &[]); + engine.apply_transaction_batch(&arrival, (&[], &[]), &[], &[], &[], &[]); let settled = engine.take_transaction(); engine.release_transaction(settled); @@ -2868,7 +3118,7 @@ mod tests { reaction: crate::css::style::transaction::STYLE_REACTION_RECOMPUTE_STYLE, inherited_style_groups: 0, }]; - engine.apply_transaction_batch(&tree, &features, &states, &declarations, &style_inputs); + engine.apply_transaction_batch(&tree, (&[], &[]), &features, &states, &declarations, &style_inputs); let transaction = engine.take_transaction(); let node0 = StyleNodeID::from_raw(nodes[0]).unwrap(); @@ -2892,7 +3142,7 @@ mod tests { #[test] fn a_batch_of_no_deltas_costs_nothing() { let mut engine = StyleEngine::new(DeviceClass::ForegroundDesktop); - engine.apply_transaction_batch(&[], &[], &[], &[], &[]); + engine.apply_transaction_batch(&[], (&[], &[]), &[], &[], &[], &[]); let transaction = engine.take_transaction(); assert!(transaction.is_empty()); engine.release_transaction(transaction); diff --git a/Libraries/LibWeb/Rust/src/css/style/cascade.rs b/Libraries/LibWeb/Rust/src/css/style/cascade.rs index 1e58aae338037..dea7201f08cc1 100644 --- a/Libraries/LibWeb/Rust/src/css/style/cascade.rs +++ b/Libraries/LibWeb/Rust/src/css/style/cascade.rs @@ -164,11 +164,6 @@ impl CascadePriority { rule_rank: (0, 0), }) } - - #[must_use] - pub(super) fn is_important(self) -> bool { - (5..=8).contains(&self.origin_importance) - } } // -- Winners and stopping keys ----------------------------------------------------------------- @@ -240,6 +235,9 @@ pub enum WinnerSource { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct PropertyWinner { pub property: PropertyID, + /// Retained separately from the ordering tuple because semantic state reuse can span an + /// importance edit, while source publication still has to identify the winning declaration. + pub important: bool, pub key: SpecifiedWinnerKey, pub priority: CascadePriority, /// Provenance, retained for diagnostics. Not part of any stopping key. @@ -353,6 +351,8 @@ impl CascadeStratum { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CascadeCandidate { pub winner: PropertyWinner, + /// Needed only while reducing contenders, never after the winner is retained. + pub priority: CascadePriority, pub stratum: CascadeStratum, } @@ -479,6 +479,28 @@ impl InternIdentity for WinnerGroupID { } } +define_id! { + /// Identity of provenance parallel to one interned winner group. + struct WinnerProvenanceGroupID(pub); +} + +impl InternIdentity for WinnerProvenanceGroupID { + fn index(self) -> usize { + self.0 as usize + } +} + +define_id! { + /// Identity of one exact priority retained by winner provenance. + struct CascadePriorityID(pub); +} + +impl InternIdentity for CascadePriorityID { + fn index(self) -> usize { + self.0 as usize + } +} + define_id! { /// Identity of one factorized sparse cascade state. pub struct CascadeStateID(pub); @@ -577,10 +599,12 @@ pub(super) enum WinnerGroupCoverageGap { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(super) enum WinnerGroupTokenGap { StaleGeneration { retained: u64, current: u64 }, + AdmissionClosed, } const WINNER_RULE_PAGE_SHIFT: usize = 6; const WINNER_RULE_PAGE_SIZE: usize = 1 << WINNER_RULE_PAGE_SHIFT; +const WINNER_RULE_NODE_LIMIT: usize = 32; struct WinnerRuleIndexPage { entries: [u32; WINNER_RULE_PAGE_SIZE], @@ -617,9 +641,16 @@ impl RemovablePagedColumnPage for WinnerRuleIndexPage { } #[derive(Clone, Copy)] +struct WinnerRuleNodeReference { + node: StyleNodeID, + references: u32, +} + +#[derive(Clone)] struct WinnerRuleReferenceEntry { rule: RuleID, references: u64, + nodes: Option>, } /// A safe dense-and-sparse set of rules whose declarations currently win somewhere. @@ -632,6 +663,7 @@ struct WinnerRuleReferences { entries: Vec, accounted_dense_len: usize, accounted_dense_capacity: usize, + posting_bytes: u64, } impl Clone for WinnerRuleReferences { @@ -642,11 +674,17 @@ impl Clone for WinnerRuleReferences { ..Self::default() }; clone.entries.reserve(self.entries.len()); - for &entry in &self.entries { + for entry in &self.entries { let index = u32::try_from(clone.entries.len()).expect("winner rule inventory exhausted"); - clone.entries.push(entry); + clone.entries.push(entry.clone()); clone.indices.insert(entry.rule.0 as usize, index); } + clone.posting_bytes = clone + .entries + .iter() + .filter_map(|entry| entry.nodes.as_ref()) + .map(|nodes| (nodes.capacity() * size_of::()) as u64) + .sum(); clone } } @@ -659,7 +697,11 @@ impl WinnerRuleReferences { return; } let index = u32::try_from(self.entries.len()).expect("winner rule inventory exhausted"); - self.entries.push(WinnerRuleReferenceEntry { rule, references: 1 }); + self.entries.push(WinnerRuleReferenceEntry { + rule, + references: 1, + nodes: Some(Vec::new()), + }); let (previous, _) = self.indices.insert(rule.0 as usize, index); debug_assert!(previous.is_none()); } @@ -674,6 +716,13 @@ impl WinnerRuleReferences { if self.entries[index].references != 0 { return; } + let released_posting_bytes = self.entries[index].nodes.as_ref().map_or(0, |nodes| { + (nodes.capacity() * size_of::()) as u64 + }); + self.posting_bytes = self + .posting_bytes + .checked_sub(released_posting_bytes) + .expect("winner rule node posting byte count underflow"); self.indices.remove(rule.0 as usize); self.entries.swap_remove(index); if let Some(moved) = self.entries.get(index) { @@ -685,6 +734,56 @@ impl WinnerRuleReferences { self.indices.get(rule.0 as usize).is_some() } + fn retain_node(&mut self, rule: RuleID, node: StyleNodeID) { + let index = self + .indices + .get(rule.0 as usize) + .expect("a winning node must reference a retained winner rule") as usize; + let Some(nodes) = &mut self.entries[index].nodes else { + return; + }; + let capacity_before = nodes.capacity(); + match nodes.binary_search_by_key(&node, |reference| reference.node) { + Ok(index) => nodes[index].references += 1, + Err(_) if nodes.len() == WINNER_RULE_NODE_LIMIT => self.entries[index].nodes = None, + Err(index) => nodes.insert(index, WinnerRuleNodeReference { node, references: 1 }), + } + let capacity_after = self.entries[index].nodes.as_ref().map_or(0, Vec::capacity); + self.posting_bytes = self + .posting_bytes + .checked_sub((capacity_before * size_of::()) as u64) + .and_then(|bytes| bytes.checked_add((capacity_after * size_of::()) as u64)) + .expect("winner rule node posting byte count overflow"); + } + + fn release_node(&mut self, rule: RuleID, node: StyleNodeID) { + let index = self + .indices + .get(rule.0 as usize) + .expect("a released winning node must reference a retained winner rule") as usize; + let Some(nodes) = &mut self.entries[index].nodes else { + return; + }; + let node_index = nodes + .binary_search_by_key(&node, |reference| reference.node) + .expect("a released winning node must be retained"); + nodes[node_index].references -= 1; + if nodes[node_index].references == 0 { + nodes.remove(node_index); + } + } + + fn nodes(&self, rule: RuleID) -> Option + '_> { + let index = self.indices.get(rule.0 as usize)? as usize; + Some( + self.entries[index] + .nodes + .as_ref()? + .iter() + .map(|reference| reference.node), + ) + } + fn account_for_dense_rule(&mut self, rule: RuleID) { let required = (rule.0 as usize) .checked_add(1) @@ -697,6 +796,15 @@ impl WinnerRuleReferences { } self.accounted_dense_len = required; } + + #[cfg(test)] + fn measured_posting_bytes(&self) -> u64 { + self.entries + .iter() + .filter_map(|entry| entry.nodes.as_ref()) + .map(|nodes| (nodes.capacity() * size_of::()) as u64) + .sum() + } } impl ShallowCapacityBytes for WinnerRuleReferences { @@ -705,7 +813,7 @@ impl ShallowCapacityBytes for WinnerRuleReferences { // correctness-neutral caches stay warm, so making this inventory physically smaller must // not give an extreme page a larger effective cache budget. let dense_bytes = (self.accounted_dense_capacity * size_of::()) as u64; - dense_bytes.max(self.indices.capacity_bytes() + self.entries.shallow_capacity_bytes()) + dense_bytes.max(self.indices.capacity_bytes() + self.entries.shallow_capacity_bytes() + self.posting_bytes) } } @@ -716,23 +824,25 @@ impl ShallowCapacityBytes for WinnerRuleReferences { /// evicting it changes no semantic version and a later observer reconstructs a state from the /// node's cascade input or from the exact cold cascade. pub struct WinnerGroups { - states: InternTable>, + states: InternTable>, state_reference_counts: Vec, + state_winning_rules: Vec>, groups: InternTable, - group_reference_counts: Vec, + provenance_groups: InternTable>, + priorities: InternTable, continuations: InternTable, winner_entry_count: usize, winner_rule_references: WinnerRuleReferences, column: Column>, - pseudo_rows: HashMap<(StyleNodeID, PseudoElementTarget), PseudoWinnerRow>, - pseudo_targets_by_node: Column>, - pseudo_target_capacity_bytes: u64, + pseudo_rows_by_node: Column>, + pseudo_row_capacity_bytes: u64, priority_current: BitColumn, row_count: usize, priority_current_row_count: usize, newest_program_version: ProgramVersion, newest_version_row_count: usize, generation: u64, + admitting: bool, residency: MemoryLease, nested_residency: MemoryLease, #[cfg(test)] @@ -741,36 +851,58 @@ pub struct WinnerGroups { #[derive(Clone)] struct PseudoWinnerRow { + pseudo: PseudoElementTarget, state: (CascadeStateID, ProgramVersion), priority_current: bool, } #[derive(Clone)] struct WinnerGroup { - winners: Vec, + winners: Vec, content_hash: u64, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct SemanticPropertyWinner { + property: PropertyID, + key: SpecifiedWinnerKey, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct WinnerProvenance { + important: bool, + source: WinnerSource, + priority: CascadePriorityID, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct WinnerGroupRef { + winners: WinnerGroupID, + provenance: WinnerProvenanceGroupID, +} + impl Default for WinnerGroups { fn default() -> Self { Self { states: InternTable::default(), state_reference_counts: Vec::new(), + state_winning_rules: Vec::new(), groups: InternTable::default(), - group_reference_counts: Vec::new(), + provenance_groups: InternTable::default(), + priorities: InternTable::default(), continuations: InternTable::default(), winner_entry_count: 0, winner_rule_references: WinnerRuleReferences::default(), column: Column::default(), - pseudo_rows: HashMap::default(), - pseudo_targets_by_node: Column::default(), - pseudo_target_capacity_bytes: 0, + pseudo_rows_by_node: Column::default(), + pseudo_row_capacity_bytes: 0, priority_current: BitColumn::default(), row_count: 0, priority_current_row_count: 0, newest_program_version: ProgramVersion::default(), newest_version_row_count: 0, generation: 0, + admitting: true, residency: MemoryLease::new(MemoryCategory::CascadeWinnerGroup), nested_residency: MemoryLease::new(MemoryCategory::CascadeWinnerGroup), #[cfg(test)] @@ -786,29 +918,31 @@ impl WinnerGroups { } pub(super) fn verification_copy(&self) -> Self { - let pseudo_targets_by_node = self.pseudo_targets_by_node.clone(); - let pseudo_target_capacity_bytes = pseudo_targets_by_node + let pseudo_rows_by_node = self.pseudo_rows_by_node.clone(); + let pseudo_row_capacity_bytes = pseudo_rows_by_node .iter() - .map(|targets| targets.capacity() * size_of::()) + .map(|rows| rows.capacity() * size_of::()) .sum::() as u64; Self { states: self.states.clone(), state_reference_counts: self.state_reference_counts.clone(), + state_winning_rules: self.state_winning_rules.clone(), groups: self.groups.clone(), - group_reference_counts: self.group_reference_counts.clone(), + provenance_groups: self.provenance_groups.clone(), + priorities: self.priorities.clone(), continuations: self.continuations.clone(), winner_entry_count: self.winner_entry_count, winner_rule_references: self.winner_rule_references.clone(), column: self.column.clone(), - pseudo_rows: self.pseudo_rows.clone(), - pseudo_targets_by_node, - pseudo_target_capacity_bytes, + pseudo_rows_by_node, + pseudo_row_capacity_bytes, priority_current: self.priority_current.clone(), row_count: self.row_count, priority_current_row_count: self.priority_current_row_count, newest_program_version: self.newest_program_version, newest_version_row_count: self.newest_version_row_count, generation: self.generation, + admitting: self.admitting, residency: MemoryLease::new(MemoryCategory::CascadeWinnerGroup), nested_residency: MemoryLease::new(MemoryCategory::CascadeWinnerGroup), #[cfg(test)] @@ -852,7 +986,7 @@ impl WinnerGroups { /// by `revert` and `revert-layer` operators. pub fn resolve_candidates(&mut self, candidates: &mut [CascadeCandidate]) -> Option { // CascadePriority is the total declaration order, so equal keys are interchangeable here. - candidates.sort_unstable_by_key(|candidate| candidate.winner.priority); + candidates.sort_unstable_by_key(|candidate| candidate.priority); self.resolve_candidates_below(candidates, &mut Vec::new()) } @@ -866,8 +1000,10 @@ impl WinnerGroups { .rev() .copied() .find(|candidate| ceilings.iter().all(|&ceiling| candidate.stratum.is_below(ceiling)))?; + let mut winner = candidate.winner; + winner.priority = candidate.priority; let Some(ceiling) = candidate.stratum.ceiling(candidate.winner.key.operator) else { - return Some(candidate.winner); + return Some(winner); }; ceilings.push(ceiling); let continuation_winner = self.resolve_candidates_below(candidates, ceilings); @@ -876,7 +1012,6 @@ impl WinnerGroups { ceiling, winner: continuation_winner, }); - let mut winner = candidate.winner; winner.key.continuation = continuation; Some(winner) } @@ -943,7 +1078,7 @@ impl WinnerGroups { .filter(|&group| self.group_bucket(group) == bucket) }); let group = previous_group - .filter(|&group| self.groups[group].winners == winners[start..end]) + .filter(|&group| self.group_matches(group, &winners[start..end])) .unwrap_or_else(|| self.intern_group(&winners[start..end])); groups.push(group); start = end; @@ -956,16 +1091,37 @@ impl WinnerGroups { self.intern_group_ids(groups) } - fn intern_group_ids(&mut self, groups: Vec) -> CascadeStateID { + fn intern_group_ids(&mut self, groups: Vec) -> CascadeStateID { let hash = content_hash(&groups); if let Some(id) = self.states.find(hash, |_id, candidate| *candidate == groups) { return id; } let id = CascadeStateID(u32::try_from(self.states.len()).expect("cascade state space exhausted")); - self.nested_residency - .grow_committed((groups.capacity() * size_of::()) as u64); + let mut winning_rules = Vec::new(); + for &group in &groups { + for mut winner in self.group_winners(group) { + loop { + if let WinnerSource::Rule(rule) = winner.source { + winning_rules.push(rule); + } + let Some(continuation) = self.continuation(winner.key.continuation) else { + break; + }; + let Some(next) = continuation.winner else { + break; + }; + winner = next; + } + } + } + winning_rules.sort_unstable(); + winning_rules.dedup(); + self.nested_residency.grow_committed( + (groups.capacity() * size_of::() + winning_rules.capacity() * size_of::()) as u64, + ); self.states.insert(hash, id, groups); self.state_reference_counts.push(0); + self.state_winning_rules.push(winning_rules); id } @@ -1001,8 +1157,8 @@ impl WinnerGroups { .get(group_index) .copied() .filter(|&group| self.group_bucket(group) == bucket); - let old_winners = old_group - .map(|group| self.groups[group].winners.clone()) + let old_winners: Vec = old_group + .map(|group| self.group_winners(group).collect()) .unwrap_or_default(); let bucket_updates = &updates[update_start..update_end]; let mut winners = Vec::with_capacity(old_winners.len() + bucket_updates.len()); @@ -1036,7 +1192,7 @@ impl WinnerGroups { groups.remove(group_index); } (Some(old_group), false) => { - let new_group = if self.groups[old_group].winners == winners { + let new_group = if self.group_matches(old_group, &winners) { old_group } else { self.intern_group(&winners) @@ -1065,36 +1221,100 @@ impl WinnerGroups { ) } - fn intern_group(&mut self, winners: &[PropertyWinner]) -> WinnerGroupID { - let hash = content_hash(winners); + fn intern_group(&mut self, winners: &[PropertyWinner]) -> WinnerGroupRef { + let semantic: Vec<_> = winners + .iter() + .map(|winner| SemanticPropertyWinner { + property: winner.property, + key: winner.key, + }) + .collect(); + let provenance: Vec<_> = winners + .iter() + .map(|winner| WinnerProvenance { + important: winner.important, + source: winner.source, + priority: self.intern_priority(winner.priority), + }) + .collect(); + let hash = content_hash(&semantic); #[cfg(test)] { self.group_hash_computations += 1; } if let Some(id) = self.groups.find(hash, |_id, group| { - group.content_hash == hash && group.winners == winners + group.content_hash == hash && group.winners == semantic }) { - return id; + return WinnerGroupRef { + winners: id, + provenance: self.intern_provenance_group(provenance), + }; } - let winners = winners.to_vec(); let id = WinnerGroupID(u32::try_from(self.groups.len()).expect("winner group space exhausted")); - self.winner_entry_count += winners.len(); + self.winner_entry_count += semantic.len(); self.nested_residency - .grow_committed((winners.capacity() * size_of::()) as u64); + .grow_committed((semantic.capacity() * size_of::()) as u64); self.groups.insert( hash, id, WinnerGroup { - winners, + winners: semantic, content_hash: hash, }, ); - self.group_reference_counts.push(0); + WinnerGroupRef { + winners: id, + provenance: self.intern_provenance_group(provenance), + } + } + + fn intern_provenance_group(&mut self, provenance: Vec) -> WinnerProvenanceGroupID { + let hash = content_hash(&provenance); + if let Some(id) = self + .provenance_groups + .find(hash, |_id, candidate| *candidate == provenance) + { + return id; + } + let id = WinnerProvenanceGroupID( + u32::try_from(self.provenance_groups.len()).expect("winner provenance group space exhausted"), + ); + self.nested_residency + .grow_committed((provenance.capacity() * size_of::()) as u64); + self.provenance_groups.insert(hash, id, provenance); id } - fn group_bucket(&self, group: WinnerGroupID) -> PropertyID { - self.groups[group] + fn intern_priority(&mut self, priority: CascadePriority) -> CascadePriorityID { + let hash = content_hash(priority); + if let Some(id) = self.priorities.find(hash, |_id, candidate| *candidate == priority) { + return id; + } + let id = CascadePriorityID(u32::try_from(self.priorities.len()).expect("cascade priority space exhausted")); + self.priorities.insert(hash, id, priority); + id + } + + fn group_matches(&self, group: WinnerGroupRef, winners: &[PropertyWinner]) -> bool { + self.group_winners(group).eq(winners.iter().copied()) + } + + fn group_winners(&self, group: WinnerGroupRef) -> impl Iterator + '_ { + self.groups[group.winners] + .winners + .iter() + .zip(&self.provenance_groups[group.provenance]) + .map(|(winner, provenance)| PropertyWinner { + property: winner.property, + important: provenance.important, + key: winner.key, + priority: self.priorities[provenance.priority], + source: provenance.source, + }) + } + + fn group_bucket(&self, group: WinnerGroupRef) -> PropertyID { + self.groups[group.winners] .winners .first() .expect("a winner group is non-empty") @@ -1106,23 +1326,28 @@ impl WinnerGroups { pub fn winner_in_state(&self, state: CascadeStateID, property: PropertyID) -> Option { let groups = &self.states[state]; let index = groups.partition_point(|group| { - self.groups[*group] + self.groups[group.winners] .winners .last() .is_some_and(|winner| winner.property < property) }); - let group = groups.get(index)?; - let winners = &self.groups[*group].winners; + let group = *groups.get(index)?; + let winners = &self.groups[group.winners].winners; + let provenance = &self.provenance_groups[group.provenance]; winners .binary_search_by_key(&property, |winner| winner.property) .ok() - .map(|index| winners[index]) + .map(|index| PropertyWinner { + property: winners[index].property, + important: provenance[index].important, + key: winners[index].key, + priority: self.priorities[provenance[index].priority], + source: provenance[index].source, + }) } pub fn winners_in_state(&self, state: CascadeStateID) -> impl Iterator + '_ { - self.states[state] - .iter() - .flat_map(|group| self.groups[*group].winners.iter().copied()) + self.states[state].iter().flat_map(|&group| self.group_winners(group)) } /// Compare the semantic winners consumed by two computed-style publications. @@ -1136,7 +1361,7 @@ impl WinnerGroups { /// priority changed. #[must_use] pub fn semantic_delta(&self, previous: Option, current: CascadeStateID) -> CascadeWinnerDelta { - let previous: &[WinnerGroupID] = previous.map_or(&[], |state| &self.states[state]); + let previous: &[WinnerGroupRef] = previous.map_or(&[], |state| &self.states[state]); let current = &self.states[current]; let mut properties = Vec::new(); for entry in merge_sorted_by(previous, current, |old, new| { @@ -1144,15 +1369,15 @@ impl WinnerGroups { }) { match entry { SortedMergeEntry::Both(old, new) => { - if old != new { - self.append_group_semantic_delta(Some(*old), Some(*new), &mut properties); + if old.winners != new.winners { + self.append_group_semantic_delta(Some(old.winners), Some(new.winners), &mut properties); } } SortedMergeEntry::Left(old) => { - self.append_group_semantic_delta(Some(*old), None, &mut properties); + self.append_group_semantic_delta(Some(old.winners), None, &mut properties); } SortedMergeEntry::Right(new) => { - self.append_group_semantic_delta(None, Some(*new), &mut properties); + self.append_group_semantic_delta(None, Some(new.winners), &mut properties); } } } @@ -1165,8 +1390,8 @@ impl WinnerGroups { current: Option, properties: &mut Vec, ) { - let previous: &[PropertyWinner] = previous.map_or(&[], |group| self.groups[group].winners.as_slice()); - let current: &[PropertyWinner] = current.map_or(&[], |group| self.groups[group].winners.as_slice()); + let previous: &[SemanticPropertyWinner] = previous.map_or(&[], |group| self.groups[group].winners.as_slice()); + let current: &[SemanticPropertyWinner] = current.map_or(&[], |group| self.groups[group].winners.as_slice()); for entry in merge_sorted_by(previous, current, |old, new| old.property.cmp(&new.property)) { match entry { SortedMergeEntry::Both(old, new) => { @@ -1204,16 +1429,21 @@ impl WinnerGroups { self.generation } - pub fn set(&mut self, node: StyleNodeID, state: CascadeStateID, program_version: ProgramVersion) { + #[must_use] + pub fn set(&mut self, node: StyleNodeID, state: CascadeStateID, program_version: ProgramVersion) -> bool { let Some(index) = node.element_index().map(|index| index as usize) else { - return; + return false; }; + if !self.admitting && self.column.get(index).is_none_or(Option::is_none) { + return false; + } self.column.ensure(index); if self.column[index] == Some((state, program_version)) { self.set_priority_current(index, true); - return; + return true; } if let Some((previous, previous_version)) = self.column[index] { + self.update_winner_rule_node_references(previous, node, false); self.release_state(previous); if previous_version == self.newest_program_version { self.newest_version_row_count -= 1; @@ -1227,49 +1457,60 @@ impl WinnerGroups { } self.column[index] = Some((state, program_version)); self.retain_state(state); + self.update_winner_rule_node_references(state, node, true); if program_version == self.newest_program_version { self.newest_version_row_count += 1; } self.set_priority_current(index, true); + true } + #[must_use] pub fn set_pseudo( &mut self, node: StyleNodeID, pseudo: PseudoElementTarget, state: CascadeStateID, program_version: ProgramVersion, - ) { + ) -> bool { let Some(index) = node.element_index().map(|index| index as usize) else { - return; + return false; }; - let key = (node, pseudo); - if self - .pseudo_rows - .get(&key) - .is_some_and(|row| row.state == (state, program_version)) - { - self.pseudo_rows.get_mut(&key).unwrap().priority_current = true; - return; + let existing = self + .pseudo_rows_by_node + .get(index) + .and_then(|rows| rows.iter().position(|row| row.pseudo == pseudo)); + if !self.admitting && existing.is_none() { + return false; } - match self.pseudo_rows.remove(&key) { - Some(previous) => self.release_state(previous.state.0), - None => { - self.pseudo_targets_by_node.ensure(index); - let capacity_before = self.pseudo_targets_by_node[index].capacity(); - self.pseudo_targets_by_node[index].push(pseudo); - self.pseudo_target_capacity_bytes += ((self.pseudo_targets_by_node[index].capacity() - capacity_before) - * size_of::()) as u64; + self.pseudo_rows_by_node.ensure(index); + if let Some(existing) = existing { + let row = &mut self.pseudo_rows_by_node[index][existing]; + if row.state == (state, program_version) { + row.priority_current = true; + return true; } - } - self.pseudo_rows.insert( - key, - PseudoWinnerRow { + let previous = row.state.0; + *row = PseudoWinnerRow { + pseudo, state: (state, program_version), priority_current: true, - }, - ); + }; + self.update_winner_rule_node_references(previous, node, false); + self.release_state(previous); + } else { + let capacity_before = self.pseudo_rows_by_node[index].capacity(); + self.pseudo_rows_by_node[index].push(PseudoWinnerRow { + pseudo, + state: (state, program_version), + priority_current: true, + }); + self.pseudo_row_capacity_bytes += + ((self.pseudo_rows_by_node[index].capacity() - capacity_before) * size_of::()) as u64; + } self.retain_state(state); + self.update_winner_rule_node_references(state, node, true); + true } pub fn remove(&mut self, node: StyleNodeID) { @@ -1277,6 +1518,7 @@ impl WinnerGroups { return; }; if let Some((previous, program_version)) = self.column.get_mut(index).and_then(Option::take) { + self.update_winner_rule_node_references(previous, node, false); self.release_state(previous); self.row_count -= 1; if program_version == self.newest_program_version { @@ -1284,13 +1526,12 @@ impl WinnerGroups { } } self.set_priority_current(index, false); - if let Some(targets) = self.pseudo_targets_by_node.get_mut(index) { - let targets = std::mem::take(targets); - self.pseudo_target_capacity_bytes -= (targets.capacity() * size_of::()) as u64; - for pseudo in targets { - if let Some(row) = self.pseudo_rows.remove(&(node, pseudo)) { - self.release_state(row.state.0); - } + if let Some(rows) = self.pseudo_rows_by_node.get_mut(index) { + let rows = std::mem::take(rows); + self.pseudo_row_capacity_bytes -= (rows.capacity() * size_of::()) as u64; + for row in rows { + self.update_winner_rule_node_references(row.state.0, node, false); + self.release_state(row.state.0); } } } @@ -1313,10 +1554,14 @@ impl WinnerGroups { * size_of::<(PseudoElementTarget, ProgramVersion, CascadeStateID, bool)>()) as u64; memory.reserve_required(MemoryCategory::BatchScratch, scratch_bytes); + if !self.admitting { + memory.release(MemoryCategory::BatchScratch, scratch_bytes); + return None; + } self.remove(target); - self.set(target, state, program_version); + assert!(self.set(target, state, program_version)); for (pseudo, version, state, priority_current) in &pseudo_states { - self.set_pseudo(target, *pseudo, *state, *version); + assert!(self.set_pseudo(target, *pseudo, *state, *version)); if !priority_current { self.mark_pseudo_inventory_incomplete(target, *pseudo); } @@ -1328,9 +1573,8 @@ impl WinnerGroups { fn retain_state(&mut self, state: CascadeStateID) { let index = state.0 as usize; if self.state_reference_counts[index] == 0 { - for group_index in 0..self.states[index].len() { - let group = self.states[index][group_index]; - self.retain_group(group); + for &rule in &self.state_winning_rules[index] { + self.winner_rule_references.retain(rule); } } self.state_reference_counts[index] += 1; @@ -1340,59 +1584,32 @@ impl WinnerGroups { let index = state.0 as usize; self.state_reference_counts[index] -= 1; if self.state_reference_counts[index] == 0 { - for group_index in 0..self.states[index].len() { - let group = self.states[index][group_index]; - self.release_group(group); + for &rule in &self.state_winning_rules[index] { + self.winner_rule_references.release(rule); } } } - fn retain_group(&mut self, group: WinnerGroupID) { - let index = group.0 as usize; - if self.group_reference_counts[index] == 0 { - for winner_index in 0..self.groups[index].winners.len() { - let winner = self.groups[index].winners[winner_index]; - self.update_winner_rule_references(winner, true); - } - } - self.group_reference_counts[index] += 1; - } - - fn release_group(&mut self, group: WinnerGroupID) { - let index = group.0 as usize; - self.group_reference_counts[index] -= 1; - if self.group_reference_counts[index] == 0 { - for winner_index in 0..self.groups[index].winners.len() { - let winner = self.groups[index].winners[winner_index]; - self.update_winner_rule_references(winner, false); + fn update_winner_rule_node_references(&mut self, state: CascadeStateID, node: StyleNodeID, retain: bool) { + let winner_rule_references = &mut self.winner_rule_references; + for &rule in &self.state_winning_rules[state.0 as usize] { + if retain { + winner_rule_references.retain_node(rule, node); + } else { + winner_rule_references.release_node(rule, node); } } } - fn update_winner_rule_references(&mut self, mut winner: PropertyWinner, retain: bool) { - loop { - if let WinnerSource::Rule(rule) = winner.source { - if retain { - self.winner_rule_references.retain(rule); - } else { - self.winner_rule_references.release(rule); - } - } - let Some(continuation) = self.continuation(winner.key.continuation) else { - break; - }; - let Some(next) = continuation.winner else { - break; - }; - winner = next; - } - } - #[must_use] pub fn rule_is_a_winner(&self, rule: RuleID) -> bool { self.winner_rule_references.contains(rule) } + pub fn winning_nodes(&self, rule: RuleID) -> Option + '_> { + self.winner_rule_references.nodes(rule) + } + fn priority_is_current(&self, index: usize) -> bool { self.priority_current.contains(index) } @@ -1414,8 +1631,10 @@ impl WinnerGroups { self.priority_current.clear(); self.priority_current_row_count = 0; } - for row in self.pseudo_rows.values_mut() { - row.priority_current = false; + for rows in self.pseudo_rows_by_node.iter_mut() { + for row in rows { + row.priority_current = false; + } } } @@ -1440,8 +1659,9 @@ impl WinnerGroups { current: self.generation, }); } - self.set(node, state, program_version); - Ok(()) + self.set(node, state, program_version) + .then_some(()) + .ok_or(WinnerGroupTokenGap::AdmissionClosed) } #[must_use] @@ -1456,6 +1676,9 @@ impl WinnerGroups { required: program_version, }); } + // Departed nodes lose their rows at the commit barrier and arriving nodes have no row yet. + // Therefore the resident row count names exactly the connected elements that must already + // have a winner row, and covering it proves there is no connected-element gap. if self.newest_version_row_count < row_count { return Lookup::Missing(WinnerGroupCoverageGap::InsufficientProgramRows { retained: self.newest_version_row_count, @@ -1471,6 +1694,14 @@ impl WinnerGroups { Lookup::Known(()) } + /// Start retained winner coverage for a new program version with no proven rows. + pub fn begin_program_version(&mut self, version: ProgramVersion) { + if version > self.newest_program_version { + self.newest_program_version = version; + self.newest_version_row_count = 0; + } + } + /// Advance rows whose retained winners were proven unchanged by a program transaction. pub fn advance_program_version_where( &mut self, @@ -1482,11 +1713,8 @@ impl WinnerGroups { return; } debug_assert!(to > from); - if to > self.newest_program_version { - self.newest_program_version = to; - self.newest_version_row_count = 0; - } - for (index, slot) in self.column.iter_mut().enumerate() { + self.begin_program_version(to); + for (index, slot) in self.column.iter_mut().enumerate().skip(1) { let Some((_, version)) = slot else { continue; }; @@ -1502,9 +1730,15 @@ impl WinnerGroups { self.newest_version_row_count += 1; } } - for ((node, _), row) in &mut self.pseudo_rows { - if row.state.1 == from && can_advance(*node) { - row.state.1 = to; + for (index, rows) in self.pseudo_rows_by_node.iter_mut().enumerate().skip(1) { + if rows.is_empty() { + continue; + } + let node = StyleNodeID::element(u32::try_from(index).expect("style node identity space exhausted")); + for row in rows { + if row.state.1 == from && can_advance(node) { + row.state.1 = to; + } } } } @@ -1522,16 +1756,12 @@ impl WinnerGroups { &self, node: StyleNodeID, ) -> impl Iterator + '_ { - let targets = node + let rows = node .element_index() - .and_then(|index| self.pseudo_targets_by_node.get(index as usize)) + .and_then(|index| self.pseudo_rows_by_node.get(index as usize)) .into_iter() .flatten(); - targets.filter_map(move |&pseudo| { - self.pseudo_rows - .get(&(node, pseudo)) - .map(|row| (pseudo, row.state.1, row.state.0, row.priority_current)) - }) + rows.map(|row| (row.pseudo, row.state.1, row.state.0, row.priority_current)) } #[must_use] @@ -1540,13 +1770,18 @@ impl WinnerGroups { node: StyleNodeID, pseudo: PseudoElementTarget, ) -> Option<(ProgramVersion, CascadeStateID, bool)> { - self.pseudo_rows - .get(&(node, pseudo)) + node.element_index() + .and_then(|index| self.pseudo_rows_by_node.get(index as usize)) + .and_then(|rows| rows.iter().find(|row| row.pseudo == pseudo)) .map(|row| (row.state.1, row.state.0, row.priority_current)) } pub(super) fn mark_pseudo_inventory_incomplete(&mut self, node: StyleNodeID, pseudo: PseudoElementTarget) { - if let Some(row) = self.pseudo_rows.get_mut(&(node, pseudo)) { + if let Some(row) = node + .element_index() + .and_then(|index| self.pseudo_rows_by_node.get_mut(index as usize)) + .and_then(|rows| rows.iter_mut().find(|row| row.pseudo == pseudo)) + { row.priority_current = false; } } @@ -1570,15 +1805,16 @@ impl WinnerGroups { self.generation = self.generation.wrapping_add(1); self.states = InternTable::default(); self.state_reference_counts = Vec::new(); + self.state_winning_rules = Vec::new(); self.groups = InternTable::default(); - self.group_reference_counts = Vec::new(); + self.provenance_groups = InternTable::default(); + self.priorities = InternTable::default(); self.continuations = InternTable::default(); self.winner_entry_count = 0; self.winner_rule_references = WinnerRuleReferences::default(); self.column = Column::default(); - self.pseudo_rows = HashMap::default(); - self.pseudo_targets_by_node = Column::default(); - self.pseudo_target_capacity_bytes = 0; + self.pseudo_rows_by_node = Column::default(); + self.pseudo_row_capacity_bytes = 0; self.priority_current = BitColumn::default(); self.row_count = 0; self.priority_current_row_count = 0; @@ -1592,17 +1828,18 @@ impl WinnerGroups { shallow [ self.states, self.groups, + self.provenance_groups, + self.priorities, self.continuations, self.column, - self.pseudo_rows, - self.pseudo_targets_by_node, + self.pseudo_rows_by_node, self.priority_current, self.state_reference_counts, - self.group_reference_counts, + self.state_winning_rules, self.winner_rule_references, ]; cached [self.nested_residency.bytes()]; - nested [self.pseudo_target_capacity_bytes]; + nested [self.pseudo_row_capacity_bytes]; skip [ self.residency, self.generation, @@ -1611,28 +1848,39 @@ impl WinnerGroups { self.priority_current_row_count, self.newest_program_version, self.newest_version_row_count, - self.pseudo_target_capacity_bytes, + self.pseudo_row_capacity_bytes, ]; } } - pub fn settle_memory(&mut self, memory: &mut MemoryController) -> bool { - self.nested_residency.settle_committed(memory); + pub fn settle_memory(&mut self, memory: &mut MemoryController) { + let nested = self.nested_residency.bytes(); + self.nested_residency.reconcile_committed(memory, nested); let current = self.capacity_bytes() - self.nested_residency.bytes(); - if !self.residency.resize_to(memory, current).is_granted() - || !memory.admit_committed(MemoryCategory::CascadeWinnerGroup) - { - self.evict(); - return false; - } - true + self.residency.reconcile_committed(memory, current); + memory.finish_committed_acceleration_growth(MemoryCategory::CascadeWinnerGroup); + self.admitting = memory.is_tier3_admitting(MemoryCategory::CascadeWinnerGroup); + } + + pub(super) fn begin_quota_period(&mut self) { + self.admitting = true; + } + + #[must_use] + pub(super) fn admits_new_rows(&self) -> bool { + self.admitting } } impl WinnerGroups { pub(super) fn lookup(&self, key: WinnerGroupKey) -> Lookup<&(CascadeStateID, ProgramVersion), WinnerGroupGap> { if let Some(pseudo) = key.pseudo { - let Some(row) = self.pseudo_rows.get(&(key.node, pseudo)) else { + let Some(row) = key + .node + .element_index() + .and_then(|index| self.pseudo_rows_by_node.get(index as usize)) + .and_then(|rows| rows.iter().find(|row| row.pseudo == pseudo)) + else { return Lookup::Missing(WinnerGroupGap::MissingNode(key.node)); }; if row.state.1 != key.program_version { @@ -1672,6 +1920,10 @@ mod tests { use super::super::memory::DeviceClass; use super::*; + fn memory() -> MemoryController { + MemoryController::new(DeviceClass::ForegroundDesktop) + } + fn inputs(origin: CascadeOrigin, important: bool) -> PriorityInputs { PriorityInputs { origin, @@ -1819,6 +2071,7 @@ mod tests { fn winner(property: PropertyID, value: u64, rule: u32) -> PropertyWinner { PropertyWinner { property, + important: false, key: winner_key(value), priority: CascadePriority::new(inputs(CascadeOrigin::Author, false)), source: WinnerSource::Rule(RuleID(rule)), @@ -1840,9 +2093,11 @@ mod tests { priority_inputs.rule_rank = (u64::from(rule), 0); let mut winner = winner(1, value, rule); winner.key.operator = operator; - winner.priority = CascadePriority::new(priority_inputs); + let priority = CascadePriority::new(priority_inputs); + winner.priority = priority; CascadeCandidate { winner, + priority, stratum: CascadeStratum::new(origin, important, 0, layer, (layer_rank, 0)), } } @@ -2044,7 +2299,10 @@ mod tests { fn semantic_winner_deltas_ignore_provenance_and_report_exact_properties() { let mut groups = WinnerGroups::new(); let before = groups.intern_sorted(&[winner(1, 10, 1), winner(3, 30, 1), winner(5, 50, 1)], None); + let semantic_group_count = groups.payload_count(); let same_values = groups.intern_sorted(&[winner(1, 10, 2), winner(3, 30, 2), winner(5, 50, 2)], None); + assert_ne!(before, same_values); + assert_eq!(groups.payload_count(), semantic_group_count); assert!(groups.semantic_delta(Some(before), same_values).is_empty()); let after = groups.intern_sorted( @@ -2055,6 +2313,35 @@ mod tests { assert_eq!(groups.semantic_delta(None, after).properties(), &[1, 2, 5, 7]); } + #[test] + fn winner_provenance_retains_exact_priority_without_semantic_identity() { + let mut groups = WinnerGroups::new(); + let mut before_winner = winner(1, 10, 1); + let mut before_inputs = inputs(CascadeOrigin::Author, false); + before_inputs.rule_rank = (1, 0); + before_winner.priority = CascadePriority::new(before_inputs); + let before = groups.intern_sorted(&[before_winner], None); + let semantic_group_count = groups.payload_count(); + + let mut after_winner = before_winner; + let mut after_inputs = before_inputs; + after_inputs.rule_rank = (2, 0); + after_winner.priority = CascadePriority::new(after_inputs); + let after = groups.intern_sorted(&[after_winner], None); + + assert_ne!(before, after); + assert_eq!(groups.payload_count(), semantic_group_count); + assert!(groups.semantic_delta(Some(before), after).is_empty()); + assert_eq!( + groups.winner_in_state(before, 1).unwrap().priority, + before_winner.priority + ); + assert_eq!( + groups.winner_in_state(after, 1).unwrap().priority, + after_winner.priority + ); + } + #[test] fn importance_is_weighed_per_declaration_not_per_rule() { // Two author rules, the less specific one marking its declaration important. Ordering the @@ -2183,15 +2470,25 @@ mod tests { let second_node = StyleNodeID::element(2); let sparse_node = StyleNodeID::element(3); - groups.set(first_node, first, ProgramVersion(1)); - groups.set(second_node, first, ProgramVersion(1)); + assert!(groups.set(first_node, first, ProgramVersion(1))); + assert!(groups.set(second_node, first, ProgramVersion(1))); assert!(groups.rule_is_a_winner(RuleID(3))); assert!(groups.rule_is_a_winner(RuleID(5))); - groups.set(sparse_node, sparse, ProgramVersion(1)); + assert!(groups.set(sparse_node, sparse, ProgramVersion(1))); assert!(groups.rule_is_a_winner(RuleID(100_000))); assert_eq!(groups.winner_rule_references.entries.len(), 3); + assert_ne!(groups.winner_rule_references.posting_bytes, 0); + assert_eq!( + groups.winner_rule_references.posting_bytes, + groups.winner_rule_references.measured_posting_bytes() + ); + let cloned_references = groups.winner_rule_references.clone(); + assert_eq!( + cloned_references.posting_bytes, + cloned_references.measured_posting_bytes() + ); - groups.set(first_node, second, ProgramVersion(1)); + assert!(groups.set(first_node, second, ProgramVersion(1))); groups.remove(second_node); assert!(!groups.rule_is_a_winner(RuleID(3))); assert!(groups.rule_is_a_winner(RuleID(5))); @@ -2201,6 +2498,53 @@ mod tests { groups.remove(sparse_node); assert!(!groups.rule_is_a_winner(RuleID(100_000))); assert!(groups.winner_rule_references.entries.is_empty()); + assert_eq!(groups.winner_rule_references.posting_bytes, 0); + } + + #[test] + fn closed_winner_admission_preserves_existing_rows_without_adding_new_ones() { + let mut memory = memory(); + memory.set_tier3_limit_for_test(0); + memory.begin_tier3_quota_period(); + let mut groups = WinnerGroups::new(); + let state = groups.intern_sorted(&[winner(1, 1, 3)], None); + let resident = StyleNodeID::element(1); + assert!(groups.set(resident, state, ProgramVersion(1))); + groups.settle_memory(&mut memory); + assert!(!memory.is_tier3_admitting(MemoryCategory::CascadeWinnerGroup)); + + let refused = StyleNodeID::element(2); + assert!(!groups.set(refused, state, ProgramVersion(1))); + assert!(matches!( + groups.lookup(WinnerGroupKey::current(resident, ProgramVersion(1))), + Lookup::Known(_) + )); + assert!(matches!( + groups.lookup(WinnerGroupKey::current(refused, ProgramVersion(1))), + Lookup::Missing(WinnerGroupGap::MissingNode(node)) if node == refused + )); + } + + #[test] + fn broad_winner_rule_node_postings_fall_back_to_the_rule_count() { + let mut groups = WinnerGroups::new(); + let state = groups.intern_sorted(&[winner(1, 1, 3)], None); + for index in 1..=WINNER_RULE_NODE_LIMIT + 1 { + assert!(groups.set( + StyleNodeID::element(u32::try_from(index).unwrap()), + state, + ProgramVersion(1), + )); + } + + assert!(groups.rule_is_a_winner(RuleID(3))); + assert!(groups.winning_nodes(RuleID(3)).is_none()); + assert_eq!(groups.winner_rule_references.posting_bytes, 0); + + for index in 1..=WINNER_RULE_NODE_LIMIT + 1 { + groups.remove(StyleNodeID::element(u32::try_from(index).unwrap())); + } + assert!(!groups.rule_is_a_winner(RuleID(3))); } #[test] @@ -2213,9 +2557,9 @@ mod tests { let before_state = groups.intern_sorted(&[winner(1, 20, 2)], None); let after_state = groups.intern_sorted(&[winner(1, 30, 3)], None); - groups.set(node, element_state, ProgramVersion(1)); - groups.set_pseudo(node, before, before_state, ProgramVersion(1)); - groups.set_pseudo(node, after, after_state, ProgramVersion(1)); + assert!(groups.set(node, element_state, ProgramVersion(1))); + assert!(groups.set_pseudo(node, before, before_state, ProgramVersion(1))); + assert!(groups.set_pseudo(node, after, after_state, ProgramVersion(1))); assert!(matches!( groups.winner(WinnerGroupKey::current(node, ProgramVersion(1)), 1), @@ -2273,9 +2617,9 @@ mod tests { let group = groups.intern_sorted(&[winner(1, 1, 1)], None); for index in 1..1000_u32 { - groups.set(StyleNodeID::element(index), group, ProgramVersion(1)); + assert!(groups.set(StyleNodeID::element(index), group, ProgramVersion(1))); } - assert!(groups.settle_memory(&mut memory)); + groups.settle_memory(&mut memory); assert!(memory.bytes_in_category(MemoryCategory::CascadeWinnerGroup) > 0); assert!(matches!( groups.coverage_at_least(ProgramVersion(1), 999), diff --git a/Libraries/LibWeb/Rust/src/css/style/catalog.rs b/Libraries/LibWeb/Rust/src/css/style/catalog.rs index 5eee726e75bfc..142e1c859ccdc 100644 --- a/Libraries/LibWeb/Rust/src/css/style/catalog.rs +++ b/Libraries/LibWeb/Rust/src/css/style/catalog.rs @@ -10,6 +10,7 @@ use super::fast_hash::fast_hasher; use super::*; define_id! { default pub(super) struct MatchAnswerID(pub(super)); } +define_id! { default pub(super) struct SelectorTruthSetID(pub(super)); } impl super::intern_table::InternIdentity for MatchAnswerID { fn index(self) -> usize { @@ -17,9 +18,69 @@ impl super::intern_table::InternIdentity for MatchAnswerID { } } -define_id! { - /// Identity of the cascade-compacted rule input published to style computation. - default pub(super) struct CascadeInputID(pub(super)); +impl super::intern_table::InternIdentity for SelectorTruthSetID { + fn index(self) -> usize { + self.0 as usize - 1 + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub(super) struct SelectorTruth { + pub(super) entry: EntryID, + pub(super) tree_scope: TreeScopeID, + pub(super) scope_proximity: u32, +} + +#[derive(Default)] +pub(super) struct SelectorTruthSetCatalog { + sets: super::intern_table::InternTable>, + verified_derived_answers: HashMap<(SelectorTruthSetID, TreeScopeID, u64), Rc<[RetainedRuleMatch]>>, + last_identity: u32, +} + +impl SelectorTruthSetCatalog { + pub(super) fn intern_prepared(&mut self, truth: Vec) -> (SelectorTruthSetID, bool) { + let hash = super::intern_table::content_hash(&truth); + if let Some(identity) = self + .sets + .find(hash, |_identity, candidate| candidate.as_ref() == truth.as_slice()) + { + return (identity, true); + } + self.last_identity = self + .last_identity + .checked_add(1) + .expect("selector truth-set identity space exhausted"); + let identity = SelectorTruthSetID(self.last_identity); + self.sets.insert(hash, identity, truth.into()); + (identity, false) + } + + pub(super) fn get(&self, identity: SelectorTruthSetID) -> &Rc<[SelectorTruth]> { + &self.sets[identity] + } + + pub(super) fn verify_derived_answer( + &mut self, + truth: SelectorTruthSetID, + tree_scope: TreeScopeID, + program_version: ProgramVersion, + answer: &[RetainedRuleMatch], + ) -> bool { + match self + .verified_derived_answers + .entry((truth, tree_scope, program_version.0)) + { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(answer.into()); + false + } + std::collections::hash_map::Entry::Occupied(entry) => { + assert_eq!(entry.get().as_ref(), answer, "selector truth derived two rule answers"); + true + } + } + } } pub(super) struct MatchAnswerCatalogEntry { @@ -71,10 +132,27 @@ impl MatchAnswerCatalog { } pub(super) fn intern(&mut self, answer: &[RuleMatch]) -> MatchAnswerID { - let mut answer: Vec = + const INLINE_ANSWER_LENGTH: usize = 16; + if let Some(first) = answer.first().copied() + && answer.len() <= INLINE_ANSWER_LENGTH + { + let first = RetainedRuleMatch::from_rule_match(first); + let mut prepared = [first; INLINE_ANSWER_LENGTH]; + for (output, matched) in prepared.iter_mut().zip(answer.iter().copied()) { + *output = RetainedRuleMatch::from_rule_match(matched); + } + let prepared = &mut prepared[..answer.len()]; + prepared.sort_unstable(); + let hash = hash_retained_rule_matches(prepared); + if let Some(identity) = self.identity(prepared, hash) { + return identity; + } + return self.insert_new(prepared.to_vec(), hash); + } + let mut prepared: Vec = answer.iter().copied().map(RetainedRuleMatch::from_rule_match).collect(); - answer.sort_unstable(); - self.intern_prepared(answer) + prepared.sort_unstable(); + self.intern_prepared(prepared) } pub(super) fn intern_prepared(&mut self, answer: Vec) -> MatchAnswerID { @@ -325,7 +403,7 @@ impl MatchAnswerCatalog { pub(super) struct PrefixAnswer { pub(super) matches: MatchAnswerID, pub(super) winner_group: Option<(u64, CascadeStateID)>, - pub(super) cascade_input: CascadeInputID, + pub(super) cascade_input: MatchAnswerID, pub(super) cascade_winner_inventory_is_complete: bool, } @@ -515,7 +593,7 @@ impl PrefixAnswerCache { key: PrefixAnswerKey, answer: &[RuleMatch], winner_group: Option<(u64, CascadeStateID)>, - cascade_input: CascadeInputID, + cascade_input: MatchAnswerID, cascade_winner_inventory_is_complete: bool, ) { self.with_payload_accounting(catalog, |cache, catalog| { @@ -579,9 +657,11 @@ impl PrefixAnswerCache { if self.retained { return true; } - if !self.residency.grow(memory, self.scratch_memory.bytes()).is_granted() { + if !memory.is_tier3_admitting(MemoryCategory::PrefixAnswerCache) { return false; } + self.residency.reconcile_committed(memory, self.scratch_memory.bytes()); + memory.finish_committed_acceleration_growth(MemoryCategory::PrefixAnswerCache); self.scratch_memory.release(); self.retained = true; true @@ -697,6 +777,23 @@ pub(super) fn prepare_retained_match_answer(matches: impl Iterator Vec { + let mut truth = matches + .iter() + .map(|matched| SelectorTruth { + entry: programs.entry_id(matched.program, matched.entry), + tree_scope: matched.tree_scope, + scope_proximity: matched.scope_proximity, + }) + .collect::>(); + truth.sort_unstable(); + truth.dedup(); + truth +} + pub(super) fn merge_retained_match_answers(answer: &mut Vec, suffix: &[RetainedRuleMatch]) { let mut answer_index = answer.len(); let mut suffix_index = suffix.len(); @@ -720,7 +817,7 @@ pub(super) fn merge_retained_match_answers(answer: &mut Vec, pub(super) struct RetainedMatchAnswers { pub(super) column: Vec, - pub(super) cascade_input_column: Vec, + pub(super) cascade_input_column: Vec, pub(super) cascade_input_memory: MemoryLease, pub(super) residency: MemoryLease, } @@ -730,7 +827,7 @@ pub(super) struct RetainedMatchAnswers { /// Active match answers normally discard a rule when its condition becomes false. This inverse /// relation preserves the selector side of that join, so a later condition flip can route through /// exact selector truth without evaluating the selector again. It is Tier-3 state: incomplete -/// retained coverage or a refused reservation simply leaves program routing on its cold path. +/// retained coverage or closed admission simply leaves program routing on its cold path. pub(super) struct RetainedSelectorIncidences { pub(super) by_program: Vec>>, pub(super) residency: MemoryLease, @@ -779,34 +876,20 @@ impl RetainedSelectorIncidences { incidences: Vec, memory: &mut MemoryController, ) -> Option> { - let required_len = program.0 as usize + 1; - let projected_capacity = if required_len > self.by_program.capacity() { - self.by_program.capacity().saturating_mul(2).max(required_len).max(4) - } else { - self.by_program.capacity() - }; - let projected = (projected_capacity * size_of::>>() - + self - .by_program - .iter() - .flatten() - .map(|incidences| incidences.len() * size_of::()) - .sum::() - + incidences.len() * size_of::()) as u64; - if !self - .residency - .grow(memory, projected.saturating_sub(self.residency.bytes())) - .is_granted() + if self.by_program.get(program.0 as usize).is_none_or(Option::is_none) + && !memory.is_tier3_admitting(MemoryCategory::RetainedSelectorIncidence) { return None; } + let required_len = program.0 as usize + 1; let incidences: Rc<[RetainedSelectorIncidence]> = incidences.into(); if self.by_program.len() < required_len { self.by_program.resize(required_len, None); } self.by_program[program.0 as usize] = Some(Rc::clone(&incidences)); - assert!(self.capacity_bytes() <= projected); - self.residency.shrink_to(self.capacity_bytes()); + let bytes = self.capacity_bytes(); + self.residency.reconcile_committed(memory, bytes); + memory.finish_committed_acceleration_growth(MemoryCategory::RetainedSelectorIncidence); Some(incidences) } @@ -832,40 +915,6 @@ pub(super) struct RetainedAnswerPatchRule { pub(super) program: SelectorProgramID, } -pub(super) struct RetainedAnswerCascadeOrder { - pub(super) rule: RuleID, - pub(super) program: SelectorProgramID, - pub(super) entry: u32, - pub(super) cascade_order: u32, -} - -#[derive(Clone, Copy)] -pub(super) enum RetainedAnswerCascadeOrders<'a> { - Dispatch(&'a RuleDispatch), - Snapshot(&'a [RetainedAnswerCascadeOrder]), -} - -impl RetainedAnswerCascadeOrders<'_> { - pub(super) fn cascade_order_for_entry( - self, - rule: RuleID, - program: SelectorProgramID, - selector_entry: u32, - ) -> Option { - match self { - Self::Dispatch(dispatch) => dispatch.cascade_order_for_entry(rule, program, selector_entry), - Self::Snapshot(orders) => { - let index = orders - .binary_search_by_key(&(rule, program, selector_entry), |order| { - (order.rule, order.program, order.entry) - }) - .ok()?; - Some(orders[index].cascade_order) - } - } - } -} - pub(super) struct RetainedAnswerPatch { pub(super) rules: Vec, /// Whether this transaction can reorder rules relative to each other (layer or sheet order). @@ -902,22 +951,28 @@ pub(super) struct RetainedAnswerPatch { #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub(super) struct RetainedAnswerDeltaMemoKey { pub(super) old_answer: MatchAnswerID, - pub(super) old_cascade_input: CascadeInputID, + pub(super) old_cascade_input: MatchAnswerID, pub(super) delta_count: usize, pub(super) delta_digest: u64, } pub(super) struct RetainedAnswerDeltaMemoEntry { - pub(super) deltas: Vec<(RuleID, SelectorProgramID, u32, SetChange)>, + pub(super) deltas: Vec<(RuleID, EntryID, SetChange)>, pub(super) transition: RetainedAnswerDeltaTransition, } +impl RetainedAnswerDeltaMemoEntry { + pub(super) fn capacity_bytes(&self) -> u64 { + (self.deltas.capacity() * size_of::<(RuleID, EntryID, SetChange)>()) as u64 + } +} + #[derive(Clone, Copy)] pub(super) struct RetainedAnswerDeltaTransition { pub(super) new_answer: MatchAnswerID, /// The compact identity after the transition. Equal to the asker's old identity exactly when /// the transition stopped. - pub(super) new_cascade_input: CascadeInputID, + pub(super) new_cascade_input: MatchAnswerID, /// The winner state the cohort's first member settled after applying the same deltas, with /// its program version, when one was retained. Emitting replays assign it by column store. pub(super) winner_state: Option<(CascadeStateID, ProgramVersion)>, @@ -937,7 +992,7 @@ pub(super) struct RetainedAnswerPatchOutcome { pub(super) struct IncrementalCascadeAnswer { pub(super) node: StyleNodeID, - pub(super) cascade_input: CascadeInputID, + pub(super) cascade_input: MatchAnswerID, pub(super) matches: Option>, pub(super) cascade_winners_are_complete: bool, } @@ -958,11 +1013,7 @@ impl RetainedAnswerPatch { self.cascade_compaction_workspace.capacity_bytes(), self.delta_memo .values() - .map(|entry| { - (entry.deltas.capacity() - * size_of::<(RuleID, SelectorProgramID, u32, SetChange)>()) - as u64 - }) + .map(RetainedAnswerDeltaMemoEntry::capacity_bytes) .sum::(), ]; skip []; @@ -1009,13 +1060,6 @@ impl RetainedMatchAnswers { self.cascade_input_memory.shrink_committed(bytes); } - pub(super) fn vec_capacity_after_growing(capacity: usize, required_len: usize) -> usize { - if required_len <= capacity { - return capacity; - } - capacity.saturating_mul(2).max(required_len).max(4) - } - pub(super) fn remember_prepared( &mut self, catalog: &mut MatchAnswerCatalog, @@ -1033,23 +1077,9 @@ impl RetainedMatchAnswers { .column .get(index) .map_or(MatchAnswerID::default(), |identity| *identity); - let column_len = index + 1; - let column_capacity = Self::vec_capacity_after_growing(self.column.capacity(), column_len); - let retained_growth = (!identity_is_retained).then_some( - (answer.len() * size_of::() - + 2 * size_of::() - + size_of::() - + size_of::() - + 1) as u64, - ); - let reserved_capacity_bytes = catalog.retained_capacity_bytes() - + retained_growth.unwrap_or(0) - + (column_capacity * size_of::()) as u64; - let reserved_growth = reserved_capacity_bytes.saturating_sub(self.residency.bytes()); - if !self.residency.grow(memory, reserved_growth).is_granted() { - // NB: Refusing a replacement must not leave this node's previous answer resident. - // Other nodes still own valid entries and remain untouched. - self.forget_answer(catalog, node); + if previous_identity == MatchAnswerID::default() + && !memory.is_tier3_admitting(MemoryCategory::RetainedMatchAnswer) + { return Err(answer); } @@ -1074,18 +1104,13 @@ impl RetainedMatchAnswers { catalog.release_identity(previous_identity); } let current = self.capacity_bytes(catalog); - assert!( - current <= reserved_capacity_bytes, - "retained match answer preflight underestimated capacity: current {current}, reserved {reserved_capacity_bytes}, columns {} <= {column_capacity}", - self.column.capacity(), - ); - self.residency.shrink_to(current); + self.residency.reconcile_committed(memory, current); + memory.finish_committed_acceleration_growth(MemoryCategory::RetainedMatchAnswer); Ok(()) } /// Repoint one node's retained answer to an identity the catalog already holds. The column may - /// grow within its reserved capacity, but neither the identity nor the column may require a new - /// reservation. + /// grow within its existing capacity, but neither the identity nor the column may allocate. pub(super) fn set_interned_identity( &mut self, node: StyleNodeID, @@ -1118,14 +1143,14 @@ impl RetainedMatchAnswers { &mut self, catalog: &mut MatchAnswerCatalog, node: StyleNodeID, - cascade_input: CascadeInputID, + cascade_input: MatchAnswerID, memory: &mut MemoryController, ) { let Some(index) = node.element_index().map(|index| index as usize) else { return; }; if self.cascade_input_column.len() <= index { - self.cascade_input_column.resize(index + 1, CascadeInputID::default()); + self.cascade_input_column.resize(index + 1, MatchAnswerID::default()); let current = self.cascade_input_capacity_bytes(catalog); self.cascade_input_memory.resize_required_to(memory, current); } @@ -1133,10 +1158,10 @@ impl RetainedMatchAnswers { if previous == cascade_input { return; } - if previous != CascadeInputID::default() { - catalog.release_cascade(MatchAnswerID(previous.0)); + if previous != MatchAnswerID::default() { + catalog.release_cascade(previous); } - catalog.retain_cascade(MatchAnswerID(cascade_input.0)); + catalog.retain_cascade(cascade_input); let current = self.cascade_input_capacity_bytes(catalog); self.cascade_input_memory.resize_required_to(memory, current); } @@ -1149,19 +1174,19 @@ impl RetainedMatchAnswers { return; }; let previous = std::mem::take(slot); - if previous != CascadeInputID::default() { - catalog.release_cascade(MatchAnswerID(previous.0)); + if previous != MatchAnswerID::default() { + catalog.release_cascade(previous); self.cascade_input_memory .shrink_to(self.cascade_input_capacity_bytes(catalog)); } } - pub(super) fn cascade_input_lookup(&self, node: StyleNodeID) -> Lookup<&CascadeInputID, StyleNodeID> { + pub(super) fn cascade_input_lookup(&self, node: StyleNodeID) -> Lookup<&MatchAnswerID, StyleNodeID> { let Some(index) = node.element_index().map(|index| index as usize) else { return Lookup::Missing(node); }; match self.cascade_input_column.get(index) { - Some(cascade_input) if *cascade_input != CascadeInputID::default() => Lookup::Known(cascade_input), + Some(cascade_input) if *cascade_input != MatchAnswerID::default() => Lookup::Known(cascade_input), _ => Lookup::Missing(node), } } @@ -1190,17 +1215,18 @@ impl RetainedMatchAnswers { } } - pub(super) fn for_each_answer_containing_rule( + pub(super) fn for_each_answer_containing_any_rule( &self, catalog: &MatchAnswerCatalog, - rule: RuleID, + rules: &[RuleID], mut visit: impl FnMut(StyleNodeID), ) { + debug_assert!(rules.is_sorted()); self.for_each_answer_node(|node| { let index = node.element_index().unwrap() as usize; if catalog .retained_answer(self.column[index]) - .is_some_and(|answer| answer.iter().any(|matched| matched.rule == rule)) + .is_some_and(|answer| answer.iter().any(|matched| rules.binary_search(&matched.rule).is_ok())) { visit(node); } @@ -1254,11 +1280,10 @@ impl RetainedMatchAnswers { /// Matching scratch owned by one synchronous style traversal. pub(super) struct BatchMatchingTraversal { pub(super) root: StyleNodeID, - pub(super) batch: Option, + pub(super) batch: Option, pub(super) topology: Option, pub(super) reuse_retained_match_answers: bool, - pub(super) retained_answer_cascade_orders: Vec, - pub(super) retained_answer_cascade_order_bytes: u64, + pub(super) retained_answer_dispatch: Option>, pub(super) ancestor_requirements: AncestorRequirementsCache, pub(super) prefix_caches: Rc>, pub(super) match_workspace: MatchEvaluationWorkspace, @@ -1273,7 +1298,7 @@ pub(super) struct BatchMatchingTraversal { /// traversal. pub(super) struct PreparedBatchMatchingTraversal { pub(super) root: StyleNodeID, - pub(super) batch: Option, + pub(super) batch: Option, pub(super) topology: Option, pub(super) reuse_retained_match_answers: bool, pub(super) match_workspace: MatchEvaluationWorkspace, @@ -1295,7 +1320,7 @@ impl PreparedBatchMatchingTraversal { shallow []; cached []; nested [ - self.batch.as_ref().map_or(0, StyleNodeFacts::capacity_bytes), + self.batch.as_ref().map_or(0, MatchingFactBatch::capacity_bytes), self.topology.as_ref().map_or(0, TransactionTopology::capacity_bytes), self.match_workspace.capacity_bytes(), ]; @@ -1311,7 +1336,7 @@ pub(super) type RoutePruningStateCache = HashMap<(DispatchKey, u64), Option, + pub(super) cascade_input: Option, pub(super) matches: Option>, pub(super) cascade_winners_are_complete: bool, pub(super) observed: bool, @@ -1319,7 +1344,7 @@ pub(super) struct PublishedMatchAnswer { pub(super) struct PublishedMatchAnswers { pub(super) entries: Vec, - pub(super) shared_payloads: HashMap>, + pub(super) shared_payloads: HashMap>, pub(super) memory: MemoryLease, pub(super) match_element_calls_at_publication: u64, pub(super) discard_unobserved_retained_answers: bool, @@ -1400,7 +1425,7 @@ impl PublishedMatchAnswers { self.entries.push(entry); let added_bytes = (self.entries.capacity() - entries_capacity_before) * size_of::() + (self.shared_payloads.capacity() - shared_payload_capacity_before) - * (size_of::() + size_of::>() + 1) + * (size_of::() + size_of::>() + 1) + added_payload_bytes; let added_bytes = added_bytes as u64; self.memory.grow_required(memory, added_bytes); @@ -1520,6 +1545,7 @@ pub(super) struct PendingRuleDeclarationChange { pub(super) new_properties: Vec, } +#[derive(Clone)] pub(super) struct PendingRuleDeclarations { pub(super) declared: Vec, pub(super) complete: bool, @@ -1539,12 +1565,6 @@ pub(super) struct DiagnosticPlanCapture { pub(super) scoped: bool, } -#[derive(Clone, Copy, PartialEq, Eq)] -pub(super) enum BeforeFactRetention { - Retain, - Omit, -} - pub(crate) struct RecordedAtomMappings { pub atoms: Vec<(u64, u32)>, pub qualified_atoms: Vec<(u32, u32, u32)>, diff --git a/Libraries/LibWeb/Rust/src/css/style/column.rs b/Libraries/LibWeb/Rust/src/css/style/column.rs index 14985bebc856b..f8f1ccb9c7c39 100644 --- a/Libraries/LibWeb/Rust/src/css/style/column.rs +++ b/Libraries/LibWeb/Rust/src/css/style/column.rs @@ -91,6 +91,17 @@ pub(super) struct BitColumn { words: Vec, } +impl PartialEq for BitColumn { + fn eq(&self, other: &Self) -> bool { + let common_length = self.words.len().min(other.words.len()); + self.words[..common_length] == other.words[..common_length] + && self.words[common_length..].iter().all(|word| *word == 0) + && other.words[common_length..].iter().all(|word| *word == 0) + } +} + +impl Eq for BitColumn {} + impl ShallowCapacityBytes for BitColumn { fn shallow_capacity_bytes(&self) -> u64 { self.capacity_bytes() @@ -148,7 +159,41 @@ pub(super) trait RemovablePagedColumnPage: PagedColumnPage { fn remove(&mut self, index: usize) -> Option; } +pub(super) const PAGED_VALUE_PAGE_SHIFT: usize = 6; +const PAGED_VALUE_PAGE_SIZE: usize = 1 << PAGED_VALUE_PAGE_SHIFT; + +#[derive(Clone)] +pub(super) struct PagedValuePage { + known: u64, + values: [T; PAGED_VALUE_PAGE_SIZE], +} + +impl Default for PagedValuePage { + fn default() -> Self { + Self { + known: 0, + values: [T::default(); PAGED_VALUE_PAGE_SIZE], + } + } +} + +impl PagedColumnPage for PagedValuePage { + type Value = T; + + const SHIFT: usize = PAGED_VALUE_PAGE_SHIFT; + + fn get(&self, index: usize) -> Option { + (self.known & (1 << index) != 0).then_some(self.values[index]) + } + + fn insert(&mut self, index: usize, value: T) { + self.known |= 1 << index; + self.values[index] = value; + } +} + /// A sparse column whose directory and page accounting are shared across page layouts. +#[derive(Clone)] pub(super) struct PagedColumn { pages: Vec>>, page_count: usize, @@ -214,6 +259,12 @@ impl PagedColumn

{ } } +impl ShallowCapacityBytes for PagedColumn

{ + fn shallow_capacity_bytes(&self) -> u64 { + self.capacity_bytes() + } +} + impl PagedColumn

{ pub(super) fn remove(&mut self, index: usize) -> Option { self.pages @@ -280,3 +331,24 @@ pub(super) fn advance_epoch(epoch: &mut u32, step: u32, columns: &mut [&mut Epoc } *epoch } + +#[cfg(test)] +mod tests { + use super::BitColumn; + + #[test] + fn bit_column_equality_ignores_trailing_zero_words() { + let mut left = BitColumn::default(); + left.set(1, true); + left.set(70, true); + left.set(70, false); + + let mut right = BitColumn::default(); + right.set(1, true); + + assert!(left == right); + + right.set(70, true); + assert!(left != right); + } +} diff --git a/Libraries/LibWeb/Rust/src/css/style/compiler.rs b/Libraries/LibWeb/Rust/src/css/style/compiler.rs index 24c8c279f3188..879e35177b73c 100644 --- a/Libraries/LibWeb/Rust/src/css/style/compiler.rs +++ b/Libraries/LibWeb/Rust/src/css/style/compiler.rs @@ -37,6 +37,7 @@ use crate::css::selector::PseudoElementType; use crate::css::selector::PseudoElementValue; use crate::css::selector::SimpleSelector; +use super::atoms::synthetic_text_atom_key; use super::fnv::fnv1a64; use super::index::StyleAtomID; use super::relative_selector::RelativeAxis; @@ -1177,7 +1178,7 @@ impl<'a> SelectorCompiler<'a> { 0x41..=0x5a => *unit + 0x20, other => other, })); - usize::MAX - (hash as usize & 0x0000_ffff_ffff_ffff) + synthetic_text_atom_key(hash) } fn intern_ascii_lowercase_text(&mut self, text: &[u16]) -> StyleAtomID { @@ -1187,7 +1188,7 @@ impl<'a> SelectorCompiler<'a> { fn intern_text(&mut self, text: &[u16]) -> StyleAtomID { let hash = fnv1a64(text.iter().copied()); // Text keys live above the pointer range so they cannot alias an interned identity. - (self.intern)(usize::MAX - (hash as usize & 0x0000_ffff_ffff_ffff), None) + (self.intern)(synthetic_text_atom_key(hash), None) } fn compile_argument_list( @@ -1371,13 +1372,13 @@ impl<'a> SelectorCompiler<'a> { } /// The positive indexable feature a witness search can drive from, if the compound has one. - fn driving_feature_of(&self, compound: SelectorNodeID) -> Option { + fn driving_feature_of(&self, compound: SelectorNodeID) -> Option { let program = self.builder.program(); let feature_of = |test: FeatureTest| match test { - FeatureTest::Class(class) => Some(super::index::FeatureKey::Class(class)), - FeatureTest::Id(_) => Some(super::index::FeatureKey::Id), - FeatureTest::Attribute(attribute) => Some(super::index::FeatureKey::Attribute(attribute.folded)), - FeatureTest::TagName(_) => Some(super::index::FeatureKey::TagName), + FeatureTest::Class(class) => Some(super::index::LocalFeatureKey::Class(class)), + FeatureTest::Id(_) => Some(super::index::LocalFeatureKey::Id), + FeatureTest::Attribute(attribute) => Some(super::index::LocalFeatureKey::Attribute(attribute.folded)), + FeatureTest::TagName(_) => Some(super::index::LocalFeatureKey::TagName), // Neither enumerates a candidate set: every element is in some namespace, so driving a // witness search from one would be driving it from the whole document. FeatureTest::AnyElement | FeatureTest::Namespace(_) => None, diff --git a/Libraries/LibWeb/Rust/src/css/style/computed.rs b/Libraries/LibWeb/Rust/src/css/style/computed.rs index 1b6d6082404a3..590ee39bd3184 100644 --- a/Libraries/LibWeb/Rust/src/css/style/computed.rs +++ b/Libraries/LibWeb/Rust/src/css/style/computed.rs @@ -23,6 +23,7 @@ use std::num::NonZeroU32; use super::capacity::capacity_bytes; use super::cascade::CascadeStateID; +use super::column::BitColumn; use super::fast_hash::FastMap as HashMap; use super::fast_hash::fast_hasher; use super::intern_table::InternIdentity; @@ -134,6 +135,7 @@ struct ComputedFixedMetadata { counter_style_environment_identity: u64, } +#[repr(C)] pub(crate) struct InheritanceDependentValue { pub property: u16, pub value: RetainedStyleValueData, @@ -143,10 +145,26 @@ struct ComputedReconstructionMetadata { property_importance: Box<[u8]>, property_inheritance: Box<[u8]>, inheritance_dependent_values: Box<[InheritanceDependentValue]>, - inheritance_dependent_value_view: Box<[super::bridge::FfiInheritanceDependentValue]>, raw_cascaded_font_size: Option, } +impl ComputedReconstructionMetadata { + fn inheritance_dependent_value_view(&self) -> &[super::bridge::FfiInheritanceDependentValue] { + const { + assert!(size_of::() == size_of::()); + assert!( + align_of::() == align_of::() + ); + } + unsafe { + std::slice::from_raw_parts( + self.inheritance_dependent_values.as_ptr().cast(), + self.inheritance_dependent_values.len(), + ) + } + } +} + /// The interned form of one drive's computed longhand table: one retained /// data pointer per longhand (null where the drive stored no value), which is /// also the borrowed span the record view hands out. Provenance (the @@ -271,9 +289,14 @@ pub struct FinalStyleRecordID(u64); impl FinalStyleRecordID { const ANIMATION_OVERLAY_TAG: u64 = 1 << 63; + const MAX_BASE_GENERATION: u32 = (1 << 31) - 1; - fn base(style_record: StyleRecordID) -> Self { - Self(style_record.raw().into()) + fn base(style_record: StyleRecordID, generation: u32) -> Self { + assert!( + generation <= Self::MAX_BASE_GENERATION, + "base style-record generation space exhausted" + ); + Self((u64::from(generation) << 32) | u64::from(style_record.raw())) } fn animation_overlay(generation: u64) -> Self { @@ -290,8 +313,14 @@ impl FinalStyleRecordID { if self.0 & Self::ANIMATION_OVERLAY_TAG != 0 { return None; } - let raw = u32::try_from(self.0).ok()?; - Some(StyleRecordID(NonZeroU32::new(raw)?)) + if self.0 as u32 == 0 { + return None; + } + Some(StyleRecordID(NonZeroU32::new(self.0 as u32)?)) + } + + fn base_generation(self) -> u32 { + (self.0 >> 32) as u32 } } @@ -321,7 +350,7 @@ struct ComputedGroup { } struct ComputedGroupSet { - identities: Box<[ComputedGroupID]>, + identity_hash: u64, payloads: Box<[*const c_void]>, canonical_longhand_table: Option, } @@ -335,7 +364,219 @@ struct PublishedComputedInputs { reconstruction_metadata: ComputedReconstructionMetadataID, style_record: StyleRecordID, animation_overlay_slot: Option, - cascade_state: Option<(u64, CascadeStateID)>, +} + +#[derive(Clone, Copy)] +struct PseudoComputedRow { + kind: u8, + flags: u8, + assignment: Option, + cascade_versions: [u64; 3], + cascade_states: [CascadeStateID; 3], +} + +#[derive(Default)] +struct PublishedComputedColumns { + groups: Vec, + inherited_groups: Vec, + custom_properties: Vec, + fixed_metadata: Vec, + reconstruction_metadata: Vec, + animation_overlay_slots: Vec, + cascade_versions: Vec, + cascade_states: Vec, + flags: Vec, +} + +impl PublishedComputedColumns { + const ASSIGNED: u8 = 1; + const INHERITED_GROUP_SWAP_ELIGIBLE: u8 = 1 << 1; + const HAS_CASCADE_STATE: u8 = 1 << 2; + + fn ensure(&mut self, index: usize) { + if self.flags.len() > index { + return; + } + let len = index + .checked_add(1) + .expect("computed publication column space exhausted"); + self.groups.resize(len, 0); + self.inherited_groups.resize(len, 0); + self.custom_properties.resize(len, 0); + self.fixed_metadata.resize(len, 0); + self.reconstruction_metadata.resize(len, 0); + self.animation_overlay_slots.resize(len, 0); + self.cascade_versions.resize(len, 0); + self.cascade_states.resize(len, 0); + self.flags.resize(len, 0); + } + + fn is_assigned(&self, index: usize) -> bool { + self.flags.get(index).is_some_and(|flags| flags & Self::ASSIGNED != 0) + } + + fn groups(&self, index: usize) -> Option { + self.is_assigned(index).then(|| ComputedGroupSetID(self.groups[index])) + } + + fn inherited_groups(&self, index: usize) -> Option { + self.is_assigned(index) + .then(|| InheritedGroupSetID(self.inherited_groups[index])) + } + + fn custom_properties(&self, index: usize) -> Option { + self.is_assigned(index) + .then(|| CustomPropertyEnvironmentID(self.custom_properties[index])) + } + + fn fixed_metadata(&self, index: usize) -> Option { + self.is_assigned(index) + .then(|| ComputedFixedMetadataID(self.fixed_metadata[index])) + } + + fn reconstruction_metadata(&self, index: usize) -> Option { + self.is_assigned(index) + .then(|| ComputedReconstructionMetadataID(self.reconstruction_metadata[index])) + } + + fn animation_overlay_slot(&self, index: usize) -> Option { + let encoded = *self.animation_overlay_slots.get(index)?; + encoded.checked_sub(1) + } + + fn set_animation_overlay_slot(&mut self, index: usize, slot: Option) { + self.animation_overlay_slots[index] = slot.map_or(0, |slot| { + slot.checked_add(1) + .expect("animation overlay slot identity space exhausted") + }); + } + + fn inherited_group_swap_eligible(&self, index: usize) -> bool { + self.flags + .get(index) + .is_some_and(|flags| flags & Self::INHERITED_GROUP_SWAP_ELIGIBLE != 0) + } + + fn cascade_state(&self, index: usize) -> Option<(u64, CascadeStateID)> { + self.flags + .get(index) + .is_some_and(|flags| flags & Self::HAS_CASCADE_STATE != 0) + .then(|| (self.cascade_versions[index], CascadeStateID(self.cascade_states[index]))) + } + + fn replace_cascade_state( + &mut self, + index: usize, + state: Option<(u64, CascadeStateID)>, + ) -> Option<(u64, CascadeStateID)> { + let previous = self.cascade_state(index); + if let Some((version, state)) = state { + self.cascade_versions[index] = version; + self.cascade_states[index] = state.0; + self.flags[index] |= Self::HAS_CASCADE_STATE; + } else if let Some(flags) = self.flags.get_mut(index) { + *flags &= !Self::HAS_CASCADE_STATE; + } + previous + } + + fn publish(&mut self, index: usize, inputs: PublishedComputedInputs, inherited_group_swap_eligible: bool) { + self.ensure(index); + self.groups[index] = inputs.groups.0; + self.inherited_groups[index] = inputs.inherited_groups.0; + self.custom_properties[index] = inputs.custom_properties.0; + self.fixed_metadata[index] = inputs.fixed_metadata.0; + self.reconstruction_metadata[index] = inputs.reconstruction_metadata.0; + self.set_animation_overlay_slot(index, inputs.animation_overlay_slot); + self.flags[index] = (self.flags[index] & Self::HAS_CASCADE_STATE) + | Self::ASSIGNED + | if inherited_group_swap_eligible { + Self::INHERITED_GROUP_SWAP_ELIGIBLE + } else { + 0 + }; + } + + fn remove(&mut self, index: usize) -> Option { + let overlay = self.animation_overlay_slot(index); + if let Some(flags) = self.flags.get_mut(index) { + *flags = 0; + self.animation_overlay_slots[index] = 0; + } + overlay + } +} + +struct ComputedReachability { + groups: Vec, + sets: Vec, + inherited_sets: Vec, + custom_property_environments: Vec, + fixed_metadata: Vec, + reconstruction_metadata: Vec, + longhand_tables: Vec, + style_records: Vec, +} + +impl ComputedReachability { + fn mark(marks: &mut [bool], identity: Identity) { + marks[identity.index()] = true; + } +} + +#[derive(Clone, Copy)] +pub(super) struct ComputedGroupRetention { + pub retained: usize, + pub reachable: usize, +} + +impl PseudoComputedRow { + const PUBLISHED: u8 = 1; + const CURRENT_CASCADE: usize = 0; + const RETAINED_CASCADE: usize = 1; + const PENDING_CASCADE: usize = 2; + + fn new(kind: u8) -> Self { + Self { + kind, + flags: 0, + assignment: None, + cascade_versions: [0; 3], + cascade_states: [CascadeStateID(0); 3], + } + } + + fn is_published(&self) -> bool { + self.flags & Self::PUBLISHED != 0 + } + + fn set_published(&mut self, published: bool) { + self.flags = (self.flags & !Self::PUBLISHED) | if published { Self::PUBLISHED } else { 0 }; + } + + fn cascade_state(&self, index: usize) -> Option<(u64, CascadeStateID)> { + (self.flags & (1 << (index + 1)) != 0).then_some((self.cascade_versions[index], self.cascade_states[index])) + } + + fn replace_cascade_state( + &mut self, + index: usize, + state: Option<(u64, CascadeStateID)>, + ) -> Option<(u64, CascadeStateID)> { + let previous = self.cascade_state(index); + if let Some((version, state)) = state { + self.cascade_versions[index] = version; + self.cascade_states[index] = state; + self.flags |= 1 << (index + 1); + } else { + self.flags &= !(1 << (index + 1)); + } + previous + } + + fn is_empty(&self) -> bool { + self.flags == 0 && self.assignment.is_none() + } } pub struct ComputedGroupPublication { @@ -407,37 +648,32 @@ impl ComputedStyleTarget { pub struct ComputedGroupSets { groups: InternTable, sets: InternTable, - column: Vec>, inherited_sets: InternTable>, - inherited_column: Vec>, custom_property_environments: InternTable, - custom_property_environment_column: Vec>, computed_fixed_metadata: InternTable, - computed_fixed_metadata_column: Vec>, computed_reconstruction_metadata: InternTable, - computed_reconstruction_metadata_column: Vec>, computed_longhand_tables: InternTable, style_records: InternTable, + style_record_liveness: BitColumn, + style_record_generations: Vec, style_record_column: Vec>, + base_style_record_pins: HashMap, + columns: PublishedComputedColumns, // Recyclable animation overlays are deliberately separate from the permanent base records // above. Dense element assignments and sparse pseudo assignments pin at most one slot each. animation_overlay_slots: Vec>, animation_overlay_slots_by_record: HashMap, free_animation_overlay_slots: Vec, - animation_overlay_column: Vec>, - inherited_group_swap_eligible_column: Vec, live_animation_overlay_assignments: usize, next_animation_overlay_generation: u64, - cascade_state_column: Vec>, - pseudo_cascade_state_rows: HashMap<(StyleNodeID, u8), (u64, CascadeStateID)>, - pseudo_retained_cascade_rows: HashMap<(StyleNodeID, u8), (u64, CascadeStateID)>, - pseudo_assignments: HashMap<(StyleNodeID, u8), PublishedComputedInputs>, - pending_cascade_states: HashMap<(StyleNodeID, u8), (u64, CascadeStateID)>, - pseudo_kinds_by_node: HashMap>, + pending_cascade_states: HashMap, + pseudo_rows_by_node: HashMap>, group_set_nested_memory: MemoryLease, reconstruction_nested_memory: MemoryLease, animation_overlay_nested_memory: MemoryLease, pseudo_assignment_nested_memory: MemoryLease, + style_records_interned_since_reclamation: usize, + next_reclamation_after: usize, } impl Default for ComputedGroupSets { @@ -445,66 +681,145 @@ impl Default for ComputedGroupSets { Self { groups: InternTable::default(), sets: InternTable::default(), - column: Vec::new(), inherited_sets: InternTable::default(), - inherited_column: Vec::new(), custom_property_environments: InternTable::default(), - custom_property_environment_column: Vec::new(), computed_fixed_metadata: InternTable::default(), - computed_fixed_metadata_column: Vec::new(), computed_reconstruction_metadata: InternTable::default(), - computed_reconstruction_metadata_column: Vec::new(), computed_longhand_tables: InternTable::default(), style_records: InternTable::default(), + style_record_liveness: BitColumn::default(), + style_record_generations: Vec::new(), style_record_column: Vec::new(), + base_style_record_pins: HashMap::default(), + columns: PublishedComputedColumns::default(), animation_overlay_slots: Vec::new(), animation_overlay_slots_by_record: HashMap::default(), free_animation_overlay_slots: Vec::new(), - animation_overlay_column: Vec::new(), - inherited_group_swap_eligible_column: Vec::new(), live_animation_overlay_assignments: 0, next_animation_overlay_generation: 0, - cascade_state_column: Vec::new(), - pseudo_cascade_state_rows: HashMap::default(), - pseudo_retained_cascade_rows: HashMap::default(), - pseudo_assignments: HashMap::default(), pending_cascade_states: HashMap::default(), - pseudo_kinds_by_node: HashMap::default(), + pseudo_rows_by_node: HashMap::default(), group_set_nested_memory: MemoryLease::new(MemoryCategory::ComputedGroupSet), reconstruction_nested_memory: MemoryLease::new(MemoryCategory::ComputedReconstructionMetadata), animation_overlay_nested_memory: MemoryLease::new(MemoryCategory::AnimationOverlayRecord), pseudo_assignment_nested_memory: MemoryLease::new(MemoryCategory::ComputedPseudoAssignment), + style_records_interned_since_reclamation: 0, + next_reclamation_after: 1024, } } } impl ComputedGroupSets { + fn group_identity(&self, index: usize, payload: *const c_void) -> ComputedGroupID { + let key = (index, payload as usize); + self.groups + .find(content_hash(key), |_identity, group| { + (group.index, group.payload as usize) == key + }) + .expect("computed group-set payload names a live group") + } + + fn group_identities(&self, set: ComputedGroupSetID) -> Vec { + self.sets[set] + .payloads + .iter() + .copied() + .enumerate() + .map(|(index, payload)| self.group_identity(index, payload)) + .collect() + } + + fn pseudo_rows(&self, node: StyleNodeID) -> &[PseudoComputedRow] { + self.pseudo_rows_by_node.get(&node).map_or(&[], Box::as_ref) + } + + fn pseudo_row(&self, node: StyleNodeID, kind: u8) -> Option<&PseudoComputedRow> { + self.pseudo_rows(node).iter().find(|row| row.kind == kind) + } + + fn pseudo_row_mut(&mut self, node: StyleNodeID, kind: u8) -> Option<&mut PseudoComputedRow> { + self.pseudo_rows_by_node + .get_mut(&node) + .and_then(|rows| rows.iter_mut().find(|row| row.kind == kind)) + } + + fn ensure_pseudo_row(&mut self, node: StyleNodeID, kind: u8) -> &mut PseudoComputedRow { + let row_index = self + .pseudo_rows_by_node + .get(&node) + .and_then(|rows| rows.iter().position(|row| row.kind == kind)); + let row_index = row_index.unwrap_or_else(|| { + let mut rows = self + .pseudo_rows_by_node + .remove(&node) + .map_or_else(Vec::new, |rows| rows.into_vec()); + rows.push(PseudoComputedRow::new(kind)); + self.pseudo_assignment_nested_memory + .grow_committed(size_of::() as u64); + let row_index = rows.len() - 1; + self.pseudo_rows_by_node.insert(node, rows.into_boxed_slice()); + row_index + }); + &mut self + .pseudo_rows_by_node + .get_mut(&node) + .expect("pseudo row entry is live")[row_index] + } + + fn remove_empty_pseudo_row(&mut self, node: StyleNodeID, kind: u8) { + let Some(row_index) = self + .pseudo_rows_by_node + .get(&node) + .and_then(|rows| rows.iter().position(|row| row.kind == kind && row.is_empty())) + else { + return; + }; + let mut rows = self + .pseudo_rows_by_node + .remove(&node) + .expect("pseudo row entry is live") + .into_vec(); + rows.remove(row_index); + self.pseudo_assignment_nested_memory + .shrink_committed(size_of::() as u64); + if !rows.is_empty() { + self.pseudo_rows_by_node.insert(node, rows.into_boxed_slice()); + } + } + pub(super) fn assigned_style_record(&self, node: StyleNodeID) -> Option { let index = node.element_index()? as usize; let style_record = *self.style_record_column.get(index)?.as_ref()?; - Some(self.final_style_record(style_record, self.animation_overlay_column[index])) - } - - fn intern_group_set(&mut self, groups: Vec) -> (ComputedGroupSetID, bool) { - let hash = content_hash(&groups); - if let Some(identity) = self.sets.find(hash, |_identity, set| set.identities.as_ref() == groups) { + Some(self.final_style_record(style_record, self.columns.animation_overlay_slot(index))) + } + + fn intern_group_set(&mut self, groups: &[ComputedGroupID]) -> (ComputedGroupSetID, bool) { + let hash = content_hash(groups); + if let Some(identity) = self.sets.find(hash, |_identity, set| { + set.payloads.len() == groups.len() + && set + .payloads + .iter() + .zip(groups) + .all(|(&payload, &identity)| payload == self.groups[identity].payload) + }) { return (identity, false); } - let identity = - ComputedGroupSetID(u32::try_from(self.sets.len()).expect("computed group-set identity space exhausted")); + let identity = self.sets.take_free_identity().unwrap_or_else(|| { + ComputedGroupSetID(u32::try_from(self.sets.len()).expect("computed group-set identity space exhausted")) + }); let payloads = groups .iter() .map(|identity| self.groups[*identity].payload) .collect::>() .into_boxed_slice(); - let identities = groups.into_boxed_slice(); self.group_set_nested_memory - .grow_committed((size_of_val(identities.as_ref()) + size_of_val(payloads.as_ref())) as u64); + .grow_committed(size_of_val(payloads.as_ref()) as u64); self.sets.insert( hash, identity, ComputedGroupSet { - identities, + identity_hash: hash, payloads, canonical_longhand_table: None, }, @@ -512,6 +827,26 @@ impl ComputedGroupSets { (identity, true) } + fn intern_inherited_group_set(&mut self, groups: &[ComputedGroupID]) -> (InheritedGroupSetID, bool) { + let hash = content_hash(groups); + if let Some(identity) = self + .inherited_sets + .find(hash, |_identity, candidate| candidate.as_ref() == groups) + { + return (identity, false); + } + let identity = self.inherited_sets.take_free_identity().unwrap_or_else(|| { + InheritedGroupSetID( + u32::try_from(self.inherited_sets.len()).expect("inherited group-set identity space exhausted"), + ) + }); + let groups: Box<[ComputedGroupID]> = groups.into(); + self.group_set_nested_memory + .grow_committed(size_of_val(groups.as_ref()) as u64); + self.inherited_sets.insert(hash, identity, groups); + (identity, true) + } + fn intern_style_record(&mut self, record: StyleRecord) -> (StyleRecordID, bool) { let hash = content_hash(record); if let Some(identity) = self @@ -520,18 +855,47 @@ impl ComputedGroupSets { { return (identity, false); } - let raw = u32::try_from( - self.style_records - .len() + let identity = self.style_records.take_free_identity().unwrap_or_else(|| { + let raw = u32::try_from( + self.style_records + .len() + .checked_add(1) + .expect("base style-record identity space exhausted"), + ) + .expect("base style-record identity space exhausted"); + StyleRecordID(NonZeroU32::new(raw).expect("base style-record identities are nonzero")) + }); + if identity.index() == self.style_record_generations.len() { + self.style_record_generations.push(0); + } else { + let generation = &mut self.style_record_generations[identity.index()]; + *generation = generation .checked_add(1) - .expect("base style-record identity space exhausted"), - ) - .expect("base style-record identity space exhausted"); - let identity = StyleRecordID(NonZeroU32::new(raw).expect("base style-record identities are nonzero")); + .filter(|&generation| generation <= FinalStyleRecordID::MAX_BASE_GENERATION) + .expect("base style-record generation space exhausted"); + } self.style_records.insert(hash, identity, record); + let (changed, _) = self.style_record_liveness.set(identity.index(), true); + assert!(changed, "new base style-record identity must not already be live"); + self.style_records_interned_since_reclamation = self + .style_records_interned_since_reclamation + .checked_add(1) + .expect("style-record reclamation growth count overflow"); (identity, true) } + fn style_record_is_live(&self, identity: StyleRecordID) -> bool { + self.style_record_liveness.contains(identity.index()) + } + + fn style_record_generation_is_live(&self, identity: StyleRecordID, generation: u32) -> bool { + self.style_record_is_live(identity) && self.style_record_generations.get(identity.index()) == Some(&generation) + } + + fn final_base_style_record(&self, identity: StyleRecordID) -> FinalStyleRecordID { + FinalStyleRecordID::base(identity, self.style_record_generations[identity.index()]) + } + /// Interns the values of one drive's frozen computed longhand table, so /// equal value tuples share one identity and one retained copy. A /// value-equal previous table keeps its identity even when the fresh @@ -558,10 +922,12 @@ impl ComputedGroupSets { }) { return identity; } - let identity = ComputedLonghandTableID( - u32::try_from(self.computed_longhand_tables.len()) - .expect("computed longhand-table identity space exhausted"), - ); + let identity = self.computed_longhand_tables.take_free_identity().unwrap_or_else(|| { + ComputedLonghandTableID( + u32::try_from(self.computed_longhand_tables.len()) + .expect("computed longhand-table identity space exhausted"), + ) + }); let retained = unsafe { crate::css::computed_longhand_table::rust_computed_longhand_table_retain(table) }; self.reconstruction_nested_memory .grow_committed(size_of_val(values) as u64); @@ -593,10 +959,12 @@ impl ComputedGroupSets { }) { return identity; } - let identity = ComputedLonghandTableID( - u32::try_from(self.computed_longhand_tables.len()) - .expect("computed longhand-table identity space exhausted"), - ); + let identity = self.computed_longhand_tables.take_free_identity().unwrap_or_else(|| { + ComputedLonghandTableID( + u32::try_from(self.computed_longhand_tables.len()) + .expect("computed longhand-table identity space exhausted"), + ) + }); let value_view: Box<[*const c_void]> = values .iter() .map(|&value| match value.is_null() { @@ -631,36 +999,26 @@ impl ComputedGroupSets { } let index = node.element_index()? as usize; let parent_index = parent.element_index()? as usize; - if self - .inherited_group_swap_eligible_column - .get(index) - .copied() - .unwrap_or(0) - == 0 - || self.animation_overlay_column.get(index).copied().flatten().is_some() - || self - .animation_overlay_column - .get(parent_index) - .copied() - .flatten() - .is_some() + if !self.columns.inherited_group_swap_eligible(index) + || self.columns.animation_overlay_slot(index).is_some() + || self.columns.animation_overlay_slot(parent_index).is_some() || self.assigned_pseudo_kinds(node).next().is_some() { return None; } let old_style_record = *self.style_record_column.get(index)?.as_ref()?; - let old_record = *self.style_records.get_index(old_style_record.raw() as usize - 1)?; + let old_record = *self.style_records.get_index(old_style_record.index())?; let old_group_set = self.sets.get_index(old_record.groups.0 as usize)?; - let parent_inherited = *self.inherited_column.get(parent_index)?.as_ref()?; + let parent_inherited = self.columns.inherited_groups(parent_index)?; let parent_groups = self.inherited_sets.get_index(parent_inherited.0 as usize)?; - if parent_groups.len() != INHERITED_GROUP_COUNT || old_group_set.identities.len() < INHERITED_GROUP_COUNT { + if parent_groups.len() != INHERITED_GROUP_COUNT || old_group_set.payloads.len() < INHERITED_GROUP_COUNT { return None; } - let mut groups = old_group_set.identities.to_vec(); + let mut groups = self.group_identities(old_record.groups); groups[..INHERITED_GROUP_COUNT].copy_from_slice(parent_groups); - let group_set = self.intern_group_set(groups).0; + let group_set = self.intern_group_set(&groups).0; // The swap is only taken for a fully inheriting element, so every // inherited-by-default longhand's value is the parent's; the swapped // record's table is the old one with those slots replaced by the @@ -669,7 +1027,7 @@ impl ComputedGroupSets { // in practice) publish without one. let parent_style_record = self.style_record_column.get(parent_index).copied().flatten(); let parent_table = parent_style_record - .and_then(|record| self.style_records.get_index(record.raw() as usize - 1)) + .and_then(|record| self.style_records.get_index(record.index())) .and_then(|record| record.longhand_table); let longhand_table = match (old_record.longhand_table, parent_table) { (Some(old_table), Some(parent_table)) => { @@ -693,12 +1051,12 @@ impl ComputedGroupSets { longhand_table, }; let new_style_record = self.intern_style_record(new_record).0; - self.column[index] = Some(group_set); - self.inherited_column[index] = Some(parent_inherited); + self.columns.groups[index] = group_set.0; + self.columns.inherited_groups[index] = parent_inherited.0; self.style_record_column[index] = Some(new_style_record); Some(( - FinalStyleRecordID::base(old_style_record), - FinalStyleRecordID::base(new_style_record), + self.final_base_style_record(old_style_record), + self.final_base_style_record(new_style_record), )) } @@ -805,7 +1163,7 @@ impl ComputedGroupSets { } return AnimationOverlayPublication { slot: None, - final_style_record: FinalStyleRecordID::base(base_style_record), + final_style_record: self.final_base_style_record(base_style_record), slot_allocated: false, slot_released: current_slot.is_some(), record_updated: false, @@ -879,7 +1237,7 @@ impl ComputedGroupSets { animation_overlay_slot: Option, ) -> FinalStyleRecordID { animation_overlay_slot.map_or_else( - || FinalStyleRecordID::base(base_style_record), + || self.final_base_style_record(base_style_record), |slot| { self.animation_overlay_slots[slot as usize] .as_ref() @@ -919,14 +1277,12 @@ impl ComputedGroupSets { let previous_group_set = target.and_then(|target| { let ComputedStyleTarget { node, pseudo_kind } = target; if target.is_pseudo() { - self.pseudo_assignments - .get(&(node, pseudo_kind)) + self.pseudo_row(node, pseudo_kind) + .and_then(|row| row.assignment) .map(|inputs| inputs.groups) } else { node.element_index() - .and_then(|index| self.column.get(index as usize)) - .copied() - .flatten() + .and_then(|index| self.columns.groups(index as usize)) } }); let mut new_groups = 0; @@ -960,7 +1316,9 @@ impl ComputedGroupSets { continue; } let key = (index, payload as usize); - let previous_identity = previous_group_set.and_then(|set| self.sets[set].identities.get(index).copied()); + let previous_identity = previous_group_set + .and_then(|set| self.sets[set].payloads.get(index).copied()) + .map(|payload| self.group_identity(index, payload)); let previous_equal_identity = previous_identity .filter(|identity| style_group_payloads_equal(index, payload, self.groups[*identity].payload)); let identity = match previous_equal_identity { @@ -976,9 +1334,11 @@ impl ComputedGroupSets { Some(identity) => identity, None => { retain_group_payload(index, payload); - let identity = ComputedGroupID( - u32::try_from(self.groups.len()).expect("computed group identity space exhausted"), - ); + let identity = self.groups.take_free_identity().unwrap_or_else(|| { + ComputedGroupID( + u32::try_from(self.groups.len()).expect("computed group identity space exhausted"), + ) + }); self.groups .insert(content_hash(key), identity, ComputedGroup { index, payload }); self.group_set_nested_memory @@ -991,26 +1351,10 @@ impl ComputedGroupSets { groups.push(identity); } - let (identity, new_group_set) = self.intern_group_set(groups); + let (identity, new_group_set) = self.intern_group_set(&groups); - let inherited_groups = &self.sets[identity].identities[..inherited_group_count]; - let inherited_hash = content_hash(inherited_groups); - let existing_inherited = self - .inherited_sets - .find(inherited_hash, |_identity, groups| groups.as_ref() == inherited_groups); - let (inherited_identity, new_inherited_group_set) = match existing_inherited { - Some(identity) => (identity, false), - None => { - let identity = InheritedGroupSetID( - u32::try_from(self.inherited_sets.len()).expect("inherited group-set identity space exhausted"), - ); - let inherited_groups: Box<[ComputedGroupID]> = inherited_groups.into(); - self.group_set_nested_memory - .grow_committed(size_of_val(inherited_groups.as_ref()) as u64); - self.inherited_sets.insert(inherited_hash, identity, inherited_groups); - (identity, true) - } - }; + let (inherited_identity, new_inherited_group_set) = + self.intern_inherited_group_set(&groups[..inherited_group_count]); let custom_property_environment_hash = content_hash(custom_property_environment); let (custom_property_environment_identity, new_custom_property_environment) = match self @@ -1020,10 +1364,15 @@ impl ComputedGroupSets { }) { Some(identity) => (identity, false), None => { - let identity = CustomPropertyEnvironmentID( - u32::try_from(self.custom_property_environments.len()) - .expect("custom-property environment identity space exhausted"), - ); + let identity = self + .custom_property_environments + .take_free_identity() + .unwrap_or_else(|| { + CustomPropertyEnvironmentID( + u32::try_from(self.custom_property_environments.len()) + .expect("custom-property environment identity space exhausted"), + ) + }); self.custom_property_environments.insert( custom_property_environment_hash, identity, @@ -1044,10 +1393,12 @@ impl ComputedGroupSets { { Some(identity) => (identity, false), None => { - let identity = ComputedFixedMetadataID( - u32::try_from(self.computed_fixed_metadata.len()) - .expect("computed fixed-metadata identity space exhausted"), - ); + let identity = self.computed_fixed_metadata.take_free_identity().unwrap_or_else(|| { + ComputedFixedMetadataID( + u32::try_from(self.computed_fixed_metadata.len()) + .expect("computed fixed-metadata identity space exhausted"), + ) + }); self.computed_fixed_metadata .insert(content_hash(metadata), identity, metadata); (identity, true) @@ -1062,8 +1413,8 @@ impl ComputedGroupSets { let previous_longhand_table = target.and_then(|target| { let ComputedStyleTarget { node, pseudo_kind } = target; let style_record = if target.is_pseudo() { - self.pseudo_assignments - .get(&(node, pseudo_kind)) + self.pseudo_row(node, pseudo_kind) + .and_then(|row| row.assignment) .map(|inputs| inputs.style_record) } else { node.element_index() @@ -1071,9 +1422,7 @@ impl ComputedGroupSets { .copied() .flatten() }?; - self.style_records - .get_index(style_record.raw() as usize - 1)? - .longhand_table + self.style_records.get_index(style_record.index())?.longhand_table }); let canonical_longhand_table = self.sets[identity].canonical_longhand_table; let longhand_table_identity = longhand_table @@ -1108,10 +1457,15 @@ impl ComputedGroupSets { match existing_reconstruction { Some(identity) => (identity, false), None => { - let identity = ComputedReconstructionMetadataID( - u32::try_from(self.computed_reconstruction_metadata.len()) - .expect("computed reconstruction-metadata identity space exhausted"), - ); + let identity = self + .computed_reconstruction_metadata + .take_free_identity() + .unwrap_or_else(|| { + ComputedReconstructionMetadataID( + u32::try_from(self.computed_reconstruction_metadata.len()) + .expect("computed reconstruction-metadata identity space exhausted"), + ) + }); let inheritance_dependent_values: Box<[InheritanceDependentValue]> = inheritance_dependent_values .into_iter() .map(|(property, value)| InheritanceDependentValue { @@ -1119,28 +1473,18 @@ impl ComputedGroupSets { value: retain_style_value(value), }) .collect(); - let inheritance_dependent_value_view: Box<[super::bridge::FfiInheritanceDependentValue]> = - inheritance_dependent_values - .iter() - .map(|entry| super::bridge::FfiInheritanceDependentValue { - property: entry.property, - value: entry.value.pointer().cast(), - }) - .collect(); let raw_cascaded_font_size = (!reconstruction_metadata.raw_cascaded_font_size.is_null()) .then(|| retain_style_value(reconstruction_metadata.raw_cascaded_font_size)); let metadata = ComputedReconstructionMetadata { property_importance: reconstruction_metadata.property_importance.into(), property_inheritance: reconstruction_metadata.property_inheritance.into(), inheritance_dependent_values, - inheritance_dependent_value_view, raw_cascaded_font_size, }; self.reconstruction_nested_memory.grow_committed( (metadata.property_importance.len() + metadata.property_inheritance.len() - + size_of_val(metadata.inheritance_dependent_values.as_ref()) - + size_of_val(metadata.inheritance_dependent_value_view.as_ref())) + + size_of_val(metadata.inheritance_dependent_values.as_ref())) as u64, ); self.computed_reconstruction_metadata @@ -1168,8 +1512,7 @@ impl ComputedGroupSets { previous_style_record_identity, animation_overlay_publication, ) = if let Some(ComputedStyleTarget { node, pseudo_kind }) = target.filter(|target| target.is_pseudo()) { - let key = (node, pseudo_kind); - let previous = self.pseudo_assignments.get(&key).copied(); + let previous = self.pseudo_row(node, pseudo_kind).and_then(|row| row.assignment); let previous_style_record_identity = previous .map(|previous| self.final_style_record(previous.style_record, previous.animation_overlay_slot)); let animation_overlay_publication = self.update_animation_overlay( @@ -1179,26 +1522,17 @@ impl ComputedGroupSets { &mut animated_properties, animation_overlay_payloads, ); - if previous.is_none() { - let kinds = self.pseudo_kinds_by_node.entry(node).or_default(); - let capacity_before = kinds.capacity(); - kinds.push(pseudo_kind); - self.pseudo_assignment_nested_memory - .grow_committed((kinds.capacity() - capacity_before) as u64); - } - self.pseudo_assignments.insert( - key, - PublishedComputedInputs { - groups: identity, - inherited_groups: inherited_identity, - custom_properties: custom_property_environment_identity, - fixed_metadata: computed_fixed_metadata_identity, - reconstruction_metadata: computed_reconstruction_metadata_identity, - style_record: style_record_identity, - animation_overlay_slot: animation_overlay_publication.slot, - cascade_state: previous.and_then(|previous| previous.cascade_state), - }, - ); + let row = self.ensure_pseudo_row(node, pseudo_kind); + row.set_published(true); + row.assignment = Some(PublishedComputedInputs { + groups: identity, + inherited_groups: inherited_identity, + custom_properties: custom_property_environment_identity, + fixed_metadata: computed_fixed_metadata_identity, + reconstruction_metadata: computed_reconstruction_metadata_identity, + style_record: style_record_identity, + animation_overlay_slot: animation_overlay_publication.slot, + }); ( previous.is_none_or(|previous| previous.groups != identity), previous.is_none_or(|previous| previous.inherited_groups != inherited_identity), @@ -1212,43 +1546,42 @@ impl ComputedGroupSets { ) } else if let Some(ComputedStyleTarget { node, .. }) = target { let index = node.element_index().expect("only elements publish computed groups") as usize; - if self.column.len() <= index { - self.column.resize(index + 1, None); - self.inherited_column.resize(index + 1, None); - self.custom_property_environment_column.resize(index + 1, None); - self.computed_fixed_metadata_column.resize(index + 1, None); - self.computed_reconstruction_metadata_column.resize(index + 1, None); + self.columns.ensure(index); + if self.style_record_column.len() <= index { self.style_record_column.resize(index + 1, None); - self.animation_overlay_column.resize(index + 1, None); - self.inherited_group_swap_eligible_column.resize(index + 1, 0); - self.cascade_state_column.resize(index + 1, None); } let previous_style_record_identity = self.style_record_column[index] - .map(|style_record| self.final_style_record(style_record, self.animation_overlay_column[index])); + .map(|style_record| self.final_style_record(style_record, self.columns.animation_overlay_slot(index))); let animation_overlay_publication = self.update_animation_overlay( - self.animation_overlay_column[index], + self.columns.animation_overlay_slot(index), style_record_identity, animation_overlay_identity, &mut animated_properties, animation_overlay_payloads, ); let changed = ( - self.column[index] != Some(identity), - self.inherited_column[index] != Some(inherited_identity), - self.custom_property_environment_column[index] != Some(custom_property_environment_identity), - self.computed_fixed_metadata_column[index] != Some(computed_fixed_metadata_identity), - self.computed_reconstruction_metadata_column[index] != Some(computed_reconstruction_metadata_identity), + self.columns.groups(index) != Some(identity), + self.columns.inherited_groups(index) != Some(inherited_identity), + self.columns.custom_properties(index) != Some(custom_property_environment_identity), + self.columns.fixed_metadata(index) != Some(computed_fixed_metadata_identity), + self.columns.reconstruction_metadata(index) != Some(computed_reconstruction_metadata_identity), previous_style_record_identity, animation_overlay_publication, ); - self.column[index] = Some(identity); - self.inherited_column[index] = Some(inherited_identity); - self.custom_property_environment_column[index] = Some(custom_property_environment_identity); - self.computed_fixed_metadata_column[index] = Some(computed_fixed_metadata_identity); - self.computed_reconstruction_metadata_column[index] = Some(computed_reconstruction_metadata_identity); + self.columns.publish( + index, + PublishedComputedInputs { + groups: identity, + inherited_groups: inherited_identity, + custom_properties: custom_property_environment_identity, + fixed_metadata: computed_fixed_metadata_identity, + reconstruction_metadata: computed_reconstruction_metadata_identity, + style_record: style_record_identity, + animation_overlay_slot: animation_overlay_publication.slot, + }, + inherited_group_swap_eligible, + ); self.style_record_column[index] = Some(style_record_identity); - self.animation_overlay_column[index] = animation_overlay_publication.slot; - self.inherited_group_swap_eligible_column[index] = u8::from(inherited_group_swap_eligible); changed } else { assert_eq!( @@ -1264,7 +1597,7 @@ impl ComputedGroupSets { None, AnimationOverlayPublication { slot: None, - final_style_record: FinalStyleRecordID::base(style_record_identity), + final_style_record: self.final_base_style_record(style_record_identity), slot_allocated: false, slot_released: false, record_updated: false, @@ -1308,9 +1641,13 @@ impl ComputedGroupSets { ) -> Option { let final_style_record = FinalStyleRecordID(raw_style_record); let requested_style_record_identity = final_style_record.base_record()?; + assert!( + self.style_record_generation_is_live(requested_style_record_identity, final_style_record.base_generation()), + "base style-record is not live" + ); let previous_base_style_record_identity = if target.is_pseudo() { - self.pseudo_assignments - .get(&(target.node, target.pseudo_kind)) + self.pseudo_row(target.node, target.pseudo_kind) + .and_then(|row| row.assignment) .map(|assignment| assignment.style_record) } else { target @@ -1327,13 +1664,10 @@ impl ComputedGroupSets { let style_record_identity = previous_base_style_record_identity .filter(|&previous| self.style_records_equal_by_value(previous, requested_style_record_identity)) .unwrap_or(requested_style_record_identity); - let record = *self.style_records.get_index(style_record_identity.raw() as usize - 1)?; - let inherited_groups = &self.sets[record.groups].identities[..inherited_group_count]; - let inherited_identity = self - .inherited_sets - .find(content_hash(inherited_groups), |_identity, groups| { - groups.as_ref() == inherited_groups - })?; + let record = *self.style_records.get_index(style_record_identity.index())?; + let group_identities = self.group_identities(record.groups); + let inherited_groups = &group_identities[..inherited_group_count]; + let (inherited_identity, new_inherited_group_set) = self.intern_inherited_group_set(inherited_groups); let is_pseudo = target.is_pseudo(); let ( @@ -1345,8 +1679,9 @@ impl ComputedGroupSets { previous_style_record_identity, animation_overlay_publication, ) = if target.is_pseudo() { - let key = (target.node, target.pseudo_kind); - let previous = self.pseudo_assignments.get(&key).copied(); + let previous = self + .pseudo_row(target.node, target.pseudo_kind) + .and_then(|row| row.assignment); let previous_style_record_identity = previous .map(|previous| self.final_style_record(previous.style_record, previous.animation_overlay_slot)); let mut animated_properties = None; @@ -1357,26 +1692,17 @@ impl ComputedGroupSets { &mut animated_properties, &[], ); - if previous.is_none() { - let kinds = self.pseudo_kinds_by_node.entry(target.node).or_default(); - let capacity_before = kinds.capacity(); - kinds.push(target.pseudo_kind); - self.pseudo_assignment_nested_memory - .grow_committed((kinds.capacity() - capacity_before) as u64); - } - self.pseudo_assignments.insert( - key, - PublishedComputedInputs { - groups: record.groups, - inherited_groups: inherited_identity, - custom_properties: record.custom_properties, - fixed_metadata: record.fixed_metadata, - reconstruction_metadata: record.reconstruction_metadata, - style_record: style_record_identity, - animation_overlay_slot: None, - cascade_state: previous.and_then(|previous| previous.cascade_state), - }, - ); + let row = self.ensure_pseudo_row(target.node, target.pseudo_kind); + row.set_published(true); + row.assignment = Some(PublishedComputedInputs { + groups: record.groups, + inherited_groups: inherited_identity, + custom_properties: record.custom_properties, + fixed_metadata: record.fixed_metadata, + reconstruction_metadata: record.reconstruction_metadata, + style_record: style_record_identity, + animation_overlay_slot: None, + }); ( previous.is_none_or(|previous| previous.groups != record.groups), previous.is_none_or(|previous| previous.inherited_groups != inherited_identity), @@ -1391,44 +1717,43 @@ impl ComputedGroupSets { .node .element_index() .expect("only elements publish computed groups") as usize; - if self.column.len() <= index { - self.column.resize(index + 1, None); - self.inherited_column.resize(index + 1, None); - self.custom_property_environment_column.resize(index + 1, None); - self.computed_fixed_metadata_column.resize(index + 1, None); - self.computed_reconstruction_metadata_column.resize(index + 1, None); + self.columns.ensure(index); + if self.style_record_column.len() <= index { self.style_record_column.resize(index + 1, None); - self.animation_overlay_column.resize(index + 1, None); - self.inherited_group_swap_eligible_column.resize(index + 1, 0); - self.cascade_state_column.resize(index + 1, None); } let previous_style_record_identity = self.style_record_column[index] - .map(|style_record| self.final_style_record(style_record, self.animation_overlay_column[index])); + .map(|style_record| self.final_style_record(style_record, self.columns.animation_overlay_slot(index))); let mut animated_properties = None; let animation_overlay_publication = self.update_animation_overlay( - self.animation_overlay_column[index], + self.columns.animation_overlay_slot(index), style_record_identity, 0, &mut animated_properties, &[], ); let changed = ( - self.column[index] != Some(record.groups), - self.inherited_column[index] != Some(inherited_identity), - self.custom_property_environment_column[index] != Some(record.custom_properties), - self.computed_fixed_metadata_column[index] != Some(record.fixed_metadata), - self.computed_reconstruction_metadata_column[index] != Some(record.reconstruction_metadata), + self.columns.groups(index) != Some(record.groups), + self.columns.inherited_groups(index) != Some(inherited_identity), + self.columns.custom_properties(index) != Some(record.custom_properties), + self.columns.fixed_metadata(index) != Some(record.fixed_metadata), + self.columns.reconstruction_metadata(index) != Some(record.reconstruction_metadata), previous_style_record_identity, animation_overlay_publication, ); - self.column[index] = Some(record.groups); - self.inherited_column[index] = Some(inherited_identity); - self.custom_property_environment_column[index] = Some(record.custom_properties); - self.computed_fixed_metadata_column[index] = Some(record.fixed_metadata); - self.computed_reconstruction_metadata_column[index] = Some(record.reconstruction_metadata); + self.columns.publish( + index, + PublishedComputedInputs { + groups: record.groups, + inherited_groups: inherited_identity, + custom_properties: record.custom_properties, + fixed_metadata: record.fixed_metadata, + reconstruction_metadata: record.reconstruction_metadata, + style_record: style_record_identity, + animation_overlay_slot: None, + }, + inherited_group_swap_eligible, + ); self.style_record_column[index] = Some(style_record_identity); - self.animation_overlay_column[index] = None; - self.inherited_group_swap_eligible_column[index] = u8::from(inherited_group_swap_eligible); changed }; @@ -1438,7 +1763,7 @@ impl ComputedGroupSets { new_groups: 0, canonical_output_groups_reused: 0, new_group_set: false, - new_inherited_group_set: false, + new_inherited_group_set, new_custom_property_environment: false, new_computed_fixed_metadata: false, new_computed_reconstruction_metadata: false, @@ -1462,10 +1787,10 @@ impl ComputedGroupSets { if first == second { return true; } - let Some(first) = self.style_records.get_index(first.raw() as usize - 1) else { + let Some(first) = self.style_records.get_index(first.index()) else { return false; }; - let Some(second) = self.style_records.get_index(second.raw() as usize - 1) else { + let Some(second) = self.style_records.get_index(second.index()) else { return false; }; if first.custom_properties != second.custom_properties @@ -1474,17 +1799,14 @@ impl ComputedGroupSets { { return false; } - let first_groups = &self.sets[first.groups].identities; - let second_groups = &self.sets[second.groups].identities; + let first_groups = &self.sets[first.groups].payloads; + let second_groups = &self.sets[second.groups].payloads; if first_groups.len() != second_groups.len() || first_groups .iter() .zip(second_groups) .enumerate() - .any(|(index, (&first, &second))| { - first != second - && !style_group_payloads_equal(index, self.groups[first].payload, self.groups[second].payload) - }) + .any(|(index, (&first, &second))| first != second && !style_group_payloads_equal(index, first, second)) { return false; } @@ -1503,57 +1825,64 @@ impl ComputedGroupSets { } pub fn set_pending_cascade_state(&mut self, target: ComputedStyleTarget, state: (u64, CascadeStateID)) { - self.pending_cascade_states - .insert((target.node, target.pseudo_kind), state); + if !target.is_pseudo() { + self.pending_cascade_states.insert(target.node, state); + return; + } + self.ensure_pseudo_row(target.node, target.pseudo_kind) + .replace_cascade_state(PseudoComputedRow::PENDING_CASCADE, Some(state)); } pub fn take_pending_cascade_state(&mut self, target: ComputedStyleTarget) -> Option<(u64, CascadeStateID)> { - self.pending_cascade_states.remove(&(target.node, target.pseudo_kind)) + if !target.is_pseudo() { + return self.pending_cascade_states.remove(&target.node); + } + let state = self + .pseudo_row_mut(target.node, target.pseudo_kind) + .and_then(|row| row.replace_cascade_state(PseudoComputedRow::PENDING_CASCADE, None)); + self.remove_empty_pseudo_row(target.node, target.pseudo_kind); + state } #[must_use] pub fn cascade_state(&self, target: ComputedStyleTarget) -> Option<(u64, CascadeStateID)> { if target.is_pseudo() { return self - .pseudo_cascade_state_rows - .get(&(target.node, target.pseudo_kind)) - .copied(); + .pseudo_row(target.node, target.pseudo_kind)? + .cascade_state(PseudoComputedRow::CURRENT_CASCADE); } target .node .element_index() - .and_then(|index| self.cascade_state_column.get(index as usize).copied().flatten()) + .and_then(|index| self.columns.cascade_state(index as usize)) } pub fn pseudo_retained_cascade_states( &self, node: StyleNodeID, ) -> impl Iterator + '_ { - self.pseudo_retained_cascade_rows - .iter() - .filter_map(move |(&(row_node, pseudo_kind), &state)| (row_node == node).then_some((pseudo_kind, state))) + self.pseudo_rows(node).iter().filter_map(|row| { + row.cascade_state(PseudoComputedRow::RETAINED_CASCADE) + .map(|state| (row.kind, state)) + }) } #[must_use] pub fn pseudo_retained_cascade_state(&self, node: StyleNodeID, pseudo_kind: u8) -> Option<(u64, CascadeStateID)> { - self.pseudo_retained_cascade_rows.get(&(node, pseudo_kind)).copied() + self.pseudo_row(node, pseudo_kind)? + .cascade_state(PseudoComputedRow::RETAINED_CASCADE) } /// The pseudo-element kinds this node holds published computed styles for. pub fn assigned_pseudo_kinds(&self, node: StyleNodeID) -> impl Iterator + '_ { - self.pseudo_kinds_by_node - .get(&node) - .into_iter() - .flat_map(|kinds| kinds.iter().copied()) + self.pseudo_rows(node) + .iter() + .filter_map(|row| row.is_published().then_some(row.kind)) } #[cfg(test)] pub fn record_pseudo_kind_for_test(&mut self, node: StyleNodeID, pseudo_kind: u8) { - let kinds = self.pseudo_kinds_by_node.entry(node).or_default(); - let capacity_before = kinds.capacity(); - kinds.push(pseudo_kind); - self.pseudo_assignment_nested_memory - .grow_committed((kinds.capacity() - capacity_before) as u64); + self.ensure_pseudo_row(node, pseudo_kind).set_published(true); } fn specified_value_dependency_mask( @@ -1562,16 +1891,14 @@ impl ComputedGroupSets { depends_on_input: impl Fn(&RetainedStyleValueData) -> bool, ) -> Option { let reconstruction_metadata = if target.is_pseudo() { - self.pseudo_assignments - .get(&(target.node, target.pseudo_kind)) + self.pseudo_row(target.node, target.pseudo_kind) + .and_then(|row| row.assignment) .map(|assignment| assignment.reconstruction_metadata) } else { target .node .element_index() - .and_then(|index| self.computed_reconstruction_metadata_column.get(index as usize)) - .copied() - .flatten() + .and_then(|index| self.columns.reconstruction_metadata(index as usize)) }?; let mask = self.computed_reconstruction_metadata[reconstruction_metadata.0 as usize] .inheritance_dependent_values @@ -1590,16 +1917,14 @@ impl ComputedGroupSets { depends_on_input: impl Fn(&RetainedStyleValueData) -> bool, ) -> Option<[u64; 6]> { let reconstruction_metadata = if target.is_pseudo() { - self.pseudo_assignments - .get(&(target.node, target.pseudo_kind)) + self.pseudo_row(target.node, target.pseudo_kind) + .and_then(|row| row.assignment) .map(|assignment| assignment.reconstruction_metadata) } else { target .node .element_index() - .and_then(|index| self.computed_reconstruction_metadata_column.get(index as usize)) - .copied() - .flatten() + .and_then(|index| self.columns.reconstruction_metadata(index as usize)) }?; let mut properties = [0u64; 6]; for entry in @@ -1666,88 +1991,54 @@ impl ComputedGroupSets { cascade_state: (u64, CascadeStateID), ) -> Option<(u64, CascadeStateID)> { if target.is_pseudo() { - let assignment = self - .pseudo_assignments - .get_mut(&(target.node, target.pseudo_kind)) - .expect("a pseudo style must be published before its cascade state is bound"); - assignment.cascade_state = Some(cascade_state); - return self - .pseudo_cascade_state_rows - .insert((target.node, target.pseudo_kind), cascade_state); + let row = self.ensure_pseudo_row(target.node, target.pseudo_kind); + assert!( + row.assignment.is_some(), + "a pseudo style must be published before its cascade state is bound" + ); + return row.replace_cascade_state(PseudoComputedRow::CURRENT_CASCADE, Some(cascade_state)); } let index = target .node .element_index() .expect("only elements publish computed groups") as usize; - debug_assert!(index < self.cascade_state_column.len()); - self.cascade_state_column[index].replace(cascade_state) + debug_assert!(index < self.columns.flags.len()); + self.columns.replace_cascade_state(index, Some(cascade_state)) } /// Forget the exact cascade state behind a style published through a path that did not run the /// cascade, so a later comparison cannot use the state behind an older publication. pub fn clear_cascade_state(&mut self, target: ComputedStyleTarget) { if target.is_pseudo() { - self.pseudo_cascade_state_rows - .remove(&(target.node, target.pseudo_kind)); - self.pseudo_retained_cascade_rows - .remove(&(target.node, target.pseudo_kind)); - if let Some(assignment) = self.pseudo_assignments.get_mut(&(target.node, target.pseudo_kind)) { - assignment.cascade_state = None; + if let Some(row) = self.pseudo_row_mut(target.node, target.pseudo_kind) { + row.replace_cascade_state(PseudoComputedRow::CURRENT_CASCADE, None); + row.replace_cascade_state(PseudoComputedRow::RETAINED_CASCADE, None); } + self.remove_empty_pseudo_row(target.node, target.pseudo_kind); return; } let Some(index) = target.node.element_index().map(|index| index as usize) else { return; }; - if let Some(slot) = self.cascade_state_column.get_mut(index) { - *slot = None; - } + self.columns.replace_cascade_state(index, None); } pub fn remove(&mut self, node: StyleNodeID) { let Some(index) = node.element_index().map(|index| index as usize) else { return; }; - if let Some(slot) = self.column.get_mut(index) { - *slot = None; - } - if let Some(slot) = self.inherited_column.get_mut(index) { - *slot = None; - } - if let Some(slot) = self.custom_property_environment_column.get_mut(index) { - *slot = None; - } - if let Some(slot) = self.computed_fixed_metadata_column.get_mut(index) { - *slot = None; - } - if let Some(slot) = self.computed_reconstruction_metadata_column.get_mut(index) { - *slot = None; - } if let Some(slot) = self.style_record_column.get_mut(index) { *slot = None; } - if let Some(slot) = self.animation_overlay_column.get_mut(index).and_then(Option::take) { + if let Some(slot) = self.columns.remove(index) { self.release_animation_overlay_assignment(slot); } - if let Some(slot) = self.inherited_group_swap_eligible_column.get_mut(index) { - *slot = 0; - } - if let Some(slot) = self.cascade_state_column.get_mut(index) { - *slot = None; - } - self.pending_cascade_states - .retain(|(pending_node, _), _| *pending_node != node); - self.pseudo_cascade_state_rows - .retain(|(row_node, _), _| *row_node != node); - self.pseudo_retained_cascade_rows - .retain(|(row_node, _), _| *row_node != node); - if let Some(kinds) = self.pseudo_kinds_by_node.remove(&node) { + self.pending_cascade_states.remove(&node); + if let Some(rows) = self.pseudo_rows_by_node.remove(&node) { self.pseudo_assignment_nested_memory - .shrink_committed(kinds.capacity() as u64); - for pseudo_kind in kinds { - if let Some(removed) = self.pseudo_assignments.remove(&(node, pseudo_kind)) - && let Some(slot) = removed.animation_overlay_slot - { + .shrink_committed(size_of_val(rows.as_ref()) as u64); + for row in rows.into_vec() { + if let Some(slot) = row.assignment.and_then(|assignment| assignment.animation_overlay_slot) { self.release_animation_overlay_assignment(slot); } } @@ -1755,32 +2046,22 @@ impl ComputedGroupSets { } pub fn remove_pseudo(&mut self, node: StyleNodeID, pseudo_kind: u8) -> Option { - let removed = self.pseudo_assignments.remove(&(node, pseudo_kind))?; + let row = self.pseudo_row_mut(node, pseudo_kind)?; + let removed = row.assignment.take()?; + row.set_published(false); + row.replace_cascade_state(PseudoComputedRow::PENDING_CASCADE, None); let final_style_record = self.final_style_record(removed.style_record, removed.animation_overlay_slot); if let Some(slot) = removed.animation_overlay_slot { self.release_animation_overlay_assignment(slot); } - self.pending_cascade_states.remove(&(node, pseudo_kind)); - let mut remove_node_entry = false; - if let Some(kinds) = self.pseudo_kinds_by_node.get_mut(&node) { - kinds.retain(|kind| *kind != pseudo_kind); - remove_node_entry = kinds.is_empty(); - } - if remove_node_entry { - let kinds = self - .pseudo_kinds_by_node - .remove(&node) - .expect("pseudo-kind index entry is live"); - self.pseudo_assignment_nested_memory - .shrink_committed(kinds.capacity() as u64); - } + self.remove_empty_pseudo_row(node, pseudo_kind); Some(final_style_record) } pub fn observe_absent_pseudo_cascade_state(&mut self, target: ComputedStyleTarget, state: (u64, CascadeStateID)) { debug_assert!(target.is_pseudo()); - self.pseudo_cascade_state_rows - .insert((target.node, target.pseudo_kind), state); + self.ensure_pseudo_row(target.node, target.pseudo_kind) + .replace_cascade_state(PseudoComputedRow::CURRENT_CASCADE, Some(state)); } pub fn observe_pseudo_retained_cascade_state( @@ -1789,13 +2070,9 @@ impl ComputedGroupSets { state: Option<(u64, CascadeStateID)>, ) { debug_assert!(target.is_pseudo()); - if let Some(state) = state { - self.pseudo_retained_cascade_rows - .insert((target.node, target.pseudo_kind), state); - } else { - self.pseudo_retained_cascade_rows - .remove(&(target.node, target.pseudo_kind)); - } + self.ensure_pseudo_row(target.node, target.pseudo_kind) + .replace_cascade_state(PseudoComputedRow::RETAINED_CASCADE, state); + self.remove_empty_pseudo_row(target.node, target.pseudo_kind); } #[must_use] @@ -1804,9 +2081,9 @@ impl ComputedGroupSets { shallow [ self.groups, self.sets, - self.column, + self.columns.groups, self.inherited_sets, - self.inherited_column, + self.columns.inherited_groups, ]; cached [self.group_set_nested_memory.bytes()]; nested []; @@ -1817,7 +2094,7 @@ impl ComputedGroupSets { #[must_use] pub fn custom_property_environment_capacity_bytes(&self) -> u64 { capacity_bytes! { - shallow [self.custom_property_environments, self.custom_property_environment_column]; + shallow [self.custom_property_environments, self.columns.custom_properties]; cached []; nested []; skip []; @@ -1829,7 +2106,7 @@ impl ComputedGroupSets { capacity_bytes! { shallow [ self.computed_fixed_metadata, - self.computed_fixed_metadata_column, + self.columns.fixed_metadata, ]; cached []; nested []; @@ -1842,7 +2119,7 @@ impl ComputedGroupSets { capacity_bytes! { shallow [ self.computed_reconstruction_metadata, - self.computed_reconstruction_metadata_column, + self.columns.reconstruction_metadata, self.computed_longhand_tables, ]; cached [self.reconstruction_nested_memory.bytes()]; @@ -1856,9 +2133,13 @@ impl ComputedGroupSets { capacity_bytes! { shallow [ self.style_records, + self.style_record_liveness, + self.style_record_generations, self.style_record_column, - self.inherited_group_swap_eligible_column, - self.cascade_state_column, + self.base_style_record_pins, + self.columns.cascade_versions, + self.columns.cascade_states, + self.columns.flags, self.pending_cascade_states, ]; cached []; @@ -1874,7 +2155,7 @@ impl ComputedGroupSets { self.animation_overlay_slots, self.animation_overlay_slots_by_record, self.free_animation_overlay_slots, - self.animation_overlay_column, + self.columns.animation_overlay_slots, ]; cached [self.animation_overlay_nested_memory.bytes()]; nested []; @@ -1887,16 +2168,272 @@ impl ComputedGroupSets { self.live_animation_overlay_assignments } + fn reachability(&self) -> ComputedReachability { + let mut reachable = ComputedReachability { + groups: vec![false; self.groups.len()], + sets: vec![false; self.sets.len()], + inherited_sets: vec![false; self.inherited_sets.len()], + custom_property_environments: vec![false; self.custom_property_environments.len()], + fixed_metadata: vec![false; self.computed_fixed_metadata.len()], + reconstruction_metadata: vec![false; self.computed_reconstruction_metadata.len()], + longhand_tables: vec![false; self.computed_longhand_tables.len()], + style_records: vec![false; self.style_records.len()], + }; + + { + let mut mark_style_record = |identity: StyleRecordID| { + ComputedReachability::mark(&mut reachable.style_records, identity); + let record = self.style_records.get(identity); + ComputedReachability::mark(&mut reachable.sets, record.groups); + ComputedReachability::mark(&mut reachable.custom_property_environments, record.custom_properties); + ComputedReachability::mark(&mut reachable.fixed_metadata, record.fixed_metadata); + ComputedReachability::mark(&mut reachable.reconstruction_metadata, record.reconstruction_metadata); + if let Some(longhand_table) = record.longhand_table { + ComputedReachability::mark(&mut reachable.longhand_tables, longhand_table); + } + }; + for &identity in self.style_record_column.iter().flatten() { + mark_style_record(identity); + } + for rows in self.pseudo_rows_by_node.values() { + for assignment in rows.iter().filter_map(|row| row.assignment) { + mark_style_record(assignment.style_record); + } + } + for overlay in self.animation_overlay_slots.iter().flatten() { + mark_style_record(overlay.base_style_record); + } + for &identity in self.base_style_record_pins.keys() { + mark_style_record(identity); + } + } + + for index in 0..self.columns.flags.len() { + if !self.columns.is_assigned(index) { + continue; + } + ComputedReachability::mark(&mut reachable.sets, self.columns.groups(index).unwrap()); + ComputedReachability::mark( + &mut reachable.inherited_sets, + self.columns.inherited_groups(index).unwrap(), + ); + ComputedReachability::mark( + &mut reachable.custom_property_environments, + self.columns.custom_properties(index).unwrap(), + ); + ComputedReachability::mark( + &mut reachable.fixed_metadata, + self.columns.fixed_metadata(index).unwrap(), + ); + ComputedReachability::mark( + &mut reachable.reconstruction_metadata, + self.columns.reconstruction_metadata(index).unwrap(), + ); + } + for rows in self.pseudo_rows_by_node.values() { + for assignment in rows.iter().filter_map(|row| row.assignment) { + ComputedReachability::mark(&mut reachable.sets, assignment.groups); + ComputedReachability::mark(&mut reachable.inherited_sets, assignment.inherited_groups); + ComputedReachability::mark( + &mut reachable.custom_property_environments, + assignment.custom_properties, + ); + ComputedReachability::mark(&mut reachable.fixed_metadata, assignment.fixed_metadata); + ComputedReachability::mark( + &mut reachable.reconstruction_metadata, + assignment.reconstruction_metadata, + ); + } + } + for (index, is_reachable) in reachable.sets.iter().copied().enumerate() { + if !is_reachable { + continue; + } + for (group_index, &payload) in self.sets[index].payloads.iter().enumerate() { + ComputedReachability::mark(&mut reachable.groups, self.group_identity(group_index, payload)); + } + } + for (index, is_reachable) in reachable.inherited_sets.iter().copied().enumerate() { + if !is_reachable { + continue; + } + for &group in &self.inherited_sets[index] { + ComputedReachability::mark(&mut reachable.groups, group); + } + } + reachable + } + + pub(super) fn reclaim_unreachable(&mut self) -> ComputedGroupRetention { + let reachable = self.reachability(); + let retention = ComputedGroupRetention { + retained: self.groups.live_identities().count(), + reachable: reachable.groups.iter().filter(|&&reachable| reachable).count(), + }; + if replaying_style_groups() { + return retention; + } + + let mut unreachable_style_records = self + .style_records + .live_identities() + .filter(|identity| !reachable.style_records[identity.index()]) + .collect::>(); + unreachable_style_records.sort_unstable_by_key(|identity| std::cmp::Reverse(identity.index())); + for identity in unreachable_style_records { + let record = *self.style_records.get(identity); + if self.style_record_generations[identity.index()] == FinalStyleRecordID::MAX_BASE_GENERATION { + self.style_records.remove_identity(content_hash(record), identity); + } else { + self.style_records.retire_identity(content_hash(record), identity); + } + let (changed, _) = self.style_record_liveness.set(identity.index(), false); + assert!(changed, "retired base style-record identity must be live"); + } + for identity in self.sets.live_identities().collect::>() { + if reachable.sets[identity.index()] { + if let Some(longhand_table) = self.sets[identity].canonical_longhand_table + && !reachable.longhand_tables[longhand_table.index()] + { + self.sets[identity].canonical_longhand_table = None; + } + continue; + } + let set = std::mem::replace( + self.sets.get_mut(identity), + ComputedGroupSet { + identity_hash: 0, + payloads: Box::default(), + canonical_longhand_table: None, + }, + ); + self.group_set_nested_memory + .shrink_committed(size_of_val(set.payloads.as_ref()) as u64); + self.sets.retire_identity(set.identity_hash, identity); + } + for identity in self.inherited_sets.live_identities().collect::>() { + if reachable.inherited_sets[identity.index()] { + continue; + } + let groups = std::mem::take(self.inherited_sets.get_mut(identity)); + self.group_set_nested_memory + .shrink_committed(size_of_val(groups.as_ref()) as u64); + self.inherited_sets.retire_identity(content_hash(&groups), identity); + } + for identity in self.custom_property_environments.live_identities().collect::>() { + if reachable.custom_property_environments[identity.index()] { + continue; + } + let environment = *self.custom_property_environments.get(identity); + self.custom_property_environments + .retire_identity(content_hash(environment), identity); + } + for identity in self.computed_fixed_metadata.live_identities().collect::>() { + if reachable.fixed_metadata[identity.index()] { + continue; + } + let metadata = *self.computed_fixed_metadata.get(identity); + self.computed_fixed_metadata + .retire_identity(content_hash(metadata), identity); + } + for identity in self + .computed_reconstruction_metadata + .live_identities() + .collect::>() + { + if reachable.reconstruction_metadata[identity.index()] { + continue; + } + let metadata = self.computed_reconstruction_metadata.get(identity); + let inheritance_dependent_values = metadata + .inheritance_dependent_values + .iter() + .map(|entry| (entry.property, entry.value.pointer().cast())) + .collect::>(); + let hash = reconstruction_metadata_hash( + &metadata.property_importance, + &metadata.property_inheritance, + &inheritance_dependent_values, + metadata + .raw_cascaded_font_size + .as_ref() + .map_or(std::ptr::null(), |value| value.pointer().cast()), + ); + let metadata = std::mem::replace( + self.computed_reconstruction_metadata.get_mut(identity), + ComputedReconstructionMetadata { + property_importance: Box::default(), + property_inheritance: Box::default(), + inheritance_dependent_values: Box::default(), + raw_cascaded_font_size: None, + }, + ); + self.reconstruction_nested_memory.shrink_committed( + (metadata.property_importance.len() + + metadata.property_inheritance.len() + + size_of_val(metadata.inheritance_dependent_values.as_ref())) as u64, + ); + self.computed_reconstruction_metadata.retire_identity(hash, identity); + } + for identity in self.computed_longhand_tables.live_identities().collect::>() { + if reachable.longhand_tables[identity.index()] { + continue; + } + let hash = longhand_table_hash(self.computed_longhand_tables[identity].value_view()); + let table = std::mem::replace( + self.computed_longhand_tables.get_mut(identity), + RetainedLonghandTable { + storage: RetainedLonghandTableStorage::Values(Box::default()), + }, + ); + self.reconstruction_nested_memory + .shrink_committed(size_of_val(table.value_view()) as u64); + self.computed_longhand_tables.retire_identity(hash, identity); + } + for identity in self.groups.live_identities().collect::>() { + if reachable.groups[identity.index()] { + continue; + } + let group = std::mem::replace( + self.groups.get_mut(identity), + ComputedGroup { + index: usize::MAX, + payload: std::ptr::null(), + }, + ); + self.groups + .retire_identity(content_hash((group.index, group.payload as usize)), identity); + self.group_set_nested_memory + .shrink_committed(retained_group_payload_bytes(group.index, group.payload) as u64); + release_group_payload(group.index, group.payload); + } + retention + } + + pub(super) fn reclaim_unreachable_if_needed(&mut self) -> Option { + if self.style_records_interned_since_reclamation < self.next_reclamation_after { + return None; + } + self.style_records_interned_since_reclamation = 0; + let retention = self.reclaim_unreachable(); + self.next_reclamation_after = self.style_records.live_identities().count().max(1024); + Some(retention) + } + pub fn style_record_payloads(&self, raw_style_record: u64) -> Option<&[*const c_void]> { + let final_style_record = FinalStyleRecordID(raw_style_record); if raw_style_record & FinalStyleRecordID::ANIMATION_OVERLAY_TAG != 0 { - let style_record = FinalStyleRecordID(raw_style_record); + let style_record = final_style_record; let slot = *self.animation_overlay_slots_by_record.get(&style_record)?; let record = self.animation_overlay_slots[slot as usize].as_ref()?; return (!record.payloads.is_empty()).then_some(record.payloads.as_ref()); } - let raw_style_record = u32::try_from(raw_style_record).ok()?; - let record_index = raw_style_record.checked_sub(1)? as usize; - let record = self.style_records.get_index(record_index)?; + let style_record = final_style_record.base_record()?; + assert!( + self.style_record_generation_is_live(style_record, final_style_record.base_generation()), + "base style-record is not live" + ); + let record = self.style_records.get_index(style_record.index())?; Some(&self.sets[record.groups].payloads) } @@ -1904,17 +2441,26 @@ impl ComputedGroupSets { pub(crate) fn recording_group_identities(&self, raw_style_record: u64) -> Option> { let final_style_record = FinalStyleRecordID(raw_style_record); let base_style_record = match final_style_record.base_record() { - Some(style_record) => style_record, + Some(style_record) => { + assert!( + self.style_record_generation_is_live(style_record, final_style_record.base_generation()), + "base style-record is not live" + ); + style_record + } None => { let slot = *self.animation_overlay_slots_by_record.get(&final_style_record)?; self.animation_overlay_slots[slot as usize].as_ref()?.base_style_record } }; - let record = self.style_records.get_index(base_style_record.raw() as usize - 1)?; + assert!( + self.style_record_is_live(base_style_record), + "base style-record is not live" + ); + let record = self.style_records.get_index(base_style_record.index())?; Some( - self.sets[record.groups] - .identities - .iter() + self.group_identities(record.groups) + .into_iter() .map(|identity| identity.0) .collect(), ) @@ -1934,11 +2480,47 @@ impl ComputedGroupSets { ) } + #[cfg(feature = "style-recording")] + pub(crate) fn recording_longhand_table(&self, raw_style_record: u64) -> Option<(u32, &[*const c_void])> { + let final_style_record = FinalStyleRecordID(raw_style_record); + let base_style_record = match final_style_record.base_record() { + Some(style_record) => { + assert!( + self.style_record_generation_is_live(style_record, final_style_record.base_generation()), + "base style-record is not live" + ); + style_record + } + None => { + let slot = *self.animation_overlay_slots_by_record.get(&final_style_record)?; + self.animation_overlay_slots[slot as usize].as_ref()?.base_style_record + } + }; + assert!( + self.style_record_is_live(base_style_record), + "base style-record is not live" + ); + let identity = self + .style_records + .get_index(base_style_record.index())? + .longhand_table?; + Some(( + identity.0, + self.computed_longhand_tables + .get_index(identity.0 as usize)? + .value_view(), + )) + } + pub(crate) fn style_record_view(&self, raw_style_record: u64) -> Option> { let final_style_record = FinalStyleRecordID(raw_style_record); let (base_style_record, payloads, animation_overlay_identity, animated_properties) = if let Some(style_record) = final_style_record.base_record() { - let record = self.style_records.get_index(style_record.raw() as usize - 1)?; + assert!( + self.style_record_generation_is_live(style_record, final_style_record.base_generation()), + "base style-record is not live" + ); + let record = self.style_records.get_index(style_record.index())?; ( style_record, self.sets[record.groups].payloads.as_ref(), @@ -1955,7 +2537,11 @@ impl ComputedGroupSets { overlay.animated_properties.pointer(), ) }; - let record = self.style_records.get_index(base_style_record.raw() as usize - 1)?; + assert!( + self.style_record_is_live(base_style_record), + "base style-record is not live" + ); + let record = self.style_records.get_index(base_style_record.index())?; let base_payloads = self.sets[record.groups].payloads.as_ref(); let fixed_metadata = self .computed_fixed_metadata @@ -1972,7 +2558,7 @@ impl ComputedGroupSets { base_payloads, property_importance: &reconstruction_metadata.property_importance, property_inheritance: &reconstruction_metadata.property_inheritance, - inheritance_dependent_values: &reconstruction_metadata.inheritance_dependent_value_view, + inheritance_dependent_values: reconstruction_metadata.inheritance_dependent_value_view(), longhand_values, raw_cascaded_font_size: reconstruction_metadata .raw_cascaded_font_size @@ -1988,7 +2574,13 @@ impl ComputedGroupSets { pub fn pin_style_record(&mut self, raw_style_record: u64) { let final_style_record = FinalStyleRecordID(raw_style_record); - if final_style_record.base_record().is_some() { + if let Some(style_record) = final_style_record.base_record() { + assert!( + self.style_record_generation_is_live(style_record, final_style_record.base_generation()), + "base style-record is not live" + ); + let pin_count = self.base_style_record_pins.entry(style_record).or_default(); + *pin_count = pin_count.checked_add(1).expect("base style-record pin count overflow"); return; } let slot = *self @@ -2006,7 +2598,19 @@ impl ComputedGroupSets { pub fn unpin_style_record(&mut self, raw_style_record: u64) { let final_style_record = FinalStyleRecordID(raw_style_record); - if final_style_record.base_record().is_some() { + if let Some(style_record) = final_style_record.base_record() { + assert!( + self.style_record_generation_is_live(style_record, final_style_record.base_generation()), + "base style-record is not live" + ); + let pin_count = self + .base_style_record_pins + .get_mut(&style_record) + .expect("base style-record is pinned"); + *pin_count = pin_count.checked_sub(1).expect("base style-record is pinned"); + if *pin_count == 0 { + self.base_style_record_pins.remove(&style_record); + } return; } let slot = *self @@ -2028,12 +2632,7 @@ impl ComputedGroupSets { #[must_use] pub fn pseudo_assignment_capacity_bytes(&self) -> u64 { capacity_bytes! { - shallow [ - self.pseudo_assignments, - self.pseudo_kinds_by_node, - self.pseudo_cascade_state_rows, - self.pseudo_retained_cascade_rows, - ]; + shallow [self.pseudo_rows_by_node]; cached [self.pseudo_assignment_nested_memory.bytes()]; nested []; skip []; @@ -2041,10 +2640,17 @@ impl ComputedGroupSets { } pub fn settle_nested_memory(&mut self, memory: &mut MemoryController) { - self.group_set_nested_memory.settle_committed(memory); - self.reconstruction_nested_memory.settle_committed(memory); - self.animation_overlay_nested_memory.settle_committed(memory); - self.pseudo_assignment_nested_memory.settle_committed(memory); + let group_set = self.group_set_nested_memory.bytes(); + self.group_set_nested_memory.reconcile_committed(memory, group_set); + let reconstruction = self.reconstruction_nested_memory.bytes(); + self.reconstruction_nested_memory + .reconcile_committed(memory, reconstruction); + let animation_overlay = self.animation_overlay_nested_memory.bytes(); + self.animation_overlay_nested_memory + .reconcile_committed(memory, animation_overlay); + let pseudo_assignment = self.pseudo_assignment_nested_memory.bytes(); + self.pseudo_assignment_nested_memory + .reconcile_committed(memory, pseudo_assignment); } #[must_use] @@ -2092,7 +2698,8 @@ impl ComputedGroupSets { impl Drop for ComputedGroupSets { fn drop(&mut self) { - for group in &self.groups { + for identity in self.groups.live_identities() { + let group = self.groups.get(identity); release_group_payload(group.index, group.payload); } } @@ -2368,6 +2975,117 @@ mod tests { assert_eq!(sets.live_animation_overlay_records(), 0); } + #[test] + fn base_style_record_pins_are_counted_until_the_last_view_releases() { + let mut sets = ComputedGroupSets::default(); + let publication = sets.publish(None, &[], 0, 0, metadata(0, 0, 0, &[], &[])); + let style_record = publication.style_record_identity; + let base_style_record = style_record.base_record().unwrap(); + + sets.pin_style_record(style_record.raw()); + sets.pin_style_record(style_record.raw()); + assert_eq!(sets.base_style_record_pins.len(), 1); + assert_eq!(sets.base_style_record_pins[&base_style_record], 2); + + sets.unpin_style_record(style_record.raw()); + assert_eq!(sets.base_style_record_pins[&base_style_record], 1); + sets.unpin_style_record(style_record.raw()); + assert!(sets.base_style_record_pins.is_empty()); + } + + #[test] + fn computed_record_reclamation_preserves_roots_while_reusing_record_slots() { + let mut sets = ComputedGroupSets::default(); + let node = StyleNodeID::element(1); + let target = ComputedStyleTarget::new(node, u8::MAX); + let pinned = sets.publish(Some(target), &[], 0, 1, metadata(0, 0, 0, &[], &[])); + sets.pin_style_record(pinned.style_record_identity.raw()); + + for environment in 2..128 { + sets.publish(Some(target), &[], 0, environment, metadata(0, 0, 0, &[], &[])); + } + let current = sets.assigned_style_record(node).unwrap().base_record().unwrap(); + let dense_record_count = sets.style_records.len(); + let retention = sets.reclaim_unreachable(); + + assert_eq!(retention.retained, retention.reachable); + assert_eq!(sets.style_records.live_len(), 2); + assert!(sets.style_records.live_identities().any(|identity| identity == current)); + assert!( + sets.style_records + .live_identities() + .any(|identity| identity == pinned.style_record_identity.base_record().unwrap()) + ); + + for environment in 128..253 { + sets.publish(Some(target), &[], 0, environment, metadata(0, 0, 0, &[], &[])); + } + assert_eq!(sets.style_records.len(), dense_record_count); + + sets.unpin_style_record(pinned.style_record_identity.raw()); + sets.reclaim_unreachable(); + assert_eq!(sets.style_records.live_len(), 1); + } + + #[test] + fn computed_record_reclamation_reuses_the_lowest_identity_first() { + let mut sets = ComputedGroupSets::default(); + let first = sets.publish(None, &[], 0, 1, metadata(0, 0, 0, &[], &[])); + sets.publish(None, &[], 0, 2, metadata(0, 0, 0, &[], &[])); + sets.publish(None, &[], 0, 3, metadata(0, 0, 0, &[], &[])); + sets.reclaim_unreachable(); + + let replacement = sets.publish(None, &[], 0, 4, metadata(0, 0, 0, &[], &[])); + assert_eq!( + replacement.style_record_identity.base_record(), + first.style_record_identity.base_record() + ); + } + + #[test] + fn computed_record_reclamation_retires_an_exhausted_identity() { + let mut sets = ComputedGroupSets::default(); + let exhausted = sets.publish(None, &[], 0, 1, metadata(0, 0, 0, &[], &[])); + let exhausted = exhausted.style_record_identity.base_record().unwrap(); + sets.style_record_generations[exhausted.index()] = FinalStyleRecordID::MAX_BASE_GENERATION; + sets.reclaim_unreachable(); + + let replacement = sets.publish(None, &[], 0, 2, metadata(0, 0, 0, &[], &[])); + assert_ne!(replacement.style_record_identity.base_record(), Some(exhausted)); + } + + #[test] + #[should_panic(expected = "base style-record is not live")] + fn a_retired_base_style_record_cannot_be_viewed() { + let mut sets = ComputedGroupSets::default(); + let publication = sets.publish(None, &[], 0, 1, metadata(0, 0, 0, &[], &[])); + let retired_final = publication.style_record_identity; + let retired = retired_final.base_record().unwrap(); + sets.reclaim_unreachable(); + let replacement = sets.publish(None, &[], 0, 2, metadata(0, 0, 0, &[], &[])); + let replacement_final = replacement.style_record_identity; + let replacement = replacement_final.base_record().unwrap(); + assert_eq!(retired.index(), replacement.index()); + assert_ne!(retired_final.base_generation(), replacement_final.base_generation()); + let _ = sets.style_record_view(publication.style_record_identity.raw()); + } + + #[test] + #[should_panic(expected = "base style-record is not live")] + fn a_retired_base_style_record_cannot_be_pinned() { + let mut sets = ComputedGroupSets::default(); + let publication = sets.publish(None, &[], 0, 1, metadata(0, 0, 0, &[], &[])); + let retired_final = publication.style_record_identity; + let retired = retired_final.base_record().unwrap(); + sets.reclaim_unreachable(); + let replacement = sets.publish(None, &[], 0, 2, metadata(0, 0, 0, &[], &[])); + let replacement_final = replacement.style_record_identity; + let replacement = replacement_final.base_record().unwrap(); + assert_eq!(retired.index(), replacement.index()); + assert_ne!(retired_final.base_generation(), replacement_final.base_generation()); + sets.pin_style_record(publication.style_record_identity.raw()); + } + #[test] fn computed_group_set_capacity_includes_hash_table_control_bytes() { let mut sets = ComputedGroupSets::default(); @@ -2381,10 +3099,13 @@ mod tests { sets.style_records.reserve(1); sets.pending_cascade_states.reserve(1); sets.animation_overlay_slots_by_record.reserve(1); - sets.pseudo_assignments.reserve(1); - sets.pseudo_kinds_by_node.reserve(1); - sets.pseudo_cascade_state_rows.reserve(1); - sets.pseudo_retained_cascade_rows.reserve(1); + sets.pseudo_rows_by_node.reserve(1); + sets.pseudo_rows_by_node.insert( + StyleNodeID::element(1), + vec![PseudoComputedRow::new(1)].into_boxed_slice(), + ); + sets.pseudo_assignment_nested_memory + .grow_committed(size_of::() as u64); let accounted = sets.capacity_bytes() + sets.custom_property_environment_capacity_bytes() @@ -2401,17 +3122,14 @@ mod tests { + sets.computed_reconstruction_metadata.capacity_bytes() as usize + sets.computed_longhand_tables.capacity_bytes() as usize + sets.style_records.capacity_bytes() as usize + + sets.style_record_generations.capacity() * size_of::() + sets.pending_cascade_states.capacity() - * (size_of::<(StyleNodeID, u8)>() + size_of::<(u64, CascadeStateID)>() + 1) + * (size_of::() + size_of::<(u64, CascadeStateID)>() + 1) + sets.animation_overlay_slots_by_record.capacity() * (size_of::() + size_of::() + 1) - + sets.pseudo_assignments.capacity() - * (size_of::<(StyleNodeID, u8)>() + size_of::() + 1) - + sets.pseudo_kinds_by_node.capacity() * (size_of::() + size_of::>() + 1) - + sets.pseudo_cascade_state_rows.capacity() - * (size_of::<(StyleNodeID, u8)>() + size_of::<(u64, CascadeStateID)>() + 1) - + sets.pseudo_retained_cascade_rows.capacity() - * (size_of::<(StyleNodeID, u8)>() + size_of::<(u64, CascadeStateID)>() + 1); + + sets.pseudo_rows_by_node.capacity() + * (size_of::() + size_of::>() + 1) + + size_of_val(sets.pseudo_rows_by_node[&StyleNodeID::element(1)].as_ref()); assert_eq!(accounted, expected as u64); } diff --git a/Libraries/LibWeb/Rust/src/css/style/differential_tests.rs b/Libraries/LibWeb/Rust/src/css/style/differential_tests.rs index 2cd6abe2d8286..c9b4d1dfd8df4 100644 --- a/Libraries/LibWeb/Rust/src/css/style/differential_tests.rs +++ b/Libraries/LibWeb/Rust/src/css/style/differential_tests.rs @@ -19,8 +19,8 @@ use super::computed::ComputedMetadataInput; use super::computed::ComputedReconstructionMetadataInput; use super::fast_hash::fast_hasher; use super::index::DispatchCandidateWorkspace; -use super::index::FeatureKey; use super::index::FeatureValue; +use super::index::LocalFeatureKey; use super::index::StyleAtomID; use super::instrumentation::Counters; use super::memory::DeviceClass; @@ -135,7 +135,7 @@ impl Workload { let mut classes = vec![Vec::new(); nodes.len()]; for (index, &node) in nodes.iter().enumerate() { engine.record_input( - InputKey::LocalFeature(node, FeatureKey::TagName), + InputKey::LocalFeature(node, LocalFeatureKey::TagName), InputValue::Feature(FeatureValue::Absent), InputValue::Feature(FeatureValue::Atom(tag_atom(node))), ); @@ -146,7 +146,7 @@ impl Workload { } classes[index].push(class); engine.record_input( - InputKey::LocalFeature(node, FeatureKey::Class(class_atom(class))), + InputKey::LocalFeature(node, LocalFeatureKey::Class(class_atom(class))), InputValue::Feature(FeatureValue::Absent), InputValue::Feature(FeatureValue::Present), ); @@ -264,7 +264,7 @@ impl Workload { (FeatureValue::Absent, FeatureValue::Present) }; self.engine.record_input( - InputKey::LocalFeature(node, FeatureKey::Class(class_atom(class))), + InputKey::LocalFeature(node, LocalFeatureKey::Class(class_atom(class))), InputValue::Feature(old), InputValue::Feature(new), ); @@ -322,9 +322,8 @@ fn retained_matches(engine: &mut StyleEngine, node: StyleNodeID) -> Option = dispatch - .entries() - .iter() + let mut orders: Vec<_> = (0..dispatch.entry_count()) + .map(|index| dispatch.entry_at(index)) .map(|entry| (entry.rule, entry.program, entry.entry, entry.cascade_order)) .collect(); orders.sort_unstable_by_key(|&(rule, program, entry, _)| (rule, program, entry)); diff --git a/Libraries/LibWeb/Rust/src/css/style/fast_hash.rs b/Libraries/LibWeb/Rust/src/css/style/fast_hash.rs index 307a90e034c0c..87949688a4a7c 100644 --- a/Libraries/LibWeb/Rust/src/css/style/fast_hash.rs +++ b/Libraries/LibWeb/Rust/src/css/style/fast_hash.rs @@ -9,8 +9,6 @@ use std::collections::HashMap; use std::collections::HashSet; use std::hash::BuildHasher; -use std::hash::BuildHasherDefault; -use std::hash::Hasher; use foldhash::fast::FixedState; pub use foldhash::fast::FoldHasher as FastHasher; @@ -21,44 +19,3 @@ pub(crate) fn fast_hasher() -> FastHasher { pub type FastMap = HashMap; pub type FastSet = HashSet; - -// NB: Staged tree rows feed slot assignment order, which is observable through the recorded flat- -// tree callback. Keep its established iteration until that callback has a separately authorized -// canonical-order migration; all lookup-only engine tables use FixedState above. -#[derive(Default)] -pub(super) struct StableIterationHasher(u64); - -impl StableIterationHasher { - #[inline] - fn fold(&mut self, value: u64) { - self.0 = (self.0.rotate_left(26) ^ value).wrapping_mul(0x2545_f491_4f6c_dd1d); - } -} - -impl Hasher for StableIterationHasher { - fn finish(&self) -> u64 { - let mut mixed = self.0; - mixed ^= mixed >> 32; - mixed = mixed.wrapping_mul(0x2545_f491_4f6c_dd1d); - mixed ^ (mixed >> 29) - } - - fn write(&mut self, bytes: &[u8]) { - let mut chunks = bytes.chunks_exact(size_of::()); - for chunk in &mut chunks { - self.fold(u64::from_le_bytes(chunk.try_into().unwrap())); - } - let remainder = chunks.remainder(); - if !remainder.is_empty() { - let mut tail = [0_u8; size_of::()]; - tail[..remainder.len()].copy_from_slice(remainder); - self.fold(u64::from_le_bytes(tail) | ((remainder.len() as u64) << 56)); - } - } - - fn write_u32(&mut self, value: u32) { - self.fold(u64::from(value)); - } -} - -pub(super) type StableIterationMap = HashMap>; diff --git a/Libraries/LibWeb/Rust/src/css/style/flush.rs b/Libraries/LibWeb/Rust/src/css/style/flush.rs index 7c212b531e2f6..d4f3de16ab851 100644 --- a/Libraries/LibWeb/Rust/src/css/style/flush.rs +++ b/Libraries/LibWeb/Rust/src/css/style/flush.rs @@ -14,6 +14,17 @@ impl StyleEngine { root: StyleNodeID, mut emit: impl FnMut(StyleTransactionVersion, ProgramVersion, &[PublishedStyleDeltaRecord]), ) -> bool { + self.reclaim_computed_memory_if_needed(); + self.sync_tier3_benefit_observations(); + let tier3_evictions = self.memory.finish_tier3_quota_period(); + for &category in &TIER3_REFUSAL_CATEGORIES { + if tier3_evictions[category as usize] && self.evict_tier3_category(category) { + self.counters.bump(Counter::Tier3BenefitEvictions); + } + } + self.memory.begin_tier3_quota_period(); + self.winner_groups.begin_quota_period(); + self.relational_witnesses.borrow_mut().set_admitting(true); self.route_pruning_states.borrow_mut().clear(); #[cfg(test)] if let Some(capture) = &mut self.diagnostic_plan_capture { @@ -22,31 +33,15 @@ impl StyleEngine { } self.discard_prepared_batch_matching_traversal(); self.discard_published_match_answers(); - self.facts.restore_prepared_selector_query_input(&mut self.memory); let document_root_arrival_is_pending = self.journal.pending_old(InputKey::TreeRelations(root)) == Some(InputValue::TreeRelations(None)); let initial_tree_was_bulk_loaded = self.initial_tree_bulk_load_is_pending && document_root_arrival_is_pending; let publish_document_root_arrival = document_root_arrival_is_pending; let mut transaction = self.drain_transaction(); - let plan_before_commit = !transaction.is_empty() - && !transaction.has_coarsened_markers() - && transaction.inputs.iter().all(|input| match input.key { - InputKey::LocalFeature( - _, - FeatureKey::Language | FeatureKey::Directionality | FeatureKey::HeadingLevel, - ) => false, - InputKey::LocalFeature(..) - | InputKey::State(..) - | InputKey::ElementDeclaration(..) - | InputKey::ElementStyleInput(..) => true, - _ => false, - }); - if !plan_before_commit { - self.commit_pending(&mut transaction, BeforeFactRetention::Retain); - } + self.apply_staged_transaction(&mut transaction); if transaction.is_empty() { - self.release_transaction(transaction); + self.release_transaction_and_sweep_atoms(transaction); return true; } let preserves_selector_incidence = !transaction.has_coarsened_markers() @@ -111,20 +106,20 @@ impl StyleEngine { input.key, InputKey::LocalFeature( _, - FeatureKey::Id - | FeatureKey::Class(_) - | FeatureKey::CustomState(_) - | FeatureKey::Attribute(_) + LocalFeatureKey::Id + | LocalFeatureKey::Class(_) + | LocalFeatureKey::CustomState(_) + | LocalFeatureKey::Attribute(_) ) ) }) && transaction.inputs.iter().all(|input| { - let InputKey::LocalFeature(node, FeatureKey::Attribute(_)) = input.key else { + let InputKey::LocalFeature(node, LocalFeatureKey::Attribute(_)) = input.key else { return true; }; transaction.inputs.iter().any(|candidate| { matches!( candidate.key, - InputKey::LocalFeature(candidate_node, FeatureKey::Id | FeatureKey::Class(_)) + InputKey::LocalFeature(candidate_node, LocalFeatureKey::Id | LocalFeatureKey::Class(_)) if candidate_node == node ) }) @@ -188,7 +183,7 @@ impl StyleEngine { { self.discard_retained_prefix_caches(); self.retained_match_answers.evict(&mut self.match_answers); - self.release_transaction(transaction); + self.release_transaction_and_sweep_atoms(transaction); return false; } @@ -199,11 +194,8 @@ impl StyleEngine { let mut regions = if transaction.inputs.len() >= TRANSACTION_TOPOLOGY_MINIMUM_INPUTS { let regions = ImpactRegions::with_topology(&self.tree, root); let bytes = regions.topology_capacity_bytes(); - if self.memory.reserve(MemoryCategory::BatchScratch, bytes).is_granted() { - regions - } else { - ImpactRegions::new() - } + self.memory.reserve_required(MemoryCategory::BatchScratch, bytes); + regions } else { ImpactRegions::new() }; @@ -317,7 +309,7 @@ impl StyleEngine { && exact_tree_routing_is_selective(arriving_nodes.len() + departing_nodes, connected_element_count); let mut transaction_fact_view = self.transaction_fact_view_for(&mut transaction, root, ®ions); if use_exact_tree_routing { - let _ = self.populate_before_sibling_relations(&mut transaction_fact_view, &transaction, &arriving_nodes); + let _ = self.install_before_sibling_geometry(&mut transaction_fact_view); } let has_before_sibling_relations = transaction_fact_view.before_sibling_relations_available; self.prefix_caches.borrow_mut().states.mark_previous(); @@ -333,14 +325,11 @@ impl StyleEngine { // Routing reads the program as it stands now, but the inputs happened before it did. // A sheet that went away in this same transaction was still deciding when the mutations // ahead of it in the journal were recorded, so its rules cannot be skipped as inactive. - let sheets_in_flux = Self::sheets_in_flux(&transaction); - let programs_in_flux = Self::programs_in_flux(&transaction); + let program_delta = self.program_staging.delta(); let retained_winners_are_current = transaction .inputs .iter() .all(|input| matches!(input.key, InputKey::LocalFeature(..) | InputKey::State(..))); - let rules_arriving = Self::rules_arriving(&transaction); - let scopes_departed = Self::scopes_departed(&transaction); if self.selector_incidence_is_current { let programs: Vec<_> = transaction .inputs @@ -382,12 +371,9 @@ impl StyleEngine { has_before_sibling_relations, transaction_inputs: &transaction.inputs, }; - let sibling_entries = self.live_sibling_entries(); - let mut sibling_candidates = SiblingCandidateWorkspace::new(&sibling_entries); - let sibling_entry_scratch_bytes = - (sibling_entries.capacity() * size_of::()) as u64 + sibling_candidates.capacity_bytes(); - self.memory - .reserve_required(MemoryCategory::BatchScratch, sibling_entry_scratch_bytes); + let routing_for_siblings = Rc::clone(&self.routing); + let sibling_entries = routing_for_siblings.live_sibling_entries(&self.program, &self.programs); + let mut sibling_candidates = routing_for_siblings.live_sibling_workspace(&self.program, &self.programs); let mut pending_routes = PendingRoutes::new(); let mut pending_sibling_routes = PendingSiblingRoutes::new(); let mut pending_prefix_producers = Vec::new(); @@ -425,6 +411,7 @@ impl StyleEngine { scopes.sort_unstable(); scopes.dedup(); } + let mut removed_rules_requiring_refresh = Vec::new(); for input in transaction .inputs .iter() @@ -433,13 +420,14 @@ impl StyleEngine { self.route_program_input( input, transaction.program_joins_for(input.key), - &rules_arriving, - &scopes_departed, + &program_delta.arriving_rules, + &program_delta.departed_scopes, ProgramRoutingContext { resident_nodes: (!outer_arrivals.is_empty()).then_some(resident_nodes.as_slice()), winner_program_version: transaction.program_base_version, document_root: root, attachment_scopes: None, + removed_rules_requiring_refresh: &mut removed_rules_requiring_refresh, }, &mut regions, ); @@ -453,13 +441,14 @@ impl StyleEngine { self.route_program_input( input, transaction.program_joins_for(input.key), - &rules_arriving, - &scopes_departed, + &program_delta.arriving_rules, + &program_delta.departed_scopes, ProgramRoutingContext { resident_nodes: (!outer_arrivals.is_empty()).then_some(resident_nodes.as_slice()), winner_program_version: transaction.program_base_version, document_root: root, attachment_scopes: Some(scopes), + removed_rules_requiring_refresh: &mut removed_rules_requiring_refresh, }, &mut regions, ); @@ -468,6 +457,16 @@ impl StyleEngine { } } } + if self.selector_truth_changes_active && !removed_rules_requiring_refresh.is_empty() { + removed_rules_requiring_refresh.sort_unstable(); + removed_rules_requiring_refresh.dedup(); + let refreshes = &mut self.selector_truth_changes.refreshes; + self.retained_match_answers.for_each_answer_containing_any_rule( + &self.match_answers, + &removed_rules_requiring_refresh, + |node| refreshes.push(SelectorTruthRefresh { node, rule: None }), + ); + } let prefix_producer_admission = if !regions.covers_document() && collect_pending_prefix_producers { Some(self.ranked_scope_program(TreeScopeID::DOCUMENT)) } else { @@ -492,7 +491,7 @@ impl StyleEngine { // the witness of a relational selector whose anchor is outside it. let nested_fact_arrival_without_relational_selectors = matches!( input.key, - InputKey::LocalFeature(node, FeatureKey::ArrivingFacts) + InputKey::LocalFeature(node, LocalFeatureKey::ArrivingFacts) if nested_arrivals.binary_search(&node).is_ok() && self.routing.relational_routes().is_empty() ); @@ -501,8 +500,8 @@ impl StyleEngine { } self.route_input( input, - &sheets_in_flux, - &programs_in_flux, + &program_delta.sheets, + &program_delta.selector_programs, tree_routing, &sibling_entries, &mut sibling_candidates, @@ -539,7 +538,7 @@ impl StyleEngine { if !regions.covers_document() { deferred_sequence_routes = self.route_sequence_changes( &sequences, - &sheets_in_flux, + &program_delta.sheets, tree_routing, &mut regions, &mut planning_workspace, @@ -592,8 +591,6 @@ impl StyleEngine { .release(MemoryCategory::BatchScratch, pending_table_scratch_bytes); self.memory .release(MemoryCategory::BatchScratch, sequence_scratch_bytes); - self.memory - .release(MemoryCategory::BatchScratch, sibling_entry_scratch_bytes); self.memory.release(MemoryCategory::BatchScratch, arrival_scratch_bytes); let mut match_workspace = std::mem::take(&mut self.match_workspace); let before = match_workspace.capacity_bytes(); @@ -636,12 +633,9 @@ impl StyleEngine { .as_ref() .map(|_| regions.compile_patch_cover(&self.tree, Some(root))); self.resolve_already_planned_selector_truth(®ions, patch_cover.as_ref().map(|cover| &cover.full)); - if plan_before_commit { - self.commit_pending(&mut transaction, BeforeFactRetention::Omit); - } let program_base_version = transaction.program_base_version; // A scoped plan consumes retained match answers or packs its missing rows adaptively. Do - // not walk and clone the complete required fact store merely to prepare for that bounded + // not walk and snapshot the complete required fact store merely to prepare for that bounded // traversal. Broad plans will consume the complete batch, so their broad reaction pays the // preparation cost once here and reuses it during matching. let prepare_complete_matching_batch = regions.regions().iter().any(|region| { @@ -662,27 +656,6 @@ impl StyleEngine { .add(Counter::PreparedMatchingBatchCompletenessRowsInspected, inspected); is_complete }; - let prepared_matching_batch = if prepared_matching_batch_is_complete { - let batch = self.facts.primary().clone(); - self.counters - .add(Counter::PreparedMatchingBatchRowsCloned, batch.row_count() as u64); - self.memory - .reserve_required(MemoryCategory::BatchScratch, batch.capacity_bytes()); - Some(batch) - } else { - None - }; - if let Some(batch) = prepared_matching_batch { - match &mut self.prepared_batch_matching_traversal { - Some(prepared) => prepared.batch = Some(batch), - None => { - let mut prepared = PreparedBatchMatchingTraversal::new(root); - prepared.batch = Some(batch); - prepared.reuse_retained_match_answers = reuse_retained_match_answers; - self.prepared_batch_matching_traversal = Some(prepared); - } - } - } let fact_view_bytes = self .transaction_fact_view .as_ref() @@ -722,7 +695,25 @@ impl StyleEngine { let style_input_reaction_bytes = (style_input_reactions.capacity() * size_of::<(StyleNodeID, u8, u8)>()) as u64; self.memory .reserve_required(MemoryCategory::BatchScratch, style_input_reaction_bytes); - self.release_transaction(transaction); + self.release_transaction_and_sweep_atoms(transaction); + // Releasing staging can compact primary payloads. Take the shared view afterwards so that + // compaction does not need to copy the complete primary arrangement away from its view. + if prepared_matching_batch_is_complete { + let batch = self.facts.primary_view(); + // NB: This externally visible counter predates shared primary views. It now counts the + // rows made available to the prepared batch without implying a physical copy. + self.counters + .add(Counter::PreparedMatchingBatchRowsCloned, batch.live_row_count() as u64); + match &mut self.prepared_batch_matching_traversal { + Some(prepared) => prepared.batch = Some(batch), + None => { + let mut prepared = PreparedBatchMatchingTraversal::new(root); + prepared.batch = Some(batch); + prepared.reuse_retained_match_answers = reuse_retained_match_answers; + self.prepared_batch_matching_traversal = Some(prepared); + } + } + } if plan_is_broad { if !transaction_reaches_no_selector { self.retained_match_answers.evict(&mut self.match_answers); @@ -739,19 +730,22 @@ impl StyleEngine { .map_or(0, RetainedAnswerPatch::capacity_bytes); self.memory .reserve_required(MemoryCategory::BatchScratch, retained_answer_patch_scratch_bytes); - let (published_retained_answer_orders, published_retained_answer_order_bytes) = - if retained_answer_patch.is_none() && reuse_retained_match_answers { - self.retained_answer_cascade_orders_for_traversal(true) - } else { - (Vec::new(), 0) - }; + let published_retained_answer_dispatch = if retained_answer_patch.is_none() && reuse_retained_match_answers { + self.retained_answer_dispatch_for_traversal(true) + } else { + None + }; let compiled_regions = regions.compile_union(regions.regions(), &self.tree, Some(root)); if let Some(base_version) = program_base_version { let current_version = self.program.version(); - self.winner_groups - .advance_program_version_where(base_version, current_version, |node| { - !regions.batch_contains_node(&compiled_regions, node) - }); + if regions.covers_document() { + self.winner_groups.begin_program_version(current_version); + } else { + self.winner_groups + .advance_program_version_where(base_version, current_version, |node| { + !regions.batch_contains_node(&compiled_regions, node) + }); + } } let mut node_count = 0; @@ -789,7 +783,7 @@ impl StyleEngine { let mut previous_cascade_inputs = Vec::new(); let mut exact_cascade_stop_nodes = DeltaBatch::default(); let mut exact_cascade_confirmation_nodes = DeltaBatch::default(); - let mut attribution_scratch: Vec<(RuleID, SelectorProgramID)> = Vec::new(); + let mut attribution_scratch: Vec<(RuleID, EntryID)> = Vec::new(); let mut attribution_sweep = AttributionSweep::default(); regions.for_each_batch(&compiled_regions, |node| { #[cfg(test)] @@ -864,7 +858,8 @@ impl StyleEngine { has_upquery = true; SelectorTruthPatch::Full }; - let rule_is_safe = |rule: RuleID, program: SelectorProgramID| { + let rule_is_safe = |rule: RuleID, entry: EntryID| { + let (program, _) = self.programs.entry_location(entry); self.program.declarations_are_complete_for(rule) && self.program.rule_version(rule).selector_program == Some(program) && !self.programs.get(program).contains_relational_selector() @@ -872,24 +867,24 @@ impl StyleEngine { let node_has_safe_exact_cascade_provenance = match truth_patch { SelectorTruthPatch::Full => false, SelectorTruthPatch::Direct(deltas) => { - deltas.iter().all(|delta| rule_is_safe(delta.rule, delta.program)) + deltas.iter().all(|delta| rule_is_safe(delta.rule, delta.entry)) } SelectorTruthPatch::Refresh { deltas, refreshes } => { - deltas.iter().all(|delta| rule_is_safe(delta.rule, delta.program)) - && refreshes.iter().all(|refresh| { - refresh.rule.is_some_and(|(rule, program)| rule_is_safe(rule, program)) - }) + deltas.iter().all(|delta| rule_is_safe(delta.rule, delta.entry)) + && refreshes + .iter() + .all(|refresh| refresh.rule.is_some_and(|(rule, entry)| rule_is_safe(rule, entry))) } SelectorTruthPatch::Attributed { deltas, refreshes, rules, } => { - deltas.iter().all(|delta| rule_is_safe(delta.rule, delta.program)) - && refreshes.iter().all(|refresh| { - refresh.rule.is_some_and(|(rule, program)| rule_is_safe(rule, program)) - }) - && rules.iter().all(|&(rule, program)| rule_is_safe(rule, program)) + deltas.iter().all(|delta| rule_is_safe(delta.rule, delta.entry)) + && refreshes + .iter() + .all(|refresh| refresh.rule.is_some_and(|(rule, entry)| rule_is_safe(rule, entry))) + && rules.iter().all(|&(rule, entry)| rule_is_safe(rule, entry)) } }; can_confirm_exact_cascade = transaction_supports_retained_cascade_stops @@ -939,7 +934,7 @@ impl StyleEngine { // and a live shadow root is a synthetic relation node rather than a style output. // Neither has a C++ element to consume a record; every live element whose style either // can affect is another member of the region. - let is_scope_root = self.scope_by_root.contains_key(&node); + let is_scope_root = self.scope_by_root.get(node).is_some(); if !self.tree.is_live(node) || is_scope_root { return; } @@ -988,14 +983,14 @@ impl StyleEngine { for node in self.elements_under(root) { self.facts.ensure_row(node); } - self.facts.commit_pending(&mut self.memory); + self.facts.apply_staged(&mut self.memory); self.counters = counters; } let publish_style_answers = true; { let published_node_bytes = (published_nodes.capacity() * size_of::()) as u64; let previous_cascade_input_bytes = - (previous_cascade_inputs.capacity() * size_of::>()) as u64; + (previous_cascade_inputs.capacity() * size_of::>()) as u64; let identity_repair_node_bytes = (identity_repair_nodes.capacity() * size_of::()) as u64; self.memory .reserve_required(MemoryCategory::BatchScratch, published_node_bytes); @@ -1014,14 +1009,10 @@ impl StyleEngine { if !reuse_active_batch_matching_traversal { self.begin_published_match_answer_completion_batch(root, prefer_complete_batch); } - let retained_answer_orders = retained_answer_patch + let retained_answer_dispatch = retained_answer_patch .as_ref() - .map(|patch| RetainedAnswerCascadeOrders::Dispatch(patch.dispatch.as_ref())) - .or_else(|| { - (!published_retained_answer_orders.is_empty()).then_some(RetainedAnswerCascadeOrders::Snapshot( - published_retained_answer_orders.as_slice(), - )) - }); + .map(|patch| patch.dispatch.as_ref()) + .or(published_retained_answer_dispatch.as_deref()); patch_preserved_nodes.sort_unstable(); patch_preserved_nodes.dedup(); patch_processed_nodes.sort_unstable(); @@ -1034,7 +1025,7 @@ impl StyleEngine { // Exact retained answers are interned across elements. Once one sufficiently // expensive answer has been compacted in this completion batch, other elements // without element declarations can share its cascade input and winner identities. - let mut completed_retained_answers: HashMap = + let mut completed_retained_answers: HashMap = HashMap::default(); let mut completed_retained_answer_bytes = 0_u64; let share_cascade_completions = published_nodes.len() >= MIN_SHARED_CASCADE_COMPLETION_BATCH; @@ -1052,7 +1043,7 @@ impl StyleEngine { .ok() .and(retained_cascade_input); let retained_answer_identity = (share_cascade_completions - && retained_answer_orders.is_some() + && retained_answer_dispatch.is_some() && self.match_answer_is_comparable_across_elements(node) && self.has_no_element_declarations(node)) .then(|| self.retained_match_answers.answer_identity(node)) @@ -1082,12 +1073,12 @@ impl StyleEngine { node, source, cascade_input, - retained_answer_orders.unwrap(), + retained_answer_dispatch.unwrap(), cascade_winners_are_complete, ) }) .unwrap_or_else(|| { - self.complete_published_match_answer(node, retained_answer_orders) + self.complete_published_match_answer(node, retained_answer_dispatch) .expect("a connected style reaction must have complete selector facts") }); if let Some(identity) = retained_answer_identity @@ -1102,7 +1093,7 @@ impl StyleEngine { published_answer.cascade_winners_are_complete, )); let added_bytes = ((completed_retained_answers.capacity() - capacity_before) - * (size_of::() + size_of::<(StyleNodeID, CascadeInputID, bool)>() + 1)) + * (size_of::() + size_of::<(StyleNodeID, MatchAnswerID, bool)>() + 1)) as u64; self.memory.reserve_required(MemoryCategory::BatchScratch, added_bytes); completed_retained_answer_bytes += added_bytes; @@ -1171,8 +1162,8 @@ impl StyleEngine { }) }); if confirmed_exact_cascade && let Some(current_cascade_input) = published_answer.cascade_input { - verify_style_answer_patch(|| { - self.verify_retained_cascade_input(node, current_cascade_input); + verify_style_answer_patch(self, |verifier| { + verifier.verify_retained_cascade_input(node, current_cascade_input); }); } match published_answer { @@ -1230,7 +1221,7 @@ impl StyleEngine { MemoryCategory::BatchScratch, sequence_touched_parent_bytes + stale_refresh_node_bytes + patch_node_scratch_bytes, ); - verify_style_plan_provenance(|| { + verify_style_plan_provenance(self, |_| { assert!( plan_is_broad || unattributed_node_count == 0, "a scoped style transaction published {unattributed_node_count} nodes without semantic provenance" @@ -1255,8 +1246,6 @@ impl StyleEngine { } self.memory .release(MemoryCategory::BatchScratch, final_retained_answer_patch_scratch_bytes); - self.memory - .release(MemoryCategory::BatchScratch, published_retained_answer_order_bytes); self.memory .release(MemoryCategory::BatchScratch, selector_truth_change_bytes); self.memory @@ -1270,13 +1259,10 @@ impl StyleEngine { for node in published_nodes.iter().copied() { let pseudo_inputs_may_have_changed = pseudo_inputs_may_have_changed || !selector_truth_changes.refreshes_for(node).is_empty() - || selector_truth_changes.deltas_for(node).iter().any(|delta| { - self.programs - .get(delta.program) - .entries() - .get(delta.entry as usize) - .is_some_and(|entry| entry.pseudo_element.is_some()) - }); + || selector_truth_changes + .deltas_for(node) + .iter() + .any(|delta| self.programs.entry(delta.entry).1.pseudo_element.is_some()); let answer = published_match_answers .lookup(node) .expect("each accepted style reaction has a published match answer"); @@ -1410,13 +1396,13 @@ impl StyleEngine { return None; }; let key = match feature { - FeatureKey::Class(atom) => DispatchKey::Class(atom), - FeatureKey::Part(atom) => DispatchKey::Part(atom), - FeatureKey::CustomState(atom) => DispatchKey::CustomState(atom), + LocalFeatureKey::Class(atom) => DispatchKey::Class(atom), + LocalFeatureKey::Part(atom) => DispatchKey::Part(atom), + LocalFeatureKey::CustomState(atom) => DispatchKey::CustomState(atom), // Emptiness is not a feature a compound dispatches on, so nothing is in flux for it. - FeatureKey::Emptiness => return None, - FeatureKey::Attribute(atom) => DispatchKey::AttributeName(atom), - FeatureKey::Id => match (input.old, input.new) { + LocalFeatureKey::Emptiness => return None, + LocalFeatureKey::Attribute(atom) => DispatchKey::AttributeName(atom), + LocalFeatureKey::Id => match (input.old, input.new) { (InputValue::Feature(FeatureValue::Atom(atom)), _) | (_, InputValue::Feature(FeatureValue::Atom(atom))) => DispatchKey::Id(atom), _ => return None, @@ -1424,79 +1410,19 @@ impl StyleEngine { // A resolved language or directionality carries its own value, like an ID. // Neither a language nor a part exposure is dispatched on its value, so nothing is in // flux for either. - FeatureKey::Language | FeatureKey::PartExposure | FeatureKey::HeadingLevel => return None, - FeatureKey::Directionality => match (input.old, input.new) { + LocalFeatureKey::Language | LocalFeatureKey::PartExposure | LocalFeatureKey::HeadingLevel => return None, + LocalFeatureKey::Directionality => match (input.old, input.new) { (InputValue::Feature(FeatureValue::Atom(atom)), _) | (_, InputValue::Feature(FeatureValue::Atom(atom))) => DispatchKey::Directionality(atom), _ => return None, }, // A tag never changes, so it is never in flux. Neither is the folded key an arrival's // facts are journalled under: every fact it stands for holds on the element now. - FeatureKey::TagName | FeatureKey::FoldedTagName | FeatureKey::ArrivingFacts => return None, + LocalFeatureKey::TagName | LocalFeatureKey::FoldedTagName | LocalFeatureKey::ArrivingFacts => return None, }; Some((node, key)) } - /// The selector programs a rule had earlier in this transaction and no longer has. - /// - /// A route is registered under the program that compiled it, and one belonging to a - /// program the rule has given up describes a selector nobody wrote - except while the change that - /// replaced it is still in the transaction, because the inputs recorded ahead of it happened while - /// that selector was the one in effect. A class removed in the same transaction as a - /// `selectorText` edit has to route through the selector it was removed from. - pub(super) fn programs_in_flux(transaction: &StyleTransaction) -> Vec { - let mut programs: Vec = transaction - .inputs - .iter() - .filter_map(|input| match (input.key, input.old) { - (InputKey::RuleField(_, RuleField::Selector), InputValue::SelectorProgram(program)) => program, - _ => None, - }) - .collect(); - programs.sort_unstable_by_key(|program| program.0); - programs.dedup(); - programs - } - - /// The rules that came into existence in this transaction. - /// - /// A rule that arrived and does not decide now decided nothing at all: both its existence and - /// its activation describe something that was never in the cascade. Knowing that needs the - /// transaction rather than the program, because the program only says where things stand. - pub(super) fn rules_arriving(transaction: &StyleTransaction) -> Vec { - let mut rules: Vec = transaction - .inputs - .iter() - .filter_map(|input| match (input.key, input.new) { - (InputKey::RuleField(rule, RuleField::Existence), InputValue::Flag(true)) => Some(rule), - _ => None, - }) - .collect(); - rules.sort_unstable_by_key(|rule| rule.0); - rules.dedup(); - rules - } - - /// The scopes each sheet was attached to when this transaction began. - /// - /// A sheet's rules reach the elements its scopes hold, and that has to be the scopes as of the - /// input rather than as of routing: re-inserting a ` +

+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/css/style-engine/computed-style-record-view-pins.html b/Tests/LibWeb/Text/input/css/style-engine/computed-style-record-view-pins.html new file mode 100644 index 0000000000000..65c17cb4b7c60 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/style-engine/computed-style-record-view-pins.html @@ -0,0 +1,12 @@ + + +
+ diff --git a/Tests/LibWeb/Text/input/css/style-engine/cross-scope-winner-priority.html b/Tests/LibWeb/Text/input/css/style-engine/cross-scope-winner-priority.html new file mode 100644 index 0000000000000..e68fe99ccf69b --- /dev/null +++ b/Tests/LibWeb/Text/input/css/style-engine/cross-scope-winner-priority.html @@ -0,0 +1,22 @@ + + + + + diff --git a/Tests/LibWeb/Text/input/css/style-engine/layout-and-paint-consume-style-record.html b/Tests/LibWeb/Text/input/css/style-engine/layout-and-paint-consume-style-record.html index bedb50f44f69d..88d3e8b2acc59 100644 --- a/Tests/LibWeb/Text/input/css/style-engine/layout-and-paint-consume-style-record.html +++ b/Tests/LibWeb/Text/input/css/style-engine/layout-and-paint-consume-style-record.html @@ -4,9 +4,13 @@ .shared { color: red; --token: initial; } .metadata { color: red; --token: changed; } .changed { color: blue; --token: changed; } + .pseudo::before { content: "before"; color: red; --token: initial; } + .pseudo.changed::before { content: "before"; color: blue; --token: initial; } + .pseudo.changed.metadata::before { content: "before"; color: blue; --token: changed; }
+
diff --git a/Tests/LibWeb/Text/input/css/style-engine/narrow-rule-removal-refreshes-losing-matches.html b/Tests/LibWeb/Text/input/css/style-engine/narrow-rule-removal-refreshes-losing-matches.html new file mode 100644 index 0000000000000..82586096a0782 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/style-engine/narrow-rule-removal-refreshes-losing-matches.html @@ -0,0 +1,56 @@ + + + diff --git a/Tests/LibWeb/Text/input/css/style-engine/reattached-sheet-refreshes-routing-liveness.html b/Tests/LibWeb/Text/input/css/style-engine/reattached-sheet-refreshes-routing-liveness.html new file mode 100644 index 0000000000000..747e911f3dfa1 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/style-engine/reattached-sheet-refreshes-routing-liveness.html @@ -0,0 +1,39 @@ + + +
+
+
+ diff --git a/Tests/LibWeb/Text/input/css/style-engine/retained-winner-priority-pseudo-target.html b/Tests/LibWeb/Text/input/css/style-engine/retained-winner-priority-pseudo-target.html new file mode 100644 index 0000000000000..899e68a121128 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/style-engine/retained-winner-priority-pseudo-target.html @@ -0,0 +1,20 @@ + + + +
+ diff --git a/Tests/LibWeb/Text/input/css/style-engine/shared-entry-selector-truth.html b/Tests/LibWeb/Text/input/css/style-engine/shared-entry-selector-truth.html new file mode 100644 index 0000000000000..26b2c739d7f9b --- /dev/null +++ b/Tests/LibWeb/Text/input/css/style-engine/shared-entry-selector-truth.html @@ -0,0 +1,27 @@ + + + +
+