Skip to content

Commit 6e2edef

Browse files
adityasharatmeta-codesync[bot]
authored andcommitted
Memoise §4.5 min-content per node and axis per layout (#58833)
Summary: X-link: facebook/litho#1089 X-link: react/yoga#2036 Pull Request resolved: #58833 `computeMinContentMainSize` asks every in-flow child of a container for its min-content along both axes, and every flex container's `resolveFlexibleLength` recomputes the automatic minimum size of each item. Nothing was cached, so a leaf with a measure function N containers below a flex item was probed O(2^N) times per call, then again for every ancestor and every layout pass. A text leaf ten containers deep was probed 7,161 times in a single layout. This memoises min-content per node and axis for the current layout generation, following the existing `computedFlexBasisGeneration` pattern: - A leaf memoises its measured min-content, before its own padding and border. That value doesn't depend on the owner, so the measure callback runs at most once per axis per layout. - A container memoises its full min-content along with the `ownerWidth` it was computed against. Percentage padding and margins resolve against that width, and each ancestor probes with its own inner width, so a result is only reused for the same width. Layout results are unchanged; only the number of measure callbacks drops. The canonical Yoga copy and the React Native vendored copy are both updated, and the C++ API snapshots gain the three new `LayoutResults` fields. Changelog: [General][Fixed] - Avoid exponential min-content measurement during automatic minimum sizing of nested flex containers Reviewed By: javache Differential Revision: D123012133 fbshipit-source-id: 58eb649b260f5707cb36d4cd373e4f44788c16a2
1 parent c4356db commit 6e2edef

14 files changed

Lines changed: 103 additions & 26 deletions

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

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -850,15 +850,19 @@ static float computeFlexBasisForChildren(
850850
// Container-level recursion does no layout writes (no positions, no
851851
// alignment, no flex distribution); only the descendant leaf measure
852852
// callbacks observe state changes (the same ones a normal layout pass
853-
// would invoke). Roughly equivalent to FlexLayout's dedicated
854-
// `computeMinContentSize` cost: one measure call per leaf + linear walk
855-
// per container.
853+
// would invoke).
854+
//
855+
// Results are memoized per node and axis for the current layout generation
856+
// (see `LayoutResults::minContent`): each leaf's measure callback runs at
857+
// most once per axis per layout, and each container is walked once per
858+
// distinct `ownerWidth`.
856859
static float computeMinContentMainSize(
857860
yoga::Node* const node,
858861
const FlexDirection requestedAxis,
859862
const Direction ownerDirection,
860863
const float ownerWidth,
861-
const float ownerHeight) {
864+
const float ownerHeight,
865+
const uint32_t generationCount) {
862866
const bool wantRow = isRow(requestedAxis);
863867

864868
// 1. Static value wins for any node (leaf or container). Short-circuits
@@ -872,36 +876,48 @@ static float computeMinContentMainSize(
872876
return staticMin.unwrap();
873877
}
874878

879+
LayoutResults& layout = node->getLayout();
880+
const auto axisIndex = yoga::to_underlying(dimension(requestedAxis));
881+
875882
if (node->hasMeasureFunc()) {
876-
// 2. Dynamic min-content callback if set (for Primitives whose
877-
// min-content depends on state). Otherwise fall back to the regular
878-
// measure function with `AtMost 0`, which text measurers naturally
879-
// answer with longest-word width.
880-
const YGSize size = node->hasMinContentMeasureFunc()
881-
? node->measureMinContent(
882-
wantRow ? 0.0f : YGUndefined,
883-
wantRow ? MeasureMode::AtMost : MeasureMode::Undefined,
884-
wantRow ? YGUndefined : 0.0f,
885-
wantRow ? MeasureMode::Undefined : MeasureMode::AtMost)
886-
: node->measure(
887-
wantRow ? 0.0f : YGUndefined,
888-
wantRow ? MeasureMode::AtMost : MeasureMode::Undefined,
889-
wantRow ? YGUndefined : 0.0f,
890-
wantRow ? MeasureMode::Undefined : MeasureMode::AtMost);
883+
if (layout.minContentGeneration[axisIndex] != generationCount) {
884+
// 2. Dynamic min-content callback if set (for Primitives whose
885+
// min-content depends on state). Otherwise fall back to the regular
886+
// measure function with `AtMost 0`, which text measurers naturally
887+
// answer with longest-word width.
888+
const YGSize size = node->hasMinContentMeasureFunc()
889+
? node->measureMinContent(
890+
wantRow ? 0.0f : YGUndefined,
891+
wantRow ? MeasureMode::AtMost : MeasureMode::Undefined,
892+
wantRow ? YGUndefined : 0.0f,
893+
wantRow ? MeasureMode::Undefined : MeasureMode::AtMost)
894+
: node->measure(
895+
wantRow ? 0.0f : YGUndefined,
896+
wantRow ? MeasureMode::AtMost : MeasureMode::Undefined,
897+
wantRow ? YGUndefined : 0.0f,
898+
wantRow ? MeasureMode::Undefined : MeasureMode::AtMost);
899+
layout.minContent[axisIndex] = wantRow ? size.width : size.height;
900+
layout.minContentGeneration[axisIndex] = generationCount;
901+
}
891902
// Add the leaf's own padding and border, like the container branch below.
892903
const Direction leafDirection = node->resolveDirection(ownerDirection);
893904
const float paddingAndBorder =
894905
node->style().computeFlexStartPaddingAndBorder(
895906
requestedAxis, leafDirection, ownerWidth) +
896907
node->style().computeFlexEndPaddingAndBorder(
897908
requestedAxis, leafDirection, ownerWidth);
898-
return (wantRow ? size.width : size.height) + paddingAndBorder;
909+
return layout.minContent[axisIndex] + paddingAndBorder;
899910
}
900911

901912
if (node->getChildCount() == 0) {
902913
return 0.0f;
903914
}
904915

916+
if (layout.minContentGeneration[axisIndex] == generationCount &&
917+
yoga::inexactEquals(layout.minContentOwnerWidth[axisIndex], ownerWidth)) {
918+
return layout.minContent[axisIndex];
919+
}
920+
905921
const Direction direction = node->resolveDirection(ownerDirection);
906922
const FlexDirection nodeMainAxis =
907923
resolveDirection(node->style().flexDirection(), direction);
@@ -919,11 +935,21 @@ static float computeMinContentMainSize(
919935
}
920936

921937
float childMain = computeMinContentMainSize(
922-
child, nodeMainAxis, direction, ownerWidth, ownerHeight);
938+
child,
939+
nodeMainAxis,
940+
direction,
941+
ownerWidth,
942+
ownerHeight,
943+
generationCount);
923944
childMain += child->style().computeMarginForAxis(nodeMainAxis, ownerWidth);
924945

925946
float childCross = computeMinContentMainSize(
926-
child, nodeCrossAxis, direction, ownerWidth, ownerHeight);
947+
child,
948+
nodeCrossAxis,
949+
direction,
950+
ownerWidth,
951+
ownerHeight,
952+
generationCount);
927953
childCross +=
928954
child->style().computeMarginForAxis(nodeCrossAxis, ownerWidth);
929955

@@ -943,7 +969,11 @@ static float computeMinContentMainSize(
943969
const bool nodeMainIsRow = isRow(nodeMainAxis);
944970
const float widthMin = nodeMainIsRow ? mainTotal : crossMax;
945971
const float heightMin = nodeMainIsRow ? crossMax : mainTotal;
946-
return wantRow ? widthMin : heightMin;
972+
const float minContent = wantRow ? widthMin : heightMin;
973+
layout.minContent[axisIndex] = minContent;
974+
layout.minContentOwnerWidth[axisIndex] = ownerWidth;
975+
layout.minContentGeneration[axisIndex] = generationCount;
976+
return minContent;
947977
}
948978

949979
// Computes the CSS Flexbox §4.5 automatic minimum main-axis size for
@@ -962,7 +992,8 @@ static FloatOptional computeAutoMinMainSize(
962992
const Direction direction,
963993
const float ownerMainAxisSize,
964994
const float ownerWidth,
965-
const float ownerHeight) {
995+
const float ownerHeight,
996+
const uint32_t generationCount) {
966997
if (child->hasErrata(Errata::MinSizeUndefinedInsteadOfAuto)) {
967998
return FloatOptional{};
968999
}
@@ -1007,7 +1038,7 @@ static FloatOptional computeAutoMinMainSize(
10071038

10081039
// Content size suggestion: probe via min-content recursion.
10091040
const FloatOptional contentMain = FloatOptional{computeMinContentMainSize(
1010-
child, mainAxis, direction, ownerWidth, ownerHeight)};
1041+
child, mainAxis, direction, ownerWidth, ownerHeight, generationCount)};
10111042

10121043
// Combine per §4.5: floor = min(content, specified) when specified is
10131044
// definite; otherwise floor = min(content, transferred) when transferred
@@ -1432,7 +1463,8 @@ static void resolveFlexibleLength(
14321463
direction,
14331464
mainAxisOwnerSize,
14341465
availableInnerWidth,
1435-
availableInnerHeight);
1466+
availableInnerHeight,
1467+
generationCount);
14361468
}
14371469
} else {
14381470
for (auto currentLineChild : flexLine.itemsInFlow) {

‎packages/react-native/ReactCommon/yoga/yoga/node/LayoutResults.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ struct LayoutResults {
3535
// means "no auto-min applies."
3636
FloatOptional computedAutoMinMainSize = {};
3737

38+
// Memoized min-content sizes for §4.5, indexed by dimension and valid only
39+
// during layout generation `minContentGeneration`. A leaf stores its measured
40+
// min-content without padding and border, which doesn't depend on the owner.
41+
// A container stores its full min-content, which depends on
42+
// `minContentOwnerWidth` through percentage padding and margins.
43+
std::array<uint32_t, 2> minContentGeneration = {};
44+
std::array<float, 2> minContentOwnerWidth = {{YGUndefined, YGUndefined}};
45+
std::array<float, 2> minContent = {{YGUndefined, YGUndefined}};
46+
3847
// Instead of recomputing the entire layout every single time, we cache some
3948
// information to break early when nothing changed
4049
uint32_t generationCount = 0;

‎scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7197,6 +7197,9 @@ struct facebook::yoga::LayoutResults {
71977197
public float rawDimension(facebook::yoga::Dimension axis) const;
71987198
public static constexpr int32_t MaxCachedMeasurements;
71997199
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
7200+
public std::array<float, 2> minContent;
7201+
public std::array<float, 2> minContentOwnerWidth;
7202+
public std::array<uint32_t, 2> minContentGeneration;
72007203
public uint32_t computedFlexBasisGeneration;
72017204
public uint32_t configVersion;
72027205
public uint32_t generationCount;

‎scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11627,6 +11627,9 @@ struct facebook::yoga::LayoutResults {
1162711627
public float rawDimension(facebook::yoga::Dimension axis) const;
1162811628
public static constexpr int32_t MaxCachedMeasurements;
1162911629
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
11630+
public std::array<float, 2> minContent;
11631+
public std::array<float, 2> minContentOwnerWidth;
11632+
public std::array<uint32_t, 2> minContentGeneration;
1163011633
public uint32_t computedFlexBasisGeneration;
1163111634
public uint32_t configVersion;
1163211635
public uint32_t generationCount;

‎scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7046,6 +7046,9 @@ struct facebook::yoga::LayoutResults {
70467046
public float rawDimension(facebook::yoga::Dimension axis) const;
70477047
public static constexpr int32_t MaxCachedMeasurements;
70487048
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
7049+
public std::array<float, 2> minContent;
7050+
public std::array<float, 2> minContentOwnerWidth;
7051+
public std::array<uint32_t, 2> minContentGeneration;
70497052
public uint32_t computedFlexBasisGeneration;
70507053
public uint32_t configVersion;
70517054
public uint32_t generationCount;

‎scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7046,6 +7046,9 @@ struct facebook::yoga::LayoutResults {
70467046
public float rawDimension(facebook::yoga::Dimension axis) const;
70477047
public static constexpr int32_t MaxCachedMeasurements;
70487048
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
7049+
public std::array<float, 2> minContent;
7050+
public std::array<float, 2> minContentOwnerWidth;
7051+
public std::array<uint32_t, 2> minContentGeneration;
70497052
public uint32_t computedFlexBasisGeneration;
70507053
public uint32_t configVersion;
70517054
public uint32_t generationCount;

‎scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8981,6 +8981,9 @@ struct facebook::yoga::LayoutResults {
89818981
public float rawDimension(facebook::yoga::Dimension axis) const;
89828982
public static constexpr int32_t MaxCachedMeasurements;
89838983
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
8984+
public std::array<float, 2> minContent;
8985+
public std::array<float, 2> minContentOwnerWidth;
8986+
public std::array<uint32_t, 2> minContentGeneration;
89848987
public uint32_t computedFlexBasisGeneration;
89858988
public uint32_t configVersion;
89868989
public uint32_t generationCount;

‎scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13446,6 +13446,9 @@ struct facebook::yoga::LayoutResults {
1344613446
public float rawDimension(facebook::yoga::Dimension axis) const;
1344713447
public static constexpr int32_t MaxCachedMeasurements;
1344813448
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
13449+
public std::array<float, 2> minContent;
13450+
public std::array<float, 2> minContentOwnerWidth;
13451+
public std::array<uint32_t, 2> minContentGeneration;
1344913452
public uint32_t computedFlexBasisGeneration;
1345013453
public uint32_t configVersion;
1345113454
public uint32_t generationCount;

‎scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8832,6 +8832,9 @@ struct facebook::yoga::LayoutResults {
88328832
public float rawDimension(facebook::yoga::Dimension axis) const;
88338833
public static constexpr int32_t MaxCachedMeasurements;
88348834
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
8835+
public std::array<float, 2> minContent;
8836+
public std::array<float, 2> minContentOwnerWidth;
8837+
public std::array<uint32_t, 2> minContentGeneration;
88358838
public uint32_t computedFlexBasisGeneration;
88368839
public uint32_t configVersion;
88378840
public uint32_t generationCount;

‎scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8840,6 +8840,9 @@ struct facebook::yoga::LayoutResults {
88408840
public float rawDimension(facebook::yoga::Dimension axis) const;
88418841
public static constexpr int32_t MaxCachedMeasurements;
88428842
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
8843+
public std::array<float, 2> minContent;
8844+
public std::array<float, 2> minContentOwnerWidth;
8845+
public std::array<uint32_t, 2> minContentGeneration;
88438846
public uint32_t computedFlexBasisGeneration;
88448847
public uint32_t configVersion;
88458848
public uint32_t generationCount;

0 commit comments

Comments
 (0)