Skip to content

Commit 24a3be5

Browse files
Fix flex shrink explosion when the shrink total is a positive float residual
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
1 parent d63648b commit 24a3be5

1 file changed

Lines changed: 6 additions & 2 deletions

File tree

‎packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,12 +1138,16 @@ static float distributeFreeSpaceSecondPass(
11381138
// Use a relative epsilon guard instead of exact equality: after the
11391139
// first pass removes all constrained items,
11401140
// totalFlexShrinkScaledFactors may be near-zero rather than exactly 0
1141-
// due to floating-point cancellation. Dividing by a near-zero value
1141+
// due to floating-point cancellation. Dividing by a near-zero value
11421142
// produces a gigantic childSize that overwhelms the min/max clamp.
1143+
// A positive total is always such a residual: with non-negative
1144+
// flexShrink a real total is never positive
1145+
// (https://github.com/react/yoga/issues/2038).
11431146
const float shrinkFactorMagnitude =
11441147
std::abs(flexLine.layout.totalFlexShrinkScaledFactors);
11451148
if (yoga::isDefined(flexLine.layout.totalFlexShrinkScaledFactors) &&
1146-
shrinkFactorMagnitude < 1e-6f) {
1149+
(shrinkFactorMagnitude < 1e-6f ||
1150+
flexLine.layout.totalFlexShrinkScaledFactors > 0)) {
11471151
childSize = childFlexBasis + flexShrinkScaledFactor;
11481152
} else {
11491153
childSize = childFlexBasis +

0 commit comments

Comments
 (0)