Split pseudo-element rules whose originating compound contains :is() or
:where() across the cheap buckets from their selector-list alternatives
when the rule would otherwise land in the pseudo-element other bucket.
Use per-collection generation stamps to suppress duplicate candidates
when more than one alternative bucket applies to the same originating
element. Add text coverage for class, attribute, and complex combinator
arguments reaching generated pseudo-element style.
Add a late rule-cache bucket for selectors that do not expose a
current-element bucket but do expose ancestor-filter metadata. These
rules previously stayed in the global other bucket and relied on the
ancestor filter after broad enumeration.
Visit the new buckets only when the current ancestor bloom filter may
contain the bucket hash. Keep pseudo-class invalidation caches opted
out because those caches answer targeted invalidation questions rather
than normal style collection.
On the GitHub profile, other-bucket candidates fell from about 5.50k
to about 1.07k per 1k rule collection calls.
Put rules whose rightmost compound has no id, class, tag, or
attribute bucket into a subject pseudo-class bucket when they have a
required cheap element-state pseudo-class such as :hover, :focus, or
:checked.
Also let :is() and :where() selector lists use a regular bucket when
every alternative exposes the same mandatory bucket key. Pseudo-class
invalidation rule caches keep using broad buckets because they must
consider rules while the matching state is changing.
Keep pseudo-element style rules out of the normal element rule buckets
while preserving the same id, class, tag, attribute and root buckets
inside each known pseudo-element type. This avoids collecting pseudo
rules for normal element style, without broadening pseudo style
collection to every rule targeting the queried pseudo-element.
Update the has invalidator to walk both the normal rule buckets and
the pseudo-element bucket maps when deciding whether pending :has()
mutations may affect style.
Split StyleCache's rule matching data from its invalidation metadata.
Allow callers to build either payload independently. Style invalidation
queries no longer force a full rule cache rebuild, and rule matching
no longer builds invalidation metadata as a side effect.
Previously, callers often treated an absent rule cache as proof that no
style invalidation metadata existed. That made some invalidation paths
do nothing until something else had populated the rule cache first.
Build invalidation data before reading it instead, so these paths use
the metadata whenever stylesheet rules require it.
Rebaseline style invalidation counter expectations for the new lazy
build points. Flush setup style work in the structural :has() feature
filter test before measuring each mutation, so the recomputation
counters describe the mutation itself instead of leftover setup work.
Keep regular rule cache invalidation from discarding the parsed user
style sheet. User style and content blocker source changes now use an
explicit invalidation path that clears the parsed sheet before the rule
cache is rebuilt.
This avoids reparsing user CSS for unrelated style changes, which was
very annoying when browsing with cosmetic CSS from content blockers.
UA and user stylesheets do not have owner nodes or owner rules, so their
matched rules cannot be mapped through the generic stylesheet identifier
helper. Share the built-in UA stylesheet enumeration and map UA rules by
the matched CSSStyleSheet object, so that they stay in sync with any
future changes.
Collect the style rules that apply to an inspected element and expose
them through the existing DOM node inspection path. This gives Firefox's
Rules panel real rule forms instead of the previous empty getApplied
response.
Treat a scoped `@import` rule as a scope descriptor while traversing
style-producing rules from imported stylesheets. Imported rules now
inherit an outer scope for unscoped imports and replace it when the
import itself carries scope(...).
Scope resolution was generalized to handle both CSSScopeRule and
CSSImportRule descriptors. Import-scope boundaries are matched using
the stylesheet that parsed the `@import` rule, while normal imported
selectors keep using the imported stylesheet context. This lets
implicit scopes, nested `@scope` rules, :scope, and top-level `&` behave
as the Cascade 6 model requires.
Keep the temporary scoped matching rule list on StyleComputer.
This lets repeated style matching reuse its allocation instead of
creating a large stack object in collect_matching_rules().
Release builds zero that stack object before the constructor runs due
to automatic variable initialization. This was causing a 16 KiB memset
on every call to this function(!)
Verify that the scratch vector is empty on entry so accidental reentry
is caught before it can corrupt the outer rule collection.
Do not invalidate shadow-root rule caches when the document user style
sheet changes. Shadow trees still need style invalidation because
document user rules can match their descendants, but their local author
rule caches do not include the document user sheet.
Since user and content-blocker sheets now live only in the document
scope, shadow-DOM state changes also have to consult the document user
selector insights for :has() and pseudo-class invalidation.
Add content blocker coverage for user-style refreshes, shadow :has()
state changes, and shadow pseudo-class invalidation.
Cache lightweight selector insights on each stylesheet so cold style
scopes can answer whether :has() invalidation is relevant without
building a complete rule cache. This avoids forcing rule caches for
shadow roots whose active sheets do not contain :has() selectors.
Imported sheets contribute to their parent sheet's effective rules, so
an imported sheet load or CSSOM change also clears ancestor selector
insight caches.
Add test-only counters and regression coverage for cold shadow roots
with and without :has() selectors, plus delayed imported :has() rules.
The base URL change handler checked whether any style scope contained
a `:local-link` rule before walking the document's links, this forced
a rule cache rebuild, which is generally slower than the link walk we
were trying to avoid.
On Speedometer2 the duplicated rule-cache and invalidation-set
construction accounted for roughly 4% of total samples. Removing the
gate lets the URL-unchanged early-exit handle the no-op case and runs
the link walk only when the URL actually changes, which the profile
showed to be inexpensive on its own.
`@scope (a) to (b) {}` applies its contained style rules to elements
that have `a` as a parent, and do not have `a b` as a parent. Both the
`a` and `b` selector lists are optional.
Because it's situational whether a `@scope` will apply to a given
element, we store the ancestor scope on the `MatchingRule`, similar to
`@container`, and then determine during matching whether all the parent
`@scope`s match or not.
The rules for how selectors inside `@scope` are adjusted and interpreted
are a bit confusing. Unlike for other at-rules, nested style rules
inside `@scope` do not get a leading `&` added during parsing. To
support this, `adapt_nested_relative_selector_list()` now takes a flag
for whether its parent is a `@scope` or not.
`@scope` can also contain nested declarations without itself being
nested inside a style rule.
When determining their selectors, nested declarations rules adopt the
`@scope`'s scoping root if it has one, or otherwise fall back to the
parent element of the `<style>` element (not implemented here,) or the
`:root`. These are required to have zero specificity, so we wrap the
selector in `:where()`.
e.g., `@container (width >= 300px) {}` and similar.
During style computation, flag any elements whose style depends on a
size container. Then re-evaluate their style after the initial layout
has been computed and size containers have a size. This may take
multiple passes, as these may have further descendants that depend on
their size, etc. We limit this to 8 passes currently.
SizeFeature itself is very similar to MediaFeature, but queries the
container element instead. There are only 6 size features specified, so
they're hard-coded instead of generated from JSON.
Also add a counter test for the narrower restyle path.
MatchingRules now have a container_rule member which stores the nearest
ancestor CSSContainerRule, if any. When populating the rule cache, we
maintain a stack of CSSContainerRules that we are within, and use
record the last one on the MatchingRule, so that it's O(1) instead of
having to walk up the rule's ancestors each time. This does mean we
have reimplement some "for each rule" code.
When collecting rules to apply to an element, we see if the MatchingRule
has a container_rule, and if so, we evaluate that rule's query to see
if the element has a matching container. We then also match any ancestor
container rules, using the cached parent container rule.
Add a custom test to cover the case of nested name-only `@container`s,
which WPT lacks currently as far as I can tell.
Currently, this is the only pseudo class which is URL-sensitive. When
we implement proper tracking of visited URLs this will need to be
replaced with something more comprehensive.
Avoid repeated :has() child-list scheduling when pending data already
covers every concrete feature bucket used by :has() selectors in a style
scope. Featureless-sensitive scopes record child-list mutations
conservatively instead.
That conservative path avoids scanning mutation subtrees for concrete
features when invalidation cannot rely on them anyway. When loading
the Intel ISA PDF in pdf.js, instrumented subtree feature-collection
visits at about 40k style invalidations dropped from around 71k to 1.6k,
saving ~650ms of main thread time on my Linux machine. :^)
Collect concrete features from pending :has() mutations and use them to
avoid re-invalidating non-subject :has() anchors whose matching rules
cannot be affected by the mutation.
Keep conservative behavior for shadow-boundary fanout, structural
sibling changes, and selectors whose relevant features cannot be proven.
For sibling-combinator relative selectors, avoid marking an anchor as
handled until it is actually invalidated, and make the sibling scan
respect anchors skipped by the feature filter for the same mutation.
Keep StyleScope responsible for storing pending :has() mutation state,
but move the invalidation walk and scheduling helpers into
CSS::Invalidation::HasMutationInvalidator. This keeps the style scope
from owning the :has() invalidation algorithm directly and gives later
changes a narrower place to optimize.
Every DOM mutation that may affect a :has() selector enqueues an
entry in StyleScope keyed by an ancestor node. The entries were
previously stored in a Vector and linearly scanned on every insert to
deduplicate by node. We now use an OrderedHashMap instead, eliminating
the quadratic deduplication.
When a :has() mutation is known to come from a specific subtree, use
that subtree as the mutation root while walking observed ancestors.
Before dirtying an anchor and its non-subject descendants, check whether
any cached :has() rule for that anchor can observe the changed subtree.
This keeps unrelated descendant mutations from invalidating every rule
that merely contains :has().
Share the style cache for shadow roots whose only active author sheet is
the same constructed stylesheet. Matching already carries the effective
shadow root separately, so the cache can be reused while selectors such
as :host and ::slotted() still evaluate against each consuming shadow
root.
Keep the optimization conservative by falling back to the existing
per-scope cache whenever the shadow root has multiple active sheets, a
non-constructed sheet, or a page user stylesheet. Drop the shared cache
when the stylesheet rules or media query match state change.
Add coverage for two shadow roots adopting the same constructed sheet,
including :host, ::slotted(), and replaceSync() invalidation.
Keep cached MatchingRule entries independent from the shadow root that
owns the rule cache. Thread the effective rule shadow root through style
matching as transient state instead, so a rule cache can later be shared
by multiple scopes without copying every cached rule.
This preserves the existing matching behavior by deriving the effective
rule root from each cache lookup site. Pseudo-class invalidation already
operates on a single style scope, so it no longer needs a per-rule scope
filter.
Track whether any :has() relative selector in a style scope uses a
sibling combinator and let the generic ancestor walk consult that
before scanning ancestor siblings.
This keeps descendant-only :has() invalidations from walking unrelated
siblings while preserving the existing behavior for selectors that use
+ or ~. Add counter-based test coverage so the reduced sibling scans
stay visible through the invalidation counters.
This was a pretty straightforward change of storing registered counter
styles on the relevant `StyleScope`s and resolving by following the
process to dereference a global tree-scoped name, the only things of
note are:
- We only define predefined counter styles (e.g. decimal) on the
document's scope (since otherwise overrides in outer scopes would
themselves be overriden).
- When registering counter styles we don't have the full list of
extendable styles so we defer fallback to "decimal" for undefined
styles until `CounterStyle::from_counter_style_definition`.
Stop rebuilding the counter style cache from every style update.
That made unrelated restyles pay the full counter-style cost even when
no relevant stylesheet state had changed.
Dirty the cache when stylesheet rule caches are invalidated and rebuild
it on the first counter-style lookup instead. Also make cold cache
rebuilds include user stylesheets.
Add regression tests covering insertRule() and replaceSync() updates
that should make newly defined counter styles take effect.
Replace the unsafe HashTable<GC::Weak<DOM::Node>> with
GC::WeakHashSet<DOM::Node>. The null check in the iteration loop is
no longer needed since WeakHashSet's iterator skips dead entries.
Add a document-level boolean flag that tracks whether any :has()
invalidations have been scheduled. This avoids iterating over all
shadow roots just to check is_empty() on each style scope when no
:has() invalidations are pending, which is the common case during
scrolling on complex pages like Reddit.
Results in ~10% reduction of is_empty() calls in profiles when
scrolling on Reddit.
This adds visit_edges(Cell::Visitor&) methods to various helper structs
that contain GC pointers, and makes sure they are called from owning
GC-heap-allocated objects as needed.
These were found by our Clang plugin after expanding its capabilities.
The added rules will be enforced by CI going forward.
Before this change, we've been maintaining various StyleComputer caches
at the document level.
This made sense for old-school documents without shadow trees, since
all the style information was document-wide anyway. However, documents
with many shadow trees ended up suffering since any time you mutated
a style sheet inside a shadow tree, *all* style caches for the entire
document would get invalidated.
This was particularly expensive on Reddit, which has tons of shadow
trees with their own style elements. Every time we'd create one of their
custom elements, we'd invalidate the document-level "rule cache" and
have to rebuild it, taking about ~60ms each time (ouch).
This commit introduces a new object called StyleScope.
Every Document and ShadowRoot has its own StyleScope. Rule caches etc
are moved from StyleComputer to StyleScope.
Rule cache invalidation now happens at StyleScope level. As an example,
rule cache rebuilds now take ~1ms on Reddit instead of ~60ms.
This is largely a mechanical change, moving things around, but there's
one key detail to be aware of: due to the :host selector, which works
across the shadow DOM boundary and reaches from inside a shadow tree out
into the light tree, there are various places where we have to check
both the shadow tree's StyleScope *and* the document-level StyleScope
in order to get all rules that may apply.