stylo: Coalesce all sibling reconstructs when processing nsChangeLists
Categories
(Core :: CSS Parsing and Computation, enhancement, P4)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox57 | --- | wontfix |
People
(Reporter: bholley, Assigned: mayankleoboy1, NeedInfo)
References
(Blocks 1 open bug)
Details
Attachments
(1 external file, 2 obsolete files)
Comment 1•9 years ago
|
||
| Reporter | ||
Updated•9 years ago
|
Updated•9 years ago
|
Comment 2•9 years ago
|
||
Updated•4 years ago
|
| Assignee | ||
Comment 3•4 months ago
|
||
Worth re-analyzing for SP3?
| Assignee | ||
Comment 4•3 months ago
|
||
We already coalesce adjacent siblings for lazy frame construction, but
nsChangeHint_ReconstructFrame on elements that already have frames is still
handled one element at a time. Each of those does its own insertion, and
nsBlockFrame::AddFrames searches the line list for every one of them, so
reframing N adjacent children of a block is quadratic in the number of lines
(bug 2023352).
Collect a run of adjacent siblings that all want a plain, synchronous reframe,
remove their frames, and then rebuild them with a single insertion, which is
what bug 1338921 left as a follow-up. Since the run is appended in DOM order,
removal walks the line list front to back on a shrinking list, and the single
insertion either appends or does one line search instead of N.
Only content taking the straightforward path in RecreateFramesForContent can be
batched. Anything that reconstructs an ancestor instead, defers to an async
reframe, or deliberately doesn't re-insert (display: none) is excluded up front
by CanBatchReframe. A run can still discover mid-removal that it has to
reconstruct an ancestor, and in that case we stop: every ancestor that
MaybeRecreateContainerForFrameRemoval reframes is at or above the parent the
siblings share, so it also rebuilds the frames we already removed.
Updated•3 months ago
|
| Assignee | ||
Comment 5•3 months ago
|
||
Out-of-flow reframes take the same path as in-flow ones: ContentWillBeRemoved
removes the placeholder, which tears down the out-of-flow frame with it, and
the insertion constructs both again. The placeholder is what sits in the
parent's line list, so a run of absolutely positioned or floated siblings
hits the same quadratic line list search when reframed one at a time.
| Assignee | ||
Comment 6•3 months ago
|
||
Text nodes only get change list entries of their own when their parent is
display: contents (see the assertion in nsStyleChangeList::AppendChange), and
those entries land between the entries of their reconstructed element
siblings, breaking the run. Any hint on a frame that the batched reframe is
about to remove and rebuild is subsumed by the rebuild, so the scan can step
over such entries. They are consumed only when a later sibling actually
extends the run past them; a trailing entry with no member after it keeps its
frame and is processed normally.
| Assignee | ||
Comment 7•3 months ago
|
||
| Assignee | ||
Comment 8•3 months ago
|
||
| Assignee | ||
Comment 9•3 months ago
|
||
Emilio, woud you mind taking a look at the patches and give feedback on what look wrong or what can be improved? All comments would be helpful.
Alternately, please redirect to someone more appropriate.
Comment 10•3 months ago
|
||
So at a glance:
- I don't understand why we need the special-case here. Seems like
RecreateFramesForContentcould be implemented in terms ofRecreateFramesForContentRange - I don't think
CanBatchReframeis generally sound, it doesn't deal with shadow dom and what not. It should use the same mechanism that we use for inserts (e.g.IssueSingleInsertNofications). - I don't get why the mathml or out-of-flow special cases either.
| Assignee | ||
Updated•3 months ago
|
| Assignee | ||
Comment 11•3 months ago
•
|
||
Thanks. I've folded in the first and third points; on the second I'd like to check what you mean before rewriting.
I don't understand why we need the special-case here. Seems like
RecreateFramesForContentcould be implemented in terms ofRecreateFramesForContentRange
Done. Its tail was already exactly that, so it now ends with
RecreateFramesForContentRange(aContent, aContent->GetNextSibling(), aInsertionKind);
and the cases above it that reframe something other than the content itself (NAC root, MathML, leaf parent, MaybeRecreateContainerForFrameRemoval, display: none, async) stay where they are. display: none and async keep their own ContentWillBeRemoved call since neither re-inserts. RecreateFramesForContentRange also lost its ContentAppended branch, since ContentAppended is ContentRangeInserted(x, nullptr) anyway.
I don't get why the mathml or out-of-flow special cases either.
The out-of-flow one is my fault: the first patch excluded out-of-flows and the second removed the exclusion, so there was never one in the end state. I've squashed the two, and D314252 can be abandoned.
MathML is only there because RecreateFramesForContent hoists to the topmost MathML element for bug 397518, so a run of MathML siblings would each redirect to a shared ancestor rather than reframe themselves. If CanBatchReframe goes away it goes with it.
I don't think
CanBatchReframeis generally sound, it doesn't deal with shadow dom and what not. It should use the same mechanism that we use for inserts (e.g.IssueSingleInsertNofications).
Agreed on the general shape: it hand-mirrors RecreateFramesForContent's preconditions and will drift out of sync with them.
On shadow DOM specifically, I think the batch path is already going through that mechanism, and I'd like to confirm I'm not missing a case. RecreateFramesForContentRange ends in ContentRangeInserted, and a multi-node range there goes to GetRangeInsertionPoint, which bails to IssueSingleInsertNofications for any parent with a shadow root (nsCSSFrameConstructor.cpp:6042) and returns an empty insertion point, so the run degrades to single inserts. I've added coverage to the crashtest and mochitest for hosts with named slots, a default slot, nothing slotted, and a run inside a shadow tree.
So before I rewrite : do you want CanBatchReframe deleted outright, with RecreateFramesForContentRange splitting the range itself on the first node needing one of the escape paths, the way GetRangeInsertionPoint splits? That's what I'd write if the up-front predicate is the objection, but it rewrites the first patch so I'd rather check first.
sp3 perfcompare: https://perf.compare/compare-results?baseRev=74b838b5cde3ad53fc511d7f9b8bba6091ea3249&baseRepo=try&newRev=cbec5d47f462a39bd48335670ada0bb4835f6ebf&newRepo=try&framework=13
layout preset tests debug: https://treeherder.mozilla.org/jobs?repo=try&revision=10d166bb1944a86c9276949d3abb1bfdd91edfea
Layout preset opt tests: https://treeherder.mozilla.org/jobs?repo=try&revision=3df40958f88d3ead552d0d4f11abade897821104
Profile of the testcase from bug 023352
Toggle: https://share.firefox.dev/45AbEIY (1s/1.5s)
| Assignee | ||
Comment 12•3 months ago
|
||
Emilio, there is a question in comment 11.
Updated•3 months ago
|
Updated•3 months ago
|
Comment 13•3 months ago
|
||
do you want CanBatchReframe deleted outright, with RecreateFramesForContentRange splitting the range itself on the first node needing one of the escape paths, the way GetRangeInsertionPoint splits? That's what I'd write if the up-front predicate is the objection, but it rewrites the first patch so I'd rather check first.
Yeah, that's what I was envisioning fwiw. But I don't know why we need a bunch of those "escape paths".
Updated•3 months ago
|
| Assignee | ||
Comment 14•3 months ago
•
|
||
(In reply to Emilio Cobos Álvarez [:emilio] from comment #13)
do you want CanBatchReframe deleted outright, with RecreateFramesForContentRange splitting the range itself on the first node needing one of the escape paths, the way GetRangeInsertionPoint splits? That's what I'd write if the up-front predicate is the objection, but it rewrites the first patch so I'd rather check first.
Yeah, that's what I was envisioning fwiw. But I don't know why we need a bunch of those "escape paths".
Yeah, that's what I was envisioning fwiw. But I don't know why we need a bunch of those "escape paths".
The second sentence is a new question, and it's aimed at pre-existing code, not mine. I traced all seven:
| Escape | Load-bearing? |
|---|---|
!GetComposedDoc() |
Yes — bug 157322, chrome frame tree |
| NAC root | Emilio's own TODO at nsCSSFrameConstructor.cpp:7940 already asks if it's removable |
| MathML hoist | Yes — bug 397518, exponential blowup |
parent->IsLeaf() |
Yes — editor NAC; insert would create no frame |
MaybeRecreateContainerForFrameRemoval |
Yes — only discoverable during removal |
| display:none | Efficiency only (earlier a11y notifications), not correctness |
| Async | Emilio's own TODO asks if lazy FC made it redundant |
Two of the ones he's questioning he already flagged himself years ago.
Both patches rewritten. Branch bug1344139-coalesce-reframes: 48e8cba + bcfadde.
Patch 1 — CanBatchReframe is gone
RecreateFramesForContent is now literally:
RecreateFramesForContentRange(aContent, aContent->GetNextSibling(), aInsertionKind);
All escapes moved into one MaybeRecreateFramesForSomethingElse that both entry points share, so there's no second copy of the conditions to drift. The range function splits at any node that hits one. RestyleManager lost the predicate and the if/else — it always calls the range function now.
A correctness trap I hit and had to design around: escapes aren't uniform. MaybeRecreateContainerForFrameRemoval, the NAC hoist and the leaf-parent hoist all reframe an ancestor, which subsumes siblings whose frames I'd already removed — inserting the pending run afterward would double-insert. A bool return would have shipped that bug. Hence the three-valued ReframeOutcome (Plain / Handled / ReframedAncestor), where only Handled flushes the pending run and ReframedAncestor abandons it. I also verified GetTopmostMathMLElement is idempotent (nsCSSFrameConstructor.cpp:7854 — it breaks immediately when already topmost), so the MathML redirect can't recurse forever.
One accepted nuance: for Handled outcomes inside a multi-node range, the escape runs before the pending run is re-inserted. No ancestor is destroyed on those paths, so it's safe, but it is an ordering difference from the unbatched path.
Patch 2 — the hint bug is fixed by the restructure
Emilio's two comments turned out to be the same fix. Folding into the sibling walk deletes the separate non-element branch, so non-element entries now pass through the existing nsChangeHint_ReconstructFrame check — which is precisely the guarantee he asked for. IsNonElementSiblingInReframeRun is gone.
It's also strictly better than before: a non-element entry is now consumed directly and its node lands inside the reframed range, instead of only being consumed when a later element happened to extend past it.
I did not remove the two TODO'd escapes — here's why
You asked me to, and I'm flagging this rather than doing it silently, because the evidence went the other way:
NAC escape. ShouldRecreateContainerForNativeAnonymousContentRoot justifies each of its exclusions in-code with "it's all abspos, so relative insertion order doesn't really matter". That's a direct statement that for everything not excluded, insertion order does matter — which confirms the first TODO's stated premise (the insertion-point rework is still needed) rather than the "maybe we can just remove this" concession. Emilio also refined this exact function in Nov 2022 (bug 1802533) and kept the escape.
Async escape. Emilio restructured this exact block in Nov 2025 (2dab799, bug 1948933 — itself a quadratic-display:none fix) and carried the TODO forward rather than acting on it. And 27 of 28 RecreateFramesForContent call sites pass Async, so deleting it changes the dominant reframe path across all of frame construction — a blast radius unrelated to this bug, and one that would make any regression here un-bisectable against the batching.
The restructure makes both easier to remove later: each is now a single block in one shared function. The async one is the more tractable follow-up, since non-elements already take the lazy path today, so ConstructLazily is already exercised.
debug try: https://treeherder.mozilla.org/jobs?repo=try&revision=ea0fed544ad2e5632c30279c47da1daba9601ade
opt try: https://treeherder.mozilla.org/jobs?repo=try&revision=7a465cc4a539d718f9e551fc89fe0aa17bb68f68
| Assignee | ||
Comment 15•3 months ago
|
||
Patches updated and pushed to try.
See the reasoning in comment 14. Let me know if this was not the direction you meant.
Comment 16•3 months ago
|
||
The change looks about right but I don't know why the in-out parameter is needed. Isn't the mathml case basically equivalent to "reframed an ancestor"?
Updated•3 months ago
|
Updated•3 months ago
|
| Assignee | ||
Comment 17•3 months ago
|
||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
| Assignee | ||
Comment 19•3 months ago
|
||
| Assignee | ||
Comment 20•2 months ago
|
||
Updated•2 months ago
|
Description
•