Skip to content

Fix flex-shrink runaway when the sole shrinkable item has an aspectRatio basis - #2039

Open
sebholl wants to merge 1 commit into
react:mainfrom
sebholl:fix/flex-shrink-aspect-ratio-residual
Open

sebholl wants to merge 1 commit into
react:mainfrom
sebholl:fix/flex-shrink-aspect-ratio-residual

Conversation

@sebholl

@sebholl sebholl commented Oct 14, 2026

Copy link
Copy Markdown

Summary

Fixes #2038.

When the only shrinkable item on an overflowing row has its flex basis derived from aspectRatio, distributeFreeSpaceSecondPass divides the remaining free space by a 2^-19 residual left in totalFlexShrinkScaledFactors, and the item is measured at ~8.3e9 instead of 0. Sibling of #1665 / #1974 / #1783; the absolute 1e-6f guard from #1974 does not catch this residual.

Mechanism (full trace in #2038): calculateFlexLine accumulates round(-3 * 21.083334f) = -63.25. distributeFreeSpaceFirstPass then subtracts the same product inline, which clang contracts into a fused multiply-add using the unrounded product 63.2500019, leaving exactly 2^-19 = 1.9e-6. Building with -ffp-contract=off makes the bug disappear, which is also why it never shows in the wasm playground.

Two changes:

  1. distributeFreeSpaceFirstPass: round the excluded product to float before subtracting it. This is the cause, and on its own it fixes the repro under the default -ffp-contract=on.
  2. Replace the 1e-6f epsilon in distributeFreeSpaceSecondPass with an exact count of shrinkable items left on the line (FlexLineRunningLayout::numberOfShrinkableItems, kept in lockstep with the total). "Are any shrinkable items left?" is a counting question, and no absolute epsilon is right because the residual scales with flexShrink * flexBasis. This also keeps the fix correct under -ffp-contract=fast, where (1) alone can still be contracted across statements.

Left alone deliberately: the first-pass division (remainingFreeSpace / totalFlexShrinkScaledFactors inside distributeFreeSpaceFirstPass) is structurally exposed to the same issue if an item freezes mid-loop, but I have no repro for it, and a count-based guard cannot apply there because the current item is not yet frozen. Happy to add something if a maintainer prefers.

Test plan

🤖 This PR includes AI-assisted changes via Claude Code.

…tio basis

Fixes react#2038.

distributeFreeSpaceFirstPass subtracted the frozen item's shrink-scaled
factor inline, which the compiler contracts into a fused multiply-add with
the unrounded product. It therefore did not cancel against the float
accumulator built in calculateFlexLine and left a 2^-19 residual that
distributeFreeSpaceSecondPass divided the remaining free space by,
measuring the item at ~8.3e9 instead of 0. The absolute 1e-6f guard from
react#1974 does not cover it.

Round the product to float before subtracting, and replace the epsilon
with an exact count of shrinkable items left on the line.

🤖 Co-authored-by: Claude <noreply@anthropic.com>
@intergalacticspacehighway

Copy link
Copy Markdown
Collaborator

Thanks @sebholl for the analysis and repro! I've opened #2044, which sums the shrink factors of the unfrozen items instead of subtracting the frozen ones. That makes the total itself exact (0 when every item is frozen), so no separate count is needed, and it also fixes #931. I've also included your aspectRatio tests there.

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

CLA Signed Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

flexShrink runaway (~8e9) when the sole shrinkable item has an aspectRatio basis; FMA residual slips past the 1e-6f guard from #1974

2 participants