Skip to content

Commit c9d9379

Browse files
adityasharatmeta-codesync[bot]
authored andcommitted
Memoise §4.5 min-content per node and axis per layout (#58833)
Summary: 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 Differential Revision: D123012133
1 parent 88a8bd2 commit c9d9379

11 files changed

Lines changed: 94 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
@@ -7262,6 +7262,9 @@ struct facebook::yoga::LayoutResults {
72627262
public float rawDimension(facebook::yoga::Dimension axis) const;
72637263
public static constexpr int32_t MaxCachedMeasurements;
72647264
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
7265+
public std::array<float, 2> minContent;
7266+
public std::array<float, 2> minContentOwnerWidth;
7267+
public std::array<uint32_t, 2> minContentGeneration;
72657268
public uint32_t computedFlexBasisGeneration;
72667269
public uint32_t configVersion;
72677270
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
@@ -7111,6 +7111,9 @@ struct facebook::yoga::LayoutResults {
71117111
public float rawDimension(facebook::yoga::Dimension axis) const;
71127112
public static constexpr int32_t MaxCachedMeasurements;
71137113
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
7114+
public std::array<float, 2> minContent;
7115+
public std::array<float, 2> minContentOwnerWidth;
7116+
public std::array<uint32_t, 2> minContentGeneration;
71147117
public uint32_t computedFlexBasisGeneration;
71157118
public uint32_t configVersion;
71167119
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
@@ -7111,6 +7111,9 @@ struct facebook::yoga::LayoutResults {
71117111
public float rawDimension(facebook::yoga::Dimension axis) const;
71127112
public static constexpr int32_t MaxCachedMeasurements;
71137113
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
7114+
public std::array<float, 2> minContent;
7115+
public std::array<float, 2> minContentOwnerWidth;
7116+
public std::array<uint32_t, 2> minContentGeneration;
71147117
public uint32_t computedFlexBasisGeneration;
71157118
public uint32_t configVersion;
71167119
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
@@ -9089,6 +9089,9 @@ struct facebook::yoga::LayoutResults {
90899089
public float rawDimension(facebook::yoga::Dimension axis) const;
90909090
public static constexpr int32_t MaxCachedMeasurements;
90919091
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
9092+
public std::array<float, 2> minContent;
9093+
public std::array<float, 2> minContentOwnerWidth;
9094+
public std::array<uint32_t, 2> minContentGeneration;
90929095
public uint32_t computedFlexBasisGeneration;
90939096
public uint32_t configVersion;
90949097
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
@@ -8940,6 +8940,9 @@ struct facebook::yoga::LayoutResults {
89408940
public float rawDimension(facebook::yoga::Dimension axis) const;
89418941
public static constexpr int32_t MaxCachedMeasurements;
89428942
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
8943+
public std::array<float, 2> minContent;
8944+
public std::array<float, 2> minContentOwnerWidth;
8945+
public std::array<uint32_t, 2> minContentGeneration;
89438946
public uint32_t computedFlexBasisGeneration;
89448947
public uint32_t configVersion;
89458948
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
@@ -8948,6 +8948,9 @@ struct facebook::yoga::LayoutResults {
89488948
public float rawDimension(facebook::yoga::Dimension axis) const;
89498949
public static constexpr int32_t MaxCachedMeasurements;
89508950
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
8951+
public std::array<float, 2> minContent;
8952+
public std::array<float, 2> minContentOwnerWidth;
8953+
public std::array<uint32_t, 2> minContentGeneration;
89518954
public uint32_t computedFlexBasisGeneration;
89528955
public uint32_t configVersion;
89538956
public uint32_t generationCount;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4492,6 +4492,9 @@ struct facebook::yoga::LayoutResults {
44924492
public float rawDimension(facebook::yoga::Dimension axis) const;
44934493
public static constexpr int32_t MaxCachedMeasurements;
44944494
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
4495+
public std::array<float, 2> minContent;
4496+
public std::array<float, 2> minContentOwnerWidth;
4497+
public std::array<uint32_t, 2> minContentGeneration;
44954498
public uint32_t computedFlexBasisGeneration;
44964499
public uint32_t configVersion;
44974500
public uint32_t generationCount;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4485,6 +4485,9 @@ struct facebook::yoga::LayoutResults {
44854485
public float rawDimension(facebook::yoga::Dimension axis) const;
44864486
public static constexpr int32_t MaxCachedMeasurements;
44874487
public std::array<facebook::yoga::CachedMeasurement, facebook::yoga::LayoutResults::MaxCachedMeasurements> cachedMeasurements;
4488+
public std::array<float, 2> minContent;
4489+
public std::array<float, 2> minContentOwnerWidth;
4490+
public std::array<uint32_t, 2> minContentGeneration;
44884491
public uint32_t computedFlexBasisGeneration;
44894492
public uint32_t configVersion;
44904493
public uint32_t generationCount;

0 commit comments

Comments
 (0)