Repository navigation
Fix flex shrink explosion when the shrink total is a positive float residual - #2044
Conversation
The second pass divided the remaining free space by the shrink total minus the frozen items' factors, and in float that subtraction leaves a residual (react#2038, react#1651). Sum the factors of the items that are not frozen instead, which is exactly 0 when none remain, and then send items to their lower bound (react#931).
162e1de to
fef4cff
Compare
|
@cortinico has imported this pull request. If you are a Meta employee, you can view this in D126038629. |
|
@cortinico Thanks for importing. Facebook Internal builds & tests seems to be failing. Could you share which tests fail? |
Those were noisy signals. It's queued for landing now @intergalacticspacehighway 👍 |
|
@intergalacticspacehighway actually there is a real failure going on which we found in an internal React Native app. (This is AI generated 👇) The broken screen has a constrained This is verified with differential CI:
The likely issue is this new zero-total handling: Passing a synthetic zero through Could we keep the new approach of summing the unfrozen shrink factors, but preserve each frozen item's bounded first-pass size and reuse that exact size in the second pass? Conceptually: This would avoid the floating-point cancellation issue without using 0 as a sentinel to retrigger the constraint. It should also preserve the intended fixes for cases such as Also: it would be useful to add a regression test for a constrained row containing an auto-min/content-sized flexible item and non-shrinking siblings, asserting that the frozen sizes from the first pass remain unchanged in the second pass. |
After the first pass subtracts the frozen items' scaled shrink factors, float cancellation can leave a residual instead of 0. When the residual was positive, the second pass divided by it and sized items in the billions (react#2038, react#1651). With non-negative flexShrink a real total is never positive, so positive totals now use the existing near-zero fallback. Negative residuals and all other layouts are unchanged.
85d16c9 to
c875527
Compare
|
@cortinico I reduced the scope of this PR to the explosion fix only (#2038, #1651). A positive shrink total, which can't happen with non-negative The regression most likely came from the Could you re-import the latest commit and rerun the internal tests? It would also help to have the styles of the failing layout, I will add it as a regression test for the follow-up. |
…esidual Summary: Fixes react/yoga#2038 Fixes react/yoga#1651 Closes react/yoga#2039 Closes react/yoga#1855 Thanks to sebholl for the analysis and the reproduction in react/yoga#2038. ## Issue When a flex line overflows, the first pass freezes the items that hit their minimum size and removes them from the shrink total. The second pass then divides the remaining free space by that total. The total was computed by subtracting the frozen items flex shrink factors from the float total. When every item freezes, the total should become 0, but float subtraction leaves a small residual. ### Example All items have `flexShrink: 1`: ``` row (width: 350, flexDirection: row) ├── A width: 100.1 minWidth: 90 ├── B width: 200.3 minWidth: 190 └── C width: 300.7 minWidth: 290 ``` The items need 601.1 but the row is only 350, so they must shrink by 251.1. Each item would go below its `minWidth`, so the first pass freezes all three (at 90 / 190 / 290) and removes them from the shrink total. With no items left, the total should be 0. But in float, `-601.1 + 100.1 + 200.3 + 300.7 = 0.000061`. That is above the `1e-6` guard added in react/yoga#1974 (for react/yoga#1665), so the second pass divides the remaining free space by it: `-220 / 0.000061 ≈ -3.6 million`. Instead of staying at their min, the items grow to about **361M / 722M / 1084M** (expected **90 / 190 / 290**). ### Solution With non-negative `flexShrink`, every scaled shrink factor is ≤ 0, so a real total is never positive. A positive total can only be this float residual, and dividing by it is what makes the items explode. The second pass now treats a positive total like the near-zero case of the `1e-6` guard from react/yoga#1974 and uses the existing fallback. Negative residuals (which the min clamp already handles) and all other layouts are unchanged. Fixing the fallback for `flexShrink < 1` (react/yoga#931) is left for a separate PR. #### Why not `inexactEquals` (|total| < 0.0001)? The leftover grows with the size of the total. For example, 60 chips of about 100pt each in a 1000pt row give a total of about 6000, and the leftover is then often larger than 0.0001. In a test with rows like this, about half of them still exploded with `inexactEquals`. With the sign check, none did, because a real shrink total is never positive. X-link: react/yoga#2044 Reviewed By: javache Differential Revision: D126038629 Pulled By: cortinico fbshipit-source-id: dc1ec3370783e227ce4477bcd632ae5907034193
…esidual Summary: Fixes react/yoga#2038 Fixes react/yoga#1651 Closes react/yoga#2039 Closes react/yoga#1855 Thanks to sebholl for the analysis and the reproduction in react/yoga#2038. ## Issue When a flex line overflows, the first pass freezes the items that hit their minimum size and removes them from the shrink total. The second pass then divides the remaining free space by that total. The total was computed by subtracting the frozen items flex shrink factors from the float total. When every item freezes, the total should become 0, but float subtraction leaves a small residual. ### Example All items have `flexShrink: 1`: ``` row (width: 350, flexDirection: row) ├── A width: 100.1 minWidth: 90 ├── B width: 200.3 minWidth: 190 └── C width: 300.7 minWidth: 290 ``` The items need 601.1 but the row is only 350, so they must shrink by 251.1. Each item would go below its `minWidth`, so the first pass freezes all three (at 90 / 190 / 290) and removes them from the shrink total. With no items left, the total should be 0. But in float, `-601.1 + 100.1 + 200.3 + 300.7 = 0.000061`. That is above the `1e-6` guard added in react/yoga#1974 (for react/yoga#1665), so the second pass divides the remaining free space by it: `-220 / 0.000061 ≈ -3.6 million`. Instead of staying at their min, the items grow to about **361M / 722M / 1084M** (expected **90 / 190 / 290**). ### Solution With non-negative `flexShrink`, every scaled shrink factor is ≤ 0, so a real total is never positive. A positive total can only be this float residual, and dividing by it is what makes the items explode. The second pass now treats a positive total like the near-zero case of the `1e-6` guard from react/yoga#1974 and uses the existing fallback. Negative residuals (which the min clamp already handles) and all other layouts are unchanged. Fixing the fallback for `flexShrink < 1` (react/yoga#931) is left for a separate PR. #### Why not `inexactEquals` (|total| < 0.0001)? The leftover grows with the size of the total. For example, 60 chips of about 100pt each in a 1000pt row give a total of about 6000, and the leftover is then often larger than 0.0001. In a test with rows like this, about half of them still exploded with `inexactEquals`. With the sign check, none did, because a real shrink total is never positive. X-link: react/yoga#2044 Reviewed By: javache Differential Revision: D126038629 Pulled By: cortinico fbshipit-source-id: dc1ec3370783e227ce4477bcd632ae5907034193
Fixes #2038
Fixes #1651
Closes #2039
Closes #1855
Thanks to @sebholl for the analysis and the reproduction in #2038.
Issue
When a flex line overflows, the first pass freezes the items that hit their minimum size and removes them from the shrink total. The second pass then divides the remaining free space by that total.
The total was computed by subtracting the frozen items flex shrink factors from the float total. When every item freezes, the total should become 0, but float subtraction leaves a small residual.
Example
All items have
flexShrink: 1:The items need 601.1 but the row is only 350, so they must shrink by 251.1. Each item would go below its
minWidth, so the first pass freezes all three (at 90 / 190 / 290) and removes them from the shrink total. With no items left, the total should be 0. But in float,-601.1 + 100.1 + 200.3 + 300.7 = 0.000061.That is above the
1e-6guard added in #1974 (for #1665), so the second pass divides the remaining free space by it:-220 / 0.000061 ≈ -3.6 million. Instead of staying at their min, the items grow to about 361M / 722M / 1084M (expected 90 / 190 / 290).Solution
With non-negative
flexShrink, every scaled shrink factor is ≤ 0, so a real total is never positive. A positive total can only be this float residual, and dividing by it is what makes the items explode. The second pass now treats a positive total like the near-zero case of the1e-6guard from #1974 and uses the existing fallback. Negative residuals (which the min clamp already handles) and all other layouts are unchanged. Fixing the fallback forflexShrink < 1(#931) is left for a separate PR.Why not
inexactEquals(|total| < 0.0001)?The leftover grows with the size of the total. For example, 60 chips of about 100pt each in a 1000pt row give a total of about 6000, and the leftover is then often larger than 0.0001. In a test with rows like this, about half of them still exploded with
inexactEquals. With the sign check, none did, because a real shrink total is never positive.