Skip to content

Fix incremental layout cache reuse with floats and atomic inline baselines - #1307

Open
nicoburns wants to merge 2 commits into
mainfrom
devin/1793097580-fix-incremental-relayout
Open

nicoburns wants to merge 2 commits into
mainfrom
devin/1793097580-fix-incremental-relayout

Conversation

@nicoburns

@nicoburns nicoburns commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

On main, the browser-layout-bench real/wikipedia leaf_10pct relayout step (every 10th leaf +1px, one incremental resolve()) gives boxes that differ from a fresh layout of the serialised DOM (775 differing elements). Two independent layout-cache bugs cause it:

  1. Same-BFC children cached without their float context. LayoutBlockContainer::compute_block_child_layout wrapped same-BFC child layout in compute_cached_layout, but the mutable BlockContext (floats placed so far, and floats the child itself contributes) isn't part of LayoutInput/the cache key. A cache hit therefore skipped replaying the child's floats into the shared context and reused a layout computed against a different float environment. E.g. <div><div style="float:right;height:200px"></div></div><div style="overflow:hidden">…</div>: changing the second div's width left the float wrapper cached, the float was never re-placed and body came out 0px tall instead of 200px.
    Fix: bypass (and clear) the cache for a same-BFC child when the context can affect it or it can affect the context:

    let affected = if is_inline_root { ctx.has_active_floats(0.0) } else { ctx.has_floats() };
    if affected || self.subtree_contains_floats(node) { clear_layout_cache; compute uncached }

    subtree_contains_floats is memoised per layout pass and is O(1) for inline roots without inline boxes (floats inside an inline formatting context are inline boxes). Block children keep the conservative has_floats() because negative margins can bring them above their parent.

  2. Atomic inline measurements lost baselines and moved children. Inline layout sized atomic inlines with the caller's run_mode, so under ComputeSize Taffy's cache returned LayoutOutput::from_outer_size (no baselines) on a hit, while a recompute had baselines → different line geometry between incremental and fresh layout. Also the inline-box placement loop ran during ComputeSize, overwriting child positions that a later parent PerformLayout cache hit then didn't restore (wikipedia navbox th inline-block at y=5 vs 17).
    Fix: atomic inline children are always laid out with RunMode::PerformLayout (baselines needed), and inline-box positions are only written during PerformLayout.

Tests

incremental_oracle.rs: cached_same_bfc_child_replays_floats, cached_inline_root_moves_below_floats, cached_atomic_inline_measurement_preserves_baselines — each fails without its fix. blitz-runner --check on wikipedia, text/paragraphs/1000, flex/wide/1000: every step matches full restyle and fresh parse (rustdoc's pre-existing round-trip difference is unchanged).

Performance

main ebacd68 vs this PR 9f8d36b, separate release builds, pinned to one core, 10 interleaved process pairs (ABAB) × 5 iters = 50 samples per cell. Layout phase of the timed resolve(), ms, median [p25–p75]; bold = IQRs don't overlap.

case step main PR Δ median
real/wikipedia initial 18.5 [17–20] 20 [18–21] +8%
root_width 8.8 [8.6–9.8] 9.5 [9.3–10] +8%
leaf_1 4.3 [4.1–4.7] 6.3 [6.1–6.7] +47%
leaf_10pct 7.7 [7.5–8.5] 9.6 [9.4–10] +26%
leaf_all 17 [16–19] 17 [17–19] 0%
append 20 [18–22] 21 [19–22] +5%
text/paragraphs/1000 initial 7.1 [6.9–7.8] 7.3 [6.9–7.9] +2%
root_width 7.0 [6.8–7.3] 7.0 [6.8–7.1] 0%
leaf_1 0.232 [0.229–0.240] 0.290 [0.284–0.295] +25%
leaf_10pct 0.95 [0.94–1.0] 1.0 [1.0–1.1] +6%
leaf_all 7.1 [6.8–7.4] 7.1 [6.9–7.5] −1%
append 1.1 [1.0–1.2] 1.15 [1.1–1.3] +5%

The cost is in small incremental relayouts: about +2 ms on wikipedia leaf_1 (+42% of total resolve(), 4.8 → 6.8 ms) and about +2 ms layout on leaf_10pct (+3% of total resolve(), which is dominated by style); paragraphs leaf_1 is about +60 µs. Other steps are within noise. Most likely this comes from the conservative cache bypass (float-adjacent same-BFC subtrees laid out uncached, plus the per-pass float-subtree walk). The phase timer rounds ≥10 ms values to whole ms.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/48e12aa27062474cb1529350310c9c58
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/48e12aa27062474cb1529350310c9c58?variant=devin-insiders

Co-Authored-By: Staging-Devin AI <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Staging-Devin AI <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com>
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access that start with 'Devin'.

  • Disable automatic comment, CI, and merge conflict monitoring

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant