From 38ca607428023d56ffb467456769094fc1dc3c96 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 9 Jun 2026 09:11:17 -0700 Subject: [PATCH 1/6] Simplify `BaseTextProps::appendTextAttributesProps` (#57150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: `BaseTextProps::appendTextAttributesProps` builds a `folly::dynamic` diff of ~29 text attributes through a long flat sequence of per-attribute `if (changed) result[key] = ...` blocks, many with an embedded `has_value() ? convert(...) : nullptr` ternary. That gave the function a cyclomatic complexity (CCN) of 47, well into the "complex, hard to test" range. This is a pure, behavior-preserving refactor. The four recurring compare-and-assign shapes — plain `!=`, `floatEquality`, optional-with-converter, and color dereference — are pulled into small file-local `static` helpers, and the function body becomes a flat list of helper calls in the exact same order, with the exact same keys, conversions, and values. The serialized output (including `folly::dynamic` insertion order) is identical. Only the `.cpp` is touched; there are no public header or API changes. Changelog: [Internal] Differential Revision: D108027815 --- .../components/text/BaseTextProps.cpp | 376 ++++++++++-------- 1 file changed, 208 insertions(+), 168 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp index e2ab6780f96..8c3562cd79b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp @@ -354,185 +354,225 @@ static folly::dynamic toDynamic(const Size& size) { return sizeResult; } -void BaseTextProps::appendTextAttributesProps( +// Behavior-preserving helpers extracted from appendTextAttributesProps below to +// keep its cyclomatic complexity low. Each mirrors one of the recurring +// compare-and-assign shapes used per text attribute, so the serialized output +// (keys, values, and insertion order) is identical to the open-coded version. +template +static void appendIfChanged( folly::dynamic& result, - const BaseTextProps* oldProps) const { - if (textAttributes.foregroundColor != - oldProps->textAttributes.foregroundColor) { - result["color"] = *textAttributes.foregroundColor; - } - - if (textAttributes.fontFamily != oldProps->textAttributes.fontFamily) { - result["fontFamily"] = textAttributes.fontFamily; - } - - if (!floatEquality( - textAttributes.fontSize, oldProps->textAttributes.fontSize)) { - result["fontSize"] = textAttributes.fontSize; - } - - if (!floatEquality( - textAttributes.fontSizeMultiplier, - oldProps->textAttributes.fontSizeMultiplier)) { - result["fontSizeMultiplier"] = textAttributes.fontSizeMultiplier; - } - - if (textAttributes.fontWeight != oldProps->textAttributes.fontWeight) { - result["fontWeight"] = textAttributes.fontWeight.has_value() - ? toString(textAttributes.fontWeight.value()) - : folly::dynamic(nullptr); - } - - if (textAttributes.fontStyle != oldProps->textAttributes.fontStyle) { - result["fontStyle"] = textAttributes.fontStyle.has_value() - ? toString(textAttributes.fontStyle.value()) - : folly::dynamic(nullptr); - } - - if (textAttributes.fontVariant != oldProps->textAttributes.fontVariant) { - result["fontVariant"] = textAttributes.fontVariant.has_value() - ? toString(textAttributes.fontVariant.value()) - : folly::dynamic(nullptr); - } - - if (textAttributes.allowFontScaling != - oldProps->textAttributes.allowFontScaling) { - result["allowFontScaling"] = textAttributes.allowFontScaling.has_value() - ? textAttributes.allowFontScaling.value() - : folly::dynamic(nullptr); - } - - if (!floatEquality( - textAttributes.maxFontSizeMultiplier, - oldProps->textAttributes.maxFontSizeMultiplier)) { - result["maxFontSizeMultiplier"] = textAttributes.maxFontSizeMultiplier; - } - - if (textAttributes.dynamicTypeRamp != - oldProps->textAttributes.dynamicTypeRamp) { - result["dynamicTypeRamp"] = textAttributes.dynamicTypeRamp.has_value() - ? toString(textAttributes.dynamicTypeRamp.value()) - : folly::dynamic(nullptr); - } - - if (!floatEquality( - textAttributes.letterSpacing, - oldProps->textAttributes.letterSpacing)) { - result["letterSpacing"] = textAttributes.letterSpacing; - } - - if (textAttributes.textTransform != oldProps->textAttributes.textTransform) { - result["textTransform"] = textAttributes.textTransform.has_value() - ? toString(textAttributes.textTransform.value()) - : folly::dynamic(nullptr); - } - - if (!floatEquality( - textAttributes.lineHeight, oldProps->textAttributes.lineHeight)) { - result["lineHeight"] = textAttributes.lineHeight; - } - - if (textAttributes.alignment != oldProps->textAttributes.alignment) { - result["textAlign"] = textAttributes.alignment.has_value() - ? toString(textAttributes.alignment.value()) - : folly::dynamic(nullptr); - } - - if (textAttributes.baseWritingDirection != - oldProps->textAttributes.baseWritingDirection) { - result["baseWritingDirection"] = - textAttributes.baseWritingDirection.has_value() - ? toString(textAttributes.baseWritingDirection.value()) - : folly::dynamic(nullptr); - } - - if (textAttributes.lineBreakStrategy != - oldProps->textAttributes.lineBreakStrategy) { - result["lineBreakStrategyIOS"] = - textAttributes.lineBreakStrategy.has_value() - ? toString(textAttributes.lineBreakStrategy.value()) - : folly::dynamic(nullptr); - } - - if (textAttributes.lineBreakMode != oldProps->textAttributes.lineBreakMode) { - result["lineBreakModeIOS"] = textAttributes.lineBreakMode.has_value() - ? toString(textAttributes.lineBreakMode.value()) - : folly::dynamic(nullptr); - } - - if (textAttributes.textDecorationColor != - oldProps->textAttributes.textDecorationColor) { - result["textDecorationColor"] = *textAttributes.textDecorationColor; - } - - if (textAttributes.textDecorationLineType != - oldProps->textAttributes.textDecorationLineType) { - result["textDecorationLine"] = - textAttributes.textDecorationLineType.has_value() - ? toString(textAttributes.textDecorationLineType.value()) - : folly::dynamic(nullptr); - } - - if (textAttributes.textDecorationStyle != - oldProps->textAttributes.textDecorationStyle) { - result["textDecorationStyle"] = - textAttributes.textDecorationStyle.has_value() - ? toString(textAttributes.textDecorationStyle.value()) - : folly::dynamic(nullptr); - } - - if (textAttributes.textShadowOffset != - oldProps->textAttributes.textShadowOffset) { - result["textShadowOffset"] = textAttributes.textShadowOffset.has_value() - ? toDynamic(textAttributes.textShadowOffset.value()) - : folly::dynamic(nullptr); - } - - if (!floatEquality( - textAttributes.textShadowRadius, - oldProps->textAttributes.textShadowRadius)) { - result["textShadowRadius"] = textAttributes.textShadowRadius; - } - - if (textAttributes.textShadowColor != - oldProps->textAttributes.textShadowColor) { - result["textShadowColor"] = *textAttributes.textShadowColor; - } - - if (textAttributes.isHighlighted != oldProps->textAttributes.isHighlighted) { - result["isHighlighted"] = textAttributes.isHighlighted.has_value() - ? textAttributes.isHighlighted.value() - : folly::dynamic(nullptr); + const char* propName, + const T& newValue, + const T& oldValue) { + if (newValue != oldValue) { + result[propName] = newValue; } +} - if (textAttributes.isPressable != oldProps->textAttributes.isPressable) { - result["isPressable"] = textAttributes.isPressable.has_value() - ? textAttributes.isPressable.value() - : folly::dynamic(nullptr); +template +static void appendDerefIfChanged( + folly::dynamic& result, + const char* propName, + const T& newValue, + const T& oldValue) { + if (newValue != oldValue) { + result[propName] = *newValue; } +} - if (textAttributes.accessibilityRole != - oldProps->textAttributes.accessibilityRole) { - result["accessibilityRole"] = textAttributes.accessibilityRole.has_value() - ? toString(textAttributes.accessibilityRole.value()) - : folly::dynamic(nullptr); +static void appendFloatIfChanged( + folly::dynamic& result, + const char* propName, + Float newValue, + Float oldValue) { + if (!floatEquality(newValue, oldValue)) { + result[propName] = newValue; } +} - if (textAttributes.role != oldProps->textAttributes.role) { - result["role"] = textAttributes.role.has_value() - ? toString(textAttributes.role.value()) +template +static void appendOptionalIfChanged( + folly::dynamic& result, + const char* propName, + const std::optional& newValue, + const std::optional& oldValue, + Convert&& convert) { + if (newValue != oldValue) { + result[propName] = newValue.has_value() + ? folly::dynamic(convert(newValue.value())) : folly::dynamic(nullptr); } +} - if (!floatEquality( - textAttributes.opacity, oldProps->textAttributes.opacity)) { - result["opacity"] = textAttributes.opacity; - } +void BaseTextProps::appendTextAttributesProps( + folly::dynamic& result, + const BaseTextProps* oldProps) const { + auto asString = [](const auto& value) { return toString(value); }; + auto asIs = [](const auto& value) { return value; }; + auto asDynamic = [](const auto& value) { return toDynamic(value); }; - if (textAttributes.backgroundColor != - oldProps->textAttributes.backgroundColor) { - result["backgroundColor"] = *textAttributes.backgroundColor; - } + appendDerefIfChanged( + result, + "color", + textAttributes.foregroundColor, + oldProps->textAttributes.foregroundColor); + appendIfChanged( + result, + "fontFamily", + textAttributes.fontFamily, + oldProps->textAttributes.fontFamily); + appendFloatIfChanged( + result, + "fontSize", + textAttributes.fontSize, + oldProps->textAttributes.fontSize); + appendFloatIfChanged( + result, + "fontSizeMultiplier", + textAttributes.fontSizeMultiplier, + oldProps->textAttributes.fontSizeMultiplier); + appendOptionalIfChanged( + result, + "fontWeight", + textAttributes.fontWeight, + oldProps->textAttributes.fontWeight, + asString); + appendOptionalIfChanged( + result, + "fontStyle", + textAttributes.fontStyle, + oldProps->textAttributes.fontStyle, + asString); + appendOptionalIfChanged( + result, + "fontVariant", + textAttributes.fontVariant, + oldProps->textAttributes.fontVariant, + asString); + appendOptionalIfChanged( + result, + "allowFontScaling", + textAttributes.allowFontScaling, + oldProps->textAttributes.allowFontScaling, + asIs); + appendFloatIfChanged( + result, + "maxFontSizeMultiplier", + textAttributes.maxFontSizeMultiplier, + oldProps->textAttributes.maxFontSizeMultiplier); + appendOptionalIfChanged( + result, + "dynamicTypeRamp", + textAttributes.dynamicTypeRamp, + oldProps->textAttributes.dynamicTypeRamp, + asString); + appendFloatIfChanged( + result, + "letterSpacing", + textAttributes.letterSpacing, + oldProps->textAttributes.letterSpacing); + appendOptionalIfChanged( + result, + "textTransform", + textAttributes.textTransform, + oldProps->textAttributes.textTransform, + asString); + appendFloatIfChanged( + result, + "lineHeight", + textAttributes.lineHeight, + oldProps->textAttributes.lineHeight); + appendOptionalIfChanged( + result, + "textAlign", + textAttributes.alignment, + oldProps->textAttributes.alignment, + asString); + appendOptionalIfChanged( + result, + "baseWritingDirection", + textAttributes.baseWritingDirection, + oldProps->textAttributes.baseWritingDirection, + asString); + appendOptionalIfChanged( + result, + "lineBreakStrategyIOS", + textAttributes.lineBreakStrategy, + oldProps->textAttributes.lineBreakStrategy, + asString); + appendOptionalIfChanged( + result, + "lineBreakModeIOS", + textAttributes.lineBreakMode, + oldProps->textAttributes.lineBreakMode, + asString); + appendDerefIfChanged( + result, + "textDecorationColor", + textAttributes.textDecorationColor, + oldProps->textAttributes.textDecorationColor); + appendOptionalIfChanged( + result, + "textDecorationLine", + textAttributes.textDecorationLineType, + oldProps->textAttributes.textDecorationLineType, + asString); + appendOptionalIfChanged( + result, + "textDecorationStyle", + textAttributes.textDecorationStyle, + oldProps->textAttributes.textDecorationStyle, + asString); + appendOptionalIfChanged( + result, + "textShadowOffset", + textAttributes.textShadowOffset, + oldProps->textAttributes.textShadowOffset, + asDynamic); + appendFloatIfChanged( + result, + "textShadowRadius", + textAttributes.textShadowRadius, + oldProps->textAttributes.textShadowRadius); + appendDerefIfChanged( + result, + "textShadowColor", + textAttributes.textShadowColor, + oldProps->textAttributes.textShadowColor); + appendOptionalIfChanged( + result, + "isHighlighted", + textAttributes.isHighlighted, + oldProps->textAttributes.isHighlighted, + asIs); + appendOptionalIfChanged( + result, + "isPressable", + textAttributes.isPressable, + oldProps->textAttributes.isPressable, + asIs); + appendOptionalIfChanged( + result, + "accessibilityRole", + textAttributes.accessibilityRole, + oldProps->textAttributes.accessibilityRole, + asString); + appendOptionalIfChanged( + result, + "role", + textAttributes.role, + oldProps->textAttributes.role, + asString); + appendFloatIfChanged( + result, + "opacity", + textAttributes.opacity, + oldProps->textAttributes.opacity); + appendDerefIfChanged( + result, + "backgroundColor", + textAttributes.backgroundColor, + oldProps->textAttributes.backgroundColor); } #endif From 5663ef6bc38baff5880b255c92adef251736f2d3 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 9 Jun 2026 09:11:17 -0700 Subject: [PATCH 2/6] Simplify `AndroidTextInputProps::getDiffProps` (#57151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: `AndroidTextInputProps::getDiffProps` computes a `folly::dynamic` prop diff through ~57 sequential per-prop `if (field != oldProps->field) result[key] = ...` blocks (plus a nested `has_value()` branch for `textAlignVertical` and an optional ternary for `acceptDragAndDropTypes`), giving the function a cyclomatic complexity (CCN) of 63 — past the landing threshold. This is a pure, behavior-preserving refactor. The recurring compare-and-assign shapes — plain `!=`, color dereference, `floatEquality`, direct `toString`/`toDynamic` conversion, and optional-with-converter — are pulled into small file-local `static` helpers, and the body becomes a flat list of helper calls in the exact same order, with the same keys, comparisons (`!=` vs `floatEquality`), conversions, and values. The serialized output (including `folly::dynamic` insertion order, so the duplicate `numberOfLines`/`includeFontPadding` keys still resolve identically) is unchanged. Only the `.cpp` is touched; there are no public header or API changes. Changelog: [Internal] Differential Revision: D108027818 --- .../AndroidTextInputProps.cpp | 502 +++++++++--------- 1 file changed, 251 insertions(+), 251 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp index 5b88b938f89..e78ea01e2bc 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp @@ -361,274 +361,274 @@ ComponentName AndroidTextInputProps::getDiffPropsImplementationTarget() const { return "TextInput"; } -folly::dynamic AndroidTextInputProps::getDiffProps( - const Props* prevProps) const { - static const auto defaultProps = AndroidTextInputProps(); - - const AndroidTextInputProps* oldProps = prevProps == nullptr - ? &defaultProps - : static_cast(prevProps); - - folly::dynamic result = ViewProps::getDiffProps(oldProps); - - // Base text input paragraph props - if (paragraphAttributes.maximumNumberOfLines != - oldProps->paragraphAttributes.maximumNumberOfLines) { - result["numberOfLines"] = paragraphAttributes.maximumNumberOfLines; - } - - if (paragraphAttributes.ellipsizeMode != - oldProps->paragraphAttributes.ellipsizeMode) { - result["ellipsizeMode"] = toString(paragraphAttributes.ellipsizeMode); - } - - if (paragraphAttributes.textBreakStrategy != - oldProps->paragraphAttributes.textBreakStrategy) { - result["textBreakStrategy"] = - toString(paragraphAttributes.textBreakStrategy); - } - - if (paragraphAttributes.adjustsFontSizeToFit != - oldProps->paragraphAttributes.adjustsFontSizeToFit) { - result["adjustsFontSizeToFit"] = paragraphAttributes.adjustsFontSizeToFit; - } - - if (!floatEquality( - paragraphAttributes.minimumFontSize, - oldProps->paragraphAttributes.minimumFontSize)) { - result["minimumFontSize"] = paragraphAttributes.minimumFontSize; - } - - if (!floatEquality( - paragraphAttributes.maximumFontSize, - oldProps->paragraphAttributes.maximumFontSize)) { - result["maximumFontSize"] = paragraphAttributes.maximumFontSize; - } - - if (paragraphAttributes.includeFontPadding != - oldProps->paragraphAttributes.includeFontPadding) { - result["includeFontPadding"] = paragraphAttributes.includeFontPadding; - } - - if (paragraphAttributes.android_hyphenationFrequency != - oldProps->paragraphAttributes.android_hyphenationFrequency) { - result["android_hyphenationFrequency"] = - toString(paragraphAttributes.android_hyphenationFrequency); - } - - if (paragraphAttributes.textAlignVertical != - oldProps->paragraphAttributes.textAlignVertical) { - if (!paragraphAttributes.textAlignVertical.has_value()) { - result["textAlignVertical"] = nullptr; - } else { - result["textAlignVertical"] = - toString(*paragraphAttributes.textAlignVertical); - } - } - - // Base text input props - if (defaultValue != oldProps->defaultValue) { - result["defaultValue"] = defaultValue; - } - - if (placeholder != oldProps->placeholder) { - result["placeholder"] = placeholder; - } - - if (placeholderTextColor != oldProps->placeholderTextColor) { - result["placeholderTextColor"] = *placeholderTextColor; - } - - if (cursorColor != oldProps->cursorColor) { - result["cursorColor"] = *cursorColor; - } - - if (selectionColor != oldProps->selectionColor) { - result["selectionColor"] = *selectionColor; - } - - if (selectionHandleColor != oldProps->selectionHandleColor) { - result["selectionHandleColor"] = *selectionHandleColor; - } - - if (underlineColorAndroid != oldProps->underlineColorAndroid) { - result["underlineColorAndroid"] = *underlineColorAndroid; - } - - if (maxLength != oldProps->maxLength) { - result["maxLength"] = maxLength; +// Behavior-preserving helpers extracted from getDiffProps below to keep its +// cyclomatic complexity low. Each mirrors one of the recurring +// compare-and-assign shapes used per prop, so the serialized output (keys, +// values, conversions, and insertion order) is identical to the open-coded +// version. +template +static void appendIfChanged( + folly::dynamic& result, + const char* propName, + const T& newValue, + const T& oldValue) { + if (newValue != oldValue) { + result[propName] = newValue; } +} - if (text != oldProps->text) { - result["text"] = text; +template +static void appendDerefIfChanged( + folly::dynamic& result, + const char* propName, + const T& newValue, + const T& oldValue) { + if (newValue != oldValue) { + result[propName] = *newValue; } +} - if (mostRecentEventCount != oldProps->mostRecentEventCount) { - result["mostRecentEventCount"] = mostRecentEventCount; +static void appendFloatIfChanged( + folly::dynamic& result, + const char* propName, + Float newValue, + Float oldValue) { + if (!floatEquality(newValue, oldValue)) { + result[propName] = newValue; } +} - if (autoFocus != oldProps->autoFocus) { - result["autoFocus"] = autoFocus; +template +static void appendConvertedIfChanged( + folly::dynamic& result, + const char* propName, + const T& newValue, + const T& oldValue, + Convert&& convert) { + if (newValue != oldValue) { + result[propName] = convert(newValue); } +} - if (autoCapitalize != oldProps->autoCapitalize) { - result["autoCapitalize"] = autoCapitalize; +template +static void appendOptionalIfChanged( + folly::dynamic& result, + const char* propName, + const std::optional& newValue, + const std::optional& oldValue, + Convert&& convert) { + if (newValue != oldValue) { + result[propName] = newValue.has_value() + ? folly::dynamic(convert(newValue.value())) + : folly::dynamic(nullptr); } +} - if (editable != oldProps->editable) { - result["editable"] = editable; - } +folly::dynamic AndroidTextInputProps::getDiffProps( + const Props* prevProps) const { + static const auto defaultProps = AndroidTextInputProps(); - if (readOnly != oldProps->readOnly) { - result["readOnly"] = readOnly; - } + const AndroidTextInputProps* oldProps = prevProps == nullptr + ? &defaultProps + : static_cast(prevProps); - if (submitBehavior != oldProps->submitBehavior) { - result["submitBehavior"] = toDynamic(submitBehavior); - } + folly::dynamic result = ViewProps::getDiffProps(oldProps); - if (multiline != oldProps->multiline) { - result["multiline"] = multiline; - } + auto asString = [](const auto& value) { return toString(value); }; + auto asDynamic = [](const auto& value) { return toDynamic(value); }; - if (disableKeyboardShortcuts != oldProps->disableKeyboardShortcuts) { - result["disableKeyboardShortcuts"] = disableKeyboardShortcuts; - } + // Base text input paragraph props + appendIfChanged( + result, + "numberOfLines", + paragraphAttributes.maximumNumberOfLines, + oldProps->paragraphAttributes.maximumNumberOfLines); + appendConvertedIfChanged( + result, + "ellipsizeMode", + paragraphAttributes.ellipsizeMode, + oldProps->paragraphAttributes.ellipsizeMode, + asString); + appendConvertedIfChanged( + result, + "textBreakStrategy", + paragraphAttributes.textBreakStrategy, + oldProps->paragraphAttributes.textBreakStrategy, + asString); + appendIfChanged( + result, + "adjustsFontSizeToFit", + paragraphAttributes.adjustsFontSizeToFit, + oldProps->paragraphAttributes.adjustsFontSizeToFit); + appendFloatIfChanged( + result, + "minimumFontSize", + paragraphAttributes.minimumFontSize, + oldProps->paragraphAttributes.minimumFontSize); + appendFloatIfChanged( + result, + "maximumFontSize", + paragraphAttributes.maximumFontSize, + oldProps->paragraphAttributes.maximumFontSize); + appendIfChanged( + result, + "includeFontPadding", + paragraphAttributes.includeFontPadding, + oldProps->paragraphAttributes.includeFontPadding); + appendConvertedIfChanged( + result, + "android_hyphenationFrequency", + paragraphAttributes.android_hyphenationFrequency, + oldProps->paragraphAttributes.android_hyphenationFrequency, + asString); + appendOptionalIfChanged( + result, + "textAlignVertical", + paragraphAttributes.textAlignVertical, + oldProps->paragraphAttributes.textAlignVertical, + asString); - if (acceptDragAndDropTypes != oldProps->acceptDragAndDropTypes) { - result["acceptDragAndDropTypes"] = acceptDragAndDropTypes.has_value() - ? toDynamic(acceptDragAndDropTypes.value()) - : nullptr; - } + // Base text input props + appendIfChanged(result, "defaultValue", defaultValue, oldProps->defaultValue); + appendIfChanged(result, "placeholder", placeholder, oldProps->placeholder); + appendDerefIfChanged( + result, + "placeholderTextColor", + placeholderTextColor, + oldProps->placeholderTextColor); + appendDerefIfChanged( + result, "cursorColor", cursorColor, oldProps->cursorColor); + appendDerefIfChanged( + result, "selectionColor", selectionColor, oldProps->selectionColor); + appendDerefIfChanged( + result, + "selectionHandleColor", + selectionHandleColor, + oldProps->selectionHandleColor); + appendDerefIfChanged( + result, + "underlineColorAndroid", + underlineColorAndroid, + oldProps->underlineColorAndroid); + appendIfChanged(result, "maxLength", maxLength, oldProps->maxLength); + appendIfChanged(result, "text", text, oldProps->text); + appendIfChanged( + result, + "mostRecentEventCount", + mostRecentEventCount, + oldProps->mostRecentEventCount); + appendIfChanged(result, "autoFocus", autoFocus, oldProps->autoFocus); + appendIfChanged( + result, "autoCapitalize", autoCapitalize, oldProps->autoCapitalize); + appendIfChanged(result, "editable", editable, oldProps->editable); + appendIfChanged(result, "readOnly", readOnly, oldProps->readOnly); + appendConvertedIfChanged( + result, + "submitBehavior", + submitBehavior, + oldProps->submitBehavior, + asDynamic); + appendIfChanged(result, "multiline", multiline, oldProps->multiline); + appendIfChanged( + result, + "disableKeyboardShortcuts", + disableKeyboardShortcuts, + oldProps->disableKeyboardShortcuts); + appendOptionalIfChanged( + result, + "acceptDragAndDropTypes", + acceptDragAndDropTypes, + oldProps->acceptDragAndDropTypes, + asDynamic); // Android text input props - if (autoComplete != oldProps->autoComplete) { - result["autoComplete"] = autoComplete; - } - - if (returnKeyLabel != oldProps->returnKeyLabel) { - result["returnKeyLabel"] = returnKeyLabel; - } - - if (numberOfLines != oldProps->numberOfLines) { - result["numberOfLines"] = numberOfLines; - } - - if (disableFullscreenUI != oldProps->disableFullscreenUI) { - result["disableFullscreenUI"] = disableFullscreenUI; - } - - if (textBreakStrategy != oldProps->textBreakStrategy) { - result["textBreakStrategy"] = textBreakStrategy; - } - - if (inlineImageLeft != oldProps->inlineImageLeft) { - result["inlineImageLeft"] = inlineImageLeft; - } - - if (inlineImagePadding != oldProps->inlineImagePadding) { - result["inlineImagePadding"] = inlineImagePadding; - } - - if (importantForAutofill != oldProps->importantForAutofill) { - result["importantForAutofill"] = importantForAutofill; - } - - if (showSoftInputOnFocus != oldProps->showSoftInputOnFocus) { - result["showSoftInputOnFocus"] = showSoftInputOnFocus; - } - - if (autoCorrect != oldProps->autoCorrect) { - result["autoCorrect"] = autoCorrect; - } - - if (allowFontScaling != oldProps->allowFontScaling) { - result["allowFontScaling"] = allowFontScaling; - } - - if (maxFontSizeMultiplier != oldProps->maxFontSizeMultiplier) { - result["maxFontSizeMultiplier"] = maxFontSizeMultiplier; - } - - if (keyboardType != oldProps->keyboardType) { - result["keyboardType"] = keyboardType; - } - - if (returnKeyType != oldProps->returnKeyType) { - result["returnKeyType"] = returnKeyType; - } - - if (secureTextEntry != oldProps->secureTextEntry) { - result["secureTextEntry"] = secureTextEntry; - } - - if (value != oldProps->value) { - result["value"] = value; - } - - if (selectTextOnFocus != oldProps->selectTextOnFocus) { - result["selectTextOnFocus"] = selectTextOnFocus; - } - - if (caretHidden != oldProps->caretHidden) { - result["caretHidden"] = caretHidden; - } - - if (contextMenuHidden != oldProps->contextMenuHidden) { - result["contextMenuHidden"] = contextMenuHidden; - } - - if (textShadowColor != oldProps->textShadowColor) { - result["textShadowColor"] = *textShadowColor; - } - - if (textShadowRadius != oldProps->textShadowRadius) { - result["textShadowRadius"] = textShadowRadius; - } - - if (textDecorationLine != oldProps->textDecorationLine) { - result["textDecorationLine"] = textDecorationLine; - } - - if (fontStyle != oldProps->fontStyle) { - result["fontStyle"] = fontStyle; - } - - if (textShadowOffset != oldProps->textShadowOffset) { - result["textShadowOffset"] = toDynamic(textShadowOffset); - } - - if (lineHeight != oldProps->lineHeight) { - result["lineHeight"] = lineHeight; - } - - if (textTransform != oldProps->textTransform) { - result["textTransform"] = textTransform; - } - - if (letterSpacing != oldProps->letterSpacing) { - result["letterSpacing"] = letterSpacing; - } - - if (fontSize != oldProps->fontSize) { - result["fontSize"] = fontSize; - } - - if (textAlign != oldProps->textAlign) { - result["textAlign"] = textAlign; - } - - if (includeFontPadding != oldProps->includeFontPadding) { - result["includeFontPadding"] = includeFontPadding; - } - - if (fontWeight != oldProps->fontWeight) { - result["fontWeight"] = fontWeight; - } - - if (fontFamily != oldProps->fontFamily) { - result["fontFamily"] = fontFamily; - } + appendIfChanged(result, "autoComplete", autoComplete, oldProps->autoComplete); + appendIfChanged( + result, "returnKeyLabel", returnKeyLabel, oldProps->returnKeyLabel); + appendIfChanged( + result, "numberOfLines", numberOfLines, oldProps->numberOfLines); + appendIfChanged( + result, + "disableFullscreenUI", + disableFullscreenUI, + oldProps->disableFullscreenUI); + appendIfChanged( + result, + "textBreakStrategy", + textBreakStrategy, + oldProps->textBreakStrategy); + appendIfChanged( + result, "inlineImageLeft", inlineImageLeft, oldProps->inlineImageLeft); + appendIfChanged( + result, + "inlineImagePadding", + inlineImagePadding, + oldProps->inlineImagePadding); + appendIfChanged( + result, + "importantForAutofill", + importantForAutofill, + oldProps->importantForAutofill); + appendIfChanged( + result, + "showSoftInputOnFocus", + showSoftInputOnFocus, + oldProps->showSoftInputOnFocus); + appendIfChanged(result, "autoCorrect", autoCorrect, oldProps->autoCorrect); + appendIfChanged( + result, "allowFontScaling", allowFontScaling, oldProps->allowFontScaling); + appendIfChanged( + result, + "maxFontSizeMultiplier", + maxFontSizeMultiplier, + oldProps->maxFontSizeMultiplier); + appendIfChanged(result, "keyboardType", keyboardType, oldProps->keyboardType); + appendIfChanged( + result, "returnKeyType", returnKeyType, oldProps->returnKeyType); + appendIfChanged( + result, "secureTextEntry", secureTextEntry, oldProps->secureTextEntry); + appendIfChanged(result, "value", value, oldProps->value); + appendIfChanged( + result, + "selectTextOnFocus", + selectTextOnFocus, + oldProps->selectTextOnFocus); + appendIfChanged(result, "caretHidden", caretHidden, oldProps->caretHidden); + appendIfChanged( + result, + "contextMenuHidden", + contextMenuHidden, + oldProps->contextMenuHidden); + appendDerefIfChanged( + result, "textShadowColor", textShadowColor, oldProps->textShadowColor); + appendIfChanged( + result, "textShadowRadius", textShadowRadius, oldProps->textShadowRadius); + appendIfChanged( + result, + "textDecorationLine", + textDecorationLine, + oldProps->textDecorationLine); + appendIfChanged(result, "fontStyle", fontStyle, oldProps->fontStyle); + appendConvertedIfChanged( + result, + "textShadowOffset", + textShadowOffset, + oldProps->textShadowOffset, + asDynamic); + appendIfChanged(result, "lineHeight", lineHeight, oldProps->lineHeight); + appendIfChanged( + result, "textTransform", textTransform, oldProps->textTransform); + appendIfChanged( + result, "letterSpacing", letterSpacing, oldProps->letterSpacing); + appendIfChanged(result, "fontSize", fontSize, oldProps->fontSize); + appendIfChanged(result, "textAlign", textAlign, oldProps->textAlign); + appendIfChanged( + result, + "includeFontPadding", + includeFontPadding, + oldProps->includeFontPadding); + appendIfChanged(result, "fontWeight", fontWeight, oldProps->fontWeight); + appendIfChanged(result, "fontFamily", fontFamily, oldProps->fontFamily); return result; } From 185990302f3f55375450cbc1fdf2ee98f81d6da9 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 9 Jun 2026 09:11:17 -0700 Subject: [PATCH 3/6] Simplify `HostPlatformViewProps::getDiffProps` (#57152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: `HostPlatformViewProps::getDiffProps` computes a `folly::dynamic` prop diff for Android with the highest complexity in the file: ~60 sequential per-prop `if (field != oldProps->field) result[key] = ...` guards plus inline `switch`es (`outlineStyle`, `backfaceVisibility`), a 28-call events block, a transform loop, and several accessibility object/array builders, for a cyclomatic complexity (CCN) of 87. This is a pure, behavior-preserving refactor. The recurring compare-and-assign shapes — plain `!=`, color dereference, direct `toString`/`toDynamic` conversion, and optional-with-converter — are pulled into small file-local `static` helpers, and the genuinely branching blocks (the two enum `switch`es, `overflow`/`scroll`, the events block, the transform loop, and the accessibility state/labelledBy/order/value/actions builders) are each moved verbatim into a named helper. Every prop keeps its explicit key/value/conversion on one line and in the same order, so the serialized `folly::dynamic` (keys, values, insertion order) is identical. The `// Borders` block is left inline unchanged. Only the `.cpp` is touched; there are no public header or API changes. Changelog: [Internal] Differential Revision: D108027816 --- .../components/view/HostPlatformViewProps.cpp | 658 ++++++++++-------- 1 file changed, 370 insertions(+), 288 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp index e3b17415629..3d7215ec8e9 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp @@ -501,67 +501,64 @@ ComponentName HostPlatformViewProps::getDiffPropsImplementationTarget() const { return "View"; } -folly::dynamic HostPlatformViewProps::getDiffProps( - const Props* prevProps) const { - folly::dynamic result = folly::dynamic::object(); - - static const auto defaultProps = HostPlatformViewProps(); - - const HostPlatformViewProps* oldProps = prevProps == nullptr - ? &defaultProps - : static_cast(prevProps); - - if (this == oldProps) { - return result; - } - - if (elevation != oldProps->elevation) { - result["elevation"] = elevation; - } - - if (focusable != oldProps->focusable) { - result["focusable"] = focusable; - } - - if (hasTVPreferredFocus != oldProps->hasTVPreferredFocus) { - result["hasTVPreferredFocus"] = hasTVPreferredFocus; - } - - if (needsOffscreenAlphaCompositing != - oldProps->needsOffscreenAlphaCompositing) { - result["needsOffscreenAlphaCompositing"] = needsOffscreenAlphaCompositing; - } - - if (renderToHardwareTextureAndroid != - oldProps->renderToHardwareTextureAndroid) { - result["renderToHardwareTextureAndroid"] = renderToHardwareTextureAndroid; - } - - if (screenReaderFocusable != oldProps->screenReaderFocusable) { - result["screenReaderFocusable"] = screenReaderFocusable; - } - - if (role != oldProps->role) { - result["role"] = toString(role); - } - - if (opacity != oldProps->opacity) { - result["opacity"] = opacity; +// Behavior-preserving helpers extracted from getDiffProps below to keep its +// cyclomatic complexity low. Each mirrors one of the recurring +// compare-and-assign shapes used per prop, so the serialized output (keys, +// values, conversions, and insertion order) is identical to the open-coded +// version. +template +static void appendIfChanged( + folly::dynamic& result, + const char* propName, + const T& newValue, + const T& oldValue) { + if (newValue != oldValue) { + result[propName] = newValue; } +} - if (backgroundColor != oldProps->backgroundColor) { - result["backgroundColor"] = *backgroundColor; +template +static void appendDerefIfChanged( + folly::dynamic& result, + const char* propName, + const T& newValue, + const T& oldValue) { + if (newValue != oldValue) { + result[propName] = *newValue; } +} - if (outlineColor != oldProps->outlineColor) { - result["outlineColor"] = *outlineColor; +template +static void appendConvertedIfChanged( + folly::dynamic& result, + const char* propName, + const T& newValue, + const T& oldValue, + Convert&& convert) { + if (newValue != oldValue) { + result[propName] = convert(newValue); } +} - if (outlineOffset != oldProps->outlineOffset) { - result["outlineOffset"] = outlineOffset; +template +static void appendOptionalIfChanged( + folly::dynamic& result, + const char* propName, + const std::optional& newValue, + const std::optional& oldValue, + Convert&& convert) { + if (newValue != oldValue) { + result[propName] = newValue.has_value() + ? folly::dynamic(convert(newValue.value())) + : folly::dynamic(nullptr); } +} - if (outlineStyle != oldProps->outlineStyle) { +static void appendOutlineStyleIfChanged( + folly::dynamic& result, + OutlineStyle outlineStyle, + OutlineStyle oldOutlineStyle) { + if (outlineStyle != oldOutlineStyle) { switch (outlineStyle) { case OutlineStyle::Solid: result["outlineStyle"] = "solid"; @@ -574,90 +571,13 @@ folly::dynamic HostPlatformViewProps::getDiffProps( break; } } +} - if (outlineWidth != oldProps->outlineWidth) { - result["outlineWidth"] = outlineWidth; - } - - if (shadowColor != oldProps->shadowColor) { - result["shadowColor"] = *shadowColor; - } - - if (shadowOpacity != oldProps->shadowOpacity) { - result["shadowOpacity"] = shadowOpacity; - } - - if (shadowRadius != oldProps->shadowRadius) { - result["shadowRadius"] = shadowRadius; - } - - if (shouldRasterize != oldProps->shouldRasterize) { - result["shouldRasterize"] = shouldRasterize; - } - - if (collapsable != oldProps->collapsable) { - result["collapsable"] = collapsable; - } - - if (removeClippedSubviews != oldProps->removeClippedSubviews) { - result["removeClippedSubviews"] = removeClippedSubviews; - } - - if (collapsableChildren != oldProps->collapsableChildren) { - result["collapsableChildren"] = collapsableChildren; - } - - if (onLayout != oldProps->onLayout) { - result["onLayout"] = onLayout; - } - - if (zIndex != oldProps->zIndex) { - result["zIndex"] = - zIndex.has_value() ? zIndex.value() : folly::dynamic(nullptr); - } - - if (boxShadow != oldProps->boxShadow) { - result["boxShadow"] = toDynamic(boxShadow); - } - - if (filter != oldProps->filter) { - result["filter"] = toDynamic(filter); - } - - if (backgroundImage != oldProps->backgroundImage) { - result["experimental_backgroundImage"] = toDynamic(backgroundImage); - } - - if (mixBlendMode != oldProps->mixBlendMode) { - result["mixBlendMode"] = toString(mixBlendMode); - } - - if (pointerEvents != oldProps->pointerEvents) { - result["pointerEvents"] = toString(pointerEvents); - } - - if (hitSlop != oldProps->hitSlop) { - result["hitSlop"] = toDynamic(hitSlop); - } - - if (nativeId != oldProps->nativeId) { - result["nativeID"] = nativeId; - } - - if (testId != oldProps->testId) { - result["testID"] = testId; - } - - if (accessible != oldProps->accessible) { - result["accessible"] = accessible; - } - - if (getClipsContentToBounds() != oldProps->getClipsContentToBounds()) { - result["overflow"] = getClipsContentToBounds() ? "hidden" : "visible"; - result["scroll"] = result["overflow"]; - } - - if (backfaceVisibility != oldProps->backfaceVisibility) { +static void appendBackfaceVisibilityIfChanged( + folly::dynamic& result, + BackfaceVisibility backfaceVisibility, + BackfaceVisibility oldBackfaceVisibility) { + if (backfaceVisibility != oldBackfaceVisibility) { switch (backfaceVisibility) { case BackfaceVisibility::Auto: result["backfaceVisibility"] = "auto"; @@ -670,237 +590,235 @@ folly::dynamic HostPlatformViewProps::getDiffProps( break; } } +} - if (nativeBackground != oldProps->nativeBackground) { - updateNativeDrawableProp( - result, "nativeBackgroundAndroid", nativeBackground); +static void appendOverflowIfChanged( + folly::dynamic& result, + bool clipsContentToBounds, + bool oldClipsContentToBounds) { + if (clipsContentToBounds != oldClipsContentToBounds) { + result["overflow"] = clipsContentToBounds ? "hidden" : "visible"; + result["scroll"] = result["overflow"]; } +} - if (nativeForeground != oldProps->nativeForeground) { - updateNativeDrawableProp( - result, "nativeForegroundAndroid", nativeForeground); +template +static void appendNativeDrawableIfChanged( + folly::dynamic& result, + const char* propName, + const T& newValue, + const T& oldValue) { + if (newValue != oldValue) { + updateNativeDrawableProp(result, propName, newValue); } +} - // Events - // TODO T212662692: pass events as std::bitset<64> to java - if (events != oldProps->events) { +template +static void +appendEventProps(folly::dynamic& result, const T& events, const T& oldEvents) { + if (events != oldEvents) { updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerEnter, "onPointerEnter"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerEnterCapture, "onPointerEnterCapture"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerMove, "onPointerMove"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerMoveCapture, "onPointerMoveCapture"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerLeave, "onPointerLeave"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerLeaveCapture, "onPointerLeaveCapture"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerOver, "onPointerOver"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerOverCapture, "onPointerOverCapture"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerOut, "onPointerOut"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::PointerOutCapture, "onPointerOutCapture"); updateEventProp( - result, events, oldProps->events, ViewEvents::Offset::Click, "onClick"); + result, events, oldEvents, ViewEvents::Offset::Click, "onClick"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ClickCapture, "onClickCapture"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::MoveShouldSetResponder, "onMoveShouldSetResponder"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::MoveShouldSetResponderCapture, "onMoveShouldSetResponderCapture"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::StartShouldSetResponder, "onStartShouldSetResponder"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::StartShouldSetResponderCapture, "onStartShouldSetResponderCapture"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ResponderGrant, "onResponderGrant"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ResponderReject, "onResponderReject"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ResponderStart, "onResponderStart"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ResponderEnd, "onResponderEnd"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ResponderRelease, "onResponderRelease"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ResponderMove, "onResponderMove"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ResponderTerminate, "onResponderTerminate"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ResponderTerminationRequest, "onResponderTerminationRequest"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::ShouldBlockNativeResponder, "onShouldBlockNativeResponder"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::TouchStart, "onTouchStart"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::TouchMove, "onTouchMove"); updateEventProp( - result, - events, - oldProps->events, - ViewEvents::Offset::TouchEnd, - "onTouchEnd"); + result, events, oldEvents, ViewEvents::Offset::TouchEnd, "onTouchEnd"); updateEventProp( result, events, - oldProps->events, + oldEvents, ViewEvents::Offset::TouchCancel, "onTouchCancel"); } +} - // Borders - auto borderWidths = getBorderWidths(); - auto oldBorderWidths = oldProps->getBorderWidths(); - if (borderWidths != oldBorderWidths) { - updateBorderWidthProps(result, borderWidths, oldBorderWidths); - } - - if (borderStyles != oldProps->borderStyles) { - updateBorderStyleProps(result, borderStyles, oldProps->borderStyles); - } - - if (borderColors != oldProps->borderColors) { - updateBorderColorsProps(result, borderColors, oldProps->borderColors); - } - - if (borderRadii != oldProps->borderRadii) { - updateBorderRadiusProps(result, borderRadii, oldProps->borderRadii); - } - - // Transforms - if (transform != oldProps->transform || - transformOrigin != oldProps->transformOrigin) { +template +static void appendTransformIfChanged( + folly::dynamic& result, + const TTransform& transform, + const TTransform& oldTransform, + const TOrigin& transformOrigin, + const TOrigin& oldTransformOrigin) { + if (transform != oldTransform || transformOrigin != oldTransformOrigin) { folly::dynamic resultTranslateArray = folly::dynamic::array(); for (const auto& operation : transform.operations) { updateTransformProps(transform, operation, resultTranslateArray); } result["transform"] = std::move(resultTranslateArray); } +} - if (transformOrigin != oldProps->transformOrigin) { - result["transformOrigin"] = transformOrigin; - } - - // Accessibility - - if (accessibilityState != oldProps->accessibilityState) { +template +static void appendAccessibilityStateIfChanged( + folly::dynamic& result, + const T& accessibilityState, + const T& oldAccessibilityState) { + if (accessibilityState != oldAccessibilityState) { updateAccessibilityStateProp( - result, accessibilityState, oldProps->accessibilityState); - } - - if (accessibilityLabel != oldProps->accessibilityLabel) { - result["accessibilityLabel"] = accessibilityLabel; + result, accessibilityState, oldAccessibilityState); } +} - if (accessibilityLabelledBy != oldProps->accessibilityLabelledBy) { +template +static void appendAccessibilityLabelledByIfChanged( + folly::dynamic& result, + const T& accessibilityLabelledBy, + const T& oldAccessibilityLabelledBy) { + if (accessibilityLabelledBy != oldAccessibilityLabelledBy) { auto accessibilityLabelledByValues = folly::dynamic::array(); for (const auto& accessibilityLabelledByValue : accessibilityLabelledBy.value) { @@ -908,32 +826,28 @@ folly::dynamic HostPlatformViewProps::getDiffProps( } result["accessibilityLabelledBy"] = accessibilityLabelledByValues; } +} - if (accessibilityOrder != oldProps->accessibilityOrder) { +template +static void appendAccessibilityOrderIfChanged( + folly::dynamic& result, + const T& accessibilityOrder, + const T& oldAccessibilityOrder) { + if (accessibilityOrder != oldAccessibilityOrder) { auto accessibilityChildrenIds = folly::dynamic::array(); for (const auto& accessibilityChildId : accessibilityOrder) { accessibilityChildrenIds.push_back(accessibilityChildId); } result["experimental_accessibilityOrder"] = accessibilityChildrenIds; } +} - if (accessibilityLiveRegion != oldProps->accessibilityLiveRegion) { - result["accessibilityLiveRegion"] = toString(accessibilityLiveRegion); - } - - if (accessibilityHint != oldProps->accessibilityHint) { - result["accessibilityHint"] = accessibilityHint; - } - - if (accessibilityRole != oldProps->accessibilityRole) { - result["accessibilityRole"] = accessibilityRole; - } - - if (accessibilityLanguage != oldProps->accessibilityLanguage) { - result["accessibilityLanguage"] = accessibilityLanguage; - } - - if (accessibilityValue != oldProps->accessibilityValue) { +template +static void appendAccessibilityValueIfChanged( + folly::dynamic& result, + const T& accessibilityValue, + const T& oldAccessibilityValue) { + if (accessibilityValue != oldAccessibilityValue) { folly::dynamic accessibilityValueObject = folly::dynamic::object(); if (accessibilityValue.min.has_value()) { accessibilityValueObject["min"] = accessibilityValue.min.value(); @@ -949,8 +863,14 @@ folly::dynamic HostPlatformViewProps::getDiffProps( } result["accessibilityValue"] = accessibilityValueObject; } +} - if (accessibilityActions != oldProps->accessibilityActions) { +template +static void appendAccessibilityActionsIfChanged( + folly::dynamic& result, + const T& accessibilityActions, + const T& oldAccessibilityActions) { + if (accessibilityActions != oldAccessibilityActions) { auto accessibilityActionsArray = folly::dynamic::array(); for (const auto& accessibilityAction : accessibilityActions) { folly::dynamic accessibilityActionObject = folly::dynamic::object(); @@ -962,80 +882,242 @@ folly::dynamic HostPlatformViewProps::getDiffProps( } result["accessibilityActions"] = accessibilityActionsArray; } +} - if (accessibilityViewIsModal != oldProps->accessibilityViewIsModal) { - result["accessibilityViewIsModal"] = accessibilityViewIsModal; - } +folly::dynamic HostPlatformViewProps::getDiffProps( + const Props* prevProps) const { + folly::dynamic result = folly::dynamic::object(); - if (accessibilityElementsHidden != oldProps->accessibilityElementsHidden) { - result["accessibilityElementsHidden"] = accessibilityElementsHidden; - } + static const auto defaultProps = HostPlatformViewProps(); - if (accessibilityIgnoresInvertColors != - oldProps->accessibilityIgnoresInvertColors) { - result["accessibilityIgnoresInvertColors"] = - accessibilityIgnoresInvertColors; - } + const HostPlatformViewProps* oldProps = prevProps == nullptr + ? &defaultProps + : static_cast(prevProps); - if (onAccessibilityTap != oldProps->onAccessibilityTap) { - result["onAccessibilityTap"] = onAccessibilityTap; + if (this == oldProps) { + return result; } - if (onAccessibilityMagicTap != oldProps->onAccessibilityMagicTap) { - result["onAccessibilityMagicTap"] = onAccessibilityMagicTap; - } + auto asString = [](const auto& value) { return toString(value); }; + auto asDynamic = [](const auto& value) { return toDynamic(value); }; + auto asIs = [](const auto& value) { return value; }; - if (onAccessibilityEscape != oldProps->onAccessibilityEscape) { - result["onAccessibilityEscape"] = onAccessibilityEscape; - } + appendIfChanged(result, "elevation", elevation, oldProps->elevation); + appendIfChanged(result, "focusable", focusable, oldProps->focusable); + appendIfChanged( + result, + "hasTVPreferredFocus", + hasTVPreferredFocus, + oldProps->hasTVPreferredFocus); + appendIfChanged( + result, + "needsOffscreenAlphaCompositing", + needsOffscreenAlphaCompositing, + oldProps->needsOffscreenAlphaCompositing); + appendIfChanged( + result, + "renderToHardwareTextureAndroid", + renderToHardwareTextureAndroid, + oldProps->renderToHardwareTextureAndroid); + appendIfChanged( + result, + "screenReaderFocusable", + screenReaderFocusable, + oldProps->screenReaderFocusable); + appendConvertedIfChanged(result, "role", role, oldProps->role, asString); + appendIfChanged(result, "opacity", opacity, oldProps->opacity); + appendDerefIfChanged( + result, "backgroundColor", backgroundColor, oldProps->backgroundColor); + appendDerefIfChanged( + result, "outlineColor", outlineColor, oldProps->outlineColor); + appendIfChanged( + result, "outlineOffset", outlineOffset, oldProps->outlineOffset); + appendOutlineStyleIfChanged(result, outlineStyle, oldProps->outlineStyle); + appendIfChanged(result, "outlineWidth", outlineWidth, oldProps->outlineWidth); + appendDerefIfChanged( + result, "shadowColor", shadowColor, oldProps->shadowColor); + appendIfChanged( + result, "shadowOpacity", shadowOpacity, oldProps->shadowOpacity); + appendIfChanged(result, "shadowRadius", shadowRadius, oldProps->shadowRadius); + appendIfChanged( + result, "shouldRasterize", shouldRasterize, oldProps->shouldRasterize); + appendIfChanged(result, "collapsable", collapsable, oldProps->collapsable); + appendIfChanged( + result, + "removeClippedSubviews", + removeClippedSubviews, + oldProps->removeClippedSubviews); + appendIfChanged( + result, + "collapsableChildren", + collapsableChildren, + oldProps->collapsableChildren); + appendIfChanged(result, "onLayout", onLayout, oldProps->onLayout); + appendOptionalIfChanged(result, "zIndex", zIndex, oldProps->zIndex, asIs); + appendConvertedIfChanged( + result, "boxShadow", boxShadow, oldProps->boxShadow, asDynamic); + appendConvertedIfChanged( + result, "filter", filter, oldProps->filter, asDynamic); + appendConvertedIfChanged( + result, + "experimental_backgroundImage", + backgroundImage, + oldProps->backgroundImage, + asDynamic); + appendConvertedIfChanged( + result, "mixBlendMode", mixBlendMode, oldProps->mixBlendMode, asString); + appendConvertedIfChanged( + result, + "pointerEvents", + pointerEvents, + oldProps->pointerEvents, + asString); + appendConvertedIfChanged( + result, "hitSlop", hitSlop, oldProps->hitSlop, asDynamic); + appendIfChanged(result, "nativeID", nativeId, oldProps->nativeId); + appendIfChanged(result, "testID", testId, oldProps->testId); + appendIfChanged(result, "accessible", accessible, oldProps->accessible); + appendOverflowIfChanged( + result, getClipsContentToBounds(), oldProps->getClipsContentToBounds()); + appendBackfaceVisibilityIfChanged( + result, backfaceVisibility, oldProps->backfaceVisibility); + appendNativeDrawableIfChanged( + result, + "nativeBackgroundAndroid", + nativeBackground, + oldProps->nativeBackground); + appendNativeDrawableIfChanged( + result, + "nativeForegroundAndroid", + nativeForeground, + oldProps->nativeForeground); - if (onAccessibilityAction != oldProps->onAccessibilityAction) { - result["onAccessibilityAction"] = onAccessibilityAction; - } + // Events + // TODO T212662692: pass events as std::bitset<64> to java + appendEventProps(result, events, oldProps->events); - if (importantForAccessibility != oldProps->importantForAccessibility) { - result["importantForAccessibility"] = toString(importantForAccessibility); + // Borders + auto borderWidths = getBorderWidths(); + auto oldBorderWidths = oldProps->getBorderWidths(); + if (borderWidths != oldBorderWidths) { + updateBorderWidthProps(result, borderWidths, oldBorderWidths); } - if (nextFocusDown != oldProps->nextFocusDown) { - if (nextFocusDown.has_value()) { - result["nextFocusDown"] = nextFocusDown.value(); - } else { - result["nextFocusDown"] = folly::dynamic(nullptr); - } + if (borderStyles != oldProps->borderStyles) { + updateBorderStyleProps(result, borderStyles, oldProps->borderStyles); } - if (nextFocusForward != oldProps->nextFocusForward) { - if (nextFocusForward.has_value()) { - result["nextFocusForward"] = nextFocusForward.value(); - } else { - result["nextFocusForward"] = folly::dynamic(nullptr); - } + if (borderColors != oldProps->borderColors) { + updateBorderColorsProps(result, borderColors, oldProps->borderColors); } - if (nextFocusLeft != oldProps->nextFocusLeft) { - if (nextFocusLeft.has_value()) { - result["nextFocusLeft"] = nextFocusLeft.value(); - } else { - result["nextFocusLeft"] = folly::dynamic(nullptr); - } + if (borderRadii != oldProps->borderRadii) { + updateBorderRadiusProps(result, borderRadii, oldProps->borderRadii); } - if (nextFocusRight != oldProps->nextFocusRight) { - if (nextFocusRight.has_value()) { - result["nextFocusRight"] = nextFocusRight.value(); - } else { - result["nextFocusRight"] = folly::dynamic(nullptr); - } - } + // Transforms + appendTransformIfChanged( + result, + transform, + oldProps->transform, + transformOrigin, + oldProps->transformOrigin); + appendIfChanged( + result, "transformOrigin", transformOrigin, oldProps->transformOrigin); - if (nextFocusUp != oldProps->nextFocusUp) { - if (nextFocusUp.has_value()) { - result["nextFocusUp"] = nextFocusUp.value(); - } else { - result["nextFocusUp"] = folly::dynamic(nullptr); - } - } + // Accessibility + + appendAccessibilityStateIfChanged( + result, accessibilityState, oldProps->accessibilityState); + appendIfChanged( + result, + "accessibilityLabel", + accessibilityLabel, + oldProps->accessibilityLabel); + appendAccessibilityLabelledByIfChanged( + result, accessibilityLabelledBy, oldProps->accessibilityLabelledBy); + appendAccessibilityOrderIfChanged( + result, accessibilityOrder, oldProps->accessibilityOrder); + appendConvertedIfChanged( + result, + "accessibilityLiveRegion", + accessibilityLiveRegion, + oldProps->accessibilityLiveRegion, + asString); + appendIfChanged( + result, + "accessibilityHint", + accessibilityHint, + oldProps->accessibilityHint); + appendIfChanged( + result, + "accessibilityRole", + accessibilityRole, + oldProps->accessibilityRole); + appendIfChanged( + result, + "accessibilityLanguage", + accessibilityLanguage, + oldProps->accessibilityLanguage); + appendAccessibilityValueIfChanged( + result, accessibilityValue, oldProps->accessibilityValue); + appendAccessibilityActionsIfChanged( + result, accessibilityActions, oldProps->accessibilityActions); + appendIfChanged( + result, + "accessibilityViewIsModal", + accessibilityViewIsModal, + oldProps->accessibilityViewIsModal); + appendIfChanged( + result, + "accessibilityElementsHidden", + accessibilityElementsHidden, + oldProps->accessibilityElementsHidden); + appendIfChanged( + result, + "accessibilityIgnoresInvertColors", + accessibilityIgnoresInvertColors, + oldProps->accessibilityIgnoresInvertColors); + appendIfChanged( + result, + "onAccessibilityTap", + onAccessibilityTap, + oldProps->onAccessibilityTap); + appendIfChanged( + result, + "onAccessibilityMagicTap", + onAccessibilityMagicTap, + oldProps->onAccessibilityMagicTap); + appendIfChanged( + result, + "onAccessibilityEscape", + onAccessibilityEscape, + oldProps->onAccessibilityEscape); + appendIfChanged( + result, + "onAccessibilityAction", + onAccessibilityAction, + oldProps->onAccessibilityAction); + appendConvertedIfChanged( + result, + "importantForAccessibility", + importantForAccessibility, + oldProps->importantForAccessibility, + asString); + appendOptionalIfChanged( + result, "nextFocusDown", nextFocusDown, oldProps->nextFocusDown, asIs); + appendOptionalIfChanged( + result, + "nextFocusForward", + nextFocusForward, + oldProps->nextFocusForward, + asIs); + appendOptionalIfChanged( + result, "nextFocusLeft", nextFocusLeft, oldProps->nextFocusLeft, asIs); + appendOptionalIfChanged( + result, "nextFocusRight", nextFocusRight, oldProps->nextFocusRight, asIs); + appendOptionalIfChanged( + result, "nextFocusUp", nextFocusUp, oldProps->nextFocusUp, asIs); return result; } From e656c0be73deaf31e24becc6157125ef3418df58 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 9 Jun 2026 09:11:17 -0700 Subject: [PATCH 4/6] Simplify Fabric `Differentiator` mutation calculation (#57153) Summary: `calculateShadowViewMutationsFlattener` (CCN 73) and `calculateShadowViewMutations` (CCN 45) are the core of the Fabric tree differ and were the two most complex functions in the mounting library. This is a pure, behavior-preserving refactor: cohesive blocks are extracted into file-local `static` helpers (remove/insert-on-(un)flatten, matched-grandchildren update, opposite/normal reparent handling, and the greedy diff stages), turning both functions into flat sequences of helper calls. Traversal order and the exact sequence of emitted mutations are preserved. Only the `.cpp` is touched; no header or public API change. Changelog: [Internal] Differential Revision: D108027813 --- .../renderer/mounting/Differentiator.cpp | 1846 ++++++++++------- 1 file changed, 1095 insertions(+), 751 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp index dd89fbb89a3..c9a0e235255 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp @@ -383,6 +383,613 @@ static void updateMatchedPair( } } +/** + * Remove all children (non-recursively) of tree being flattened, or insert + * children into parent tree if they're being unflattened. Caller will take + * care of the corresponding action in the other tree (caller will handle + * DELETE case if we REMOVE here; caller will handle CREATE case if we INSERT + * here). + * + * Extracted from `calculateShadowViewMutationsFlattener` to reduce its + * complexity; behavior is unchanged. + */ +static void calculateShadowViewMutationsFlattenerRemoveOrInsertChild( + OrderedMutationInstructionContainer& mutationContainer, + ReparentMode reparentMode, + const ShadowViewNodePair& node, + const ShadowViewNodePair& treeChildPair, + bool existsInOtherTree) { + if (!treeChildPair.isConcreteView) { + return; + } + if (reparentMode == ReparentMode::Flatten) { + // treeChildPair.shadowView represents the "old" view in this case. + // If there's a "new" view, an UPDATE new -> old will be generated + // and will be executed before the REMOVE. Thus, we must actually + // perform a REMOVE (new view) FROM (old index) in this case so that + // we don't hit asserts in StubViewTree's REMOVE path. + // We also only do this if the "other" (newer) view is concrete. If + // it's not concrete, there will be no UPDATE mutation. + react_native_assert(existsInOtherTree == treeChildPair.inOtherTree()); + if (treeChildPair.inOtherTree() && + treeChildPair.otherTreePair->isConcreteView) { + mutationContainer.removeMutations.push_back( + ShadowViewMutation::RemoveMutation( + node.shadowView.tag, + treeChildPair.otherTreePair->shadowView, + static_cast(treeChildPair.mountIndex))); + } else { + mutationContainer.removeMutations.push_back( + ShadowViewMutation::RemoveMutation( + node.shadowView.tag, + treeChildPair.shadowView, + static_cast(treeChildPair.mountIndex))); + } + } else { + // treeChildParent represents the "new" version of the node, so + // we can safely insert it without checking in the other tree + mutationContainer.insertMutations.push_back( + ShadowViewMutation::InsertMutation( + node.shadowView.tag, + treeChildPair.shadowView, + static_cast(treeChildPair.mountIndex))); + } +} + +/** + * Update the grandchildren of a matched node pair when neither side is + * flattened. Extracted from `calculateShadowViewMutationsFlattener` to reduce + * its complexity; behavior is unchanged. + */ +static void calculateShadowViewMutationsFlattenerUpdateMatchedGrandchildren( + OrderedMutationInstructionContainer& mutationContainer, + const ShadowViewNodePair& oldTreeNodePair, + const ShadowViewNodePair& newTreeNodePair, + const CullingContext& adjustedOldCullingContext, + const CullingContext& adjustedNewCullingContext) { + if (oldTreeNodePair.shadowNode != newTreeNodePair.shadowNode || + adjustedOldCullingContext != adjustedNewCullingContext) { + ViewNodePairScope innerScope{}; + auto oldGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( + oldTreeNodePair, innerScope, false, adjustedOldCullingContext); + auto newGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( + newTreeNodePair, innerScope, false, adjustedNewCullingContext); + + calculateShadowViewMutations( + innerScope, + mutationContainer.downwardMutations, + newTreeNodePair.shadowView.tag, + std::move(oldGrandChildPairs), + std::move(newGrandChildPairs), + adjustedOldCullingContext, + adjustedNewCullingContext); + } +} + +/** + * Handle the case where a child is being flattened/unflattened in the opposite + * direction to its parent (child reparent mode differs from parent reparent + * mode). Extracted from `calculateShadowViewMutationsFlattener` to reduce its + * complexity; behavior is unchanged. + */ +static void calculateShadowViewMutationsFlattenerChildOppositeReparent( + ViewNodePairScope& scope, + ReparentMode reparentMode, + ReparentMode childReparentMode, + OrderedMutationInstructionContainer& mutationContainer, + Tag parentTag, + std::unordered_map& unvisitedOtherNodes, + const ShadowViewNodePair& oldTreeNodePair, + const ShadowViewNodePair& newTreeNodePair, + Tag parentTagForUpdate, + std::unordered_map* subVisitedNewMap, + std::unordered_map* subVisitedOldMap, + const CullingContext& adjustedOldCullingContext, + const CullingContext& adjustedNewCullingContext, + std::unordered_map& + deletionCreationCandidatePairs) { + // Get flattened nodes from either new or old tree + auto flattenedNodes = sliceChildShadowNodeViewPairsFromViewNodePair( + (childReparentMode == ReparentMode::Flatten ? newTreeNodePair + : oldTreeNodePair), + scope, + true, + childReparentMode == ReparentMode::Flatten ? adjustedNewCullingContext + : adjustedOldCullingContext); + // Construct unvisited nodes map + std::unordered_map unvisitedRecursiveChildPairs; + unvisitedRecursiveChildPairs.reserve(flattenedNodes.size()); + for (auto& flattenedNode : flattenedNodes) { + auto& newChild = *flattenedNode; + + auto unvisitedOtherNodesIt = + unvisitedOtherNodes.find(newChild.shadowView.tag); + if (unvisitedOtherNodesIt != unvisitedOtherNodes.end()) { + auto unvisitedItPair = *unvisitedOtherNodesIt->second; + unvisitedRecursiveChildPairs.insert( + {unvisitedItPair.shadowView.tag, &unvisitedItPair}); + } else { + unvisitedRecursiveChildPairs.insert({newChild.shadowView.tag, &newChild}); + } + } + + if (childReparentMode == ReparentMode::Flatten) { + // Unflatten parent, flatten child + react_native_assert(reparentMode == ReparentMode::Unflatten); + auto fixedParentTagForUpdate = newTreeNodePair.shadowView.tag; + // Flatten old tree into new list + // At the end of this loop we still want to know which of these + // children are visited, so we reuse the `newRemainingPairs` map. + calculateShadowViewMutationsFlattener( + scope, + ReparentMode::Flatten, + mutationContainer, + newTreeNodePair.shadowView.tag, + unvisitedRecursiveChildPairs, + oldTreeNodePair, + fixedParentTagForUpdate, + subVisitedNewMap, + subVisitedOldMap, + adjustedNewCullingContext, + adjustedNewCullingContext); + } else { + // Flatten parent, unflatten child + react_native_assert(reparentMode == ReparentMode::Flatten); + // Unflatten old list into new tree + auto fixedParentTagForUpdate = parentTagForUpdate; + calculateShadowViewMutationsFlattener( + scope, + /* reparentMode */ ReparentMode::Unflatten, + mutationContainer, + parentTag, + /* unvisitedOtherNodes */ unvisitedRecursiveChildPairs, + /* node */ newTreeNodePair, + /* parentTagForUpdate */ fixedParentTagForUpdate, + /* parentSubVisitedOtherNewNodes */ subVisitedNewMap, + /* parentSubVisitedOtherOldNodes */ subVisitedOldMap, + /* cullingContextForUnvisitedOtherNodes */ + adjustedOldCullingContext, + /* cullingContext */ adjustedOldCullingContext); + + // If old nodes were not visited, we know that we can delete them + // now. They will be removed from the hierarchy by the outermost + // loop of this function. + for (auto& unvisitedRecursiveChildPair : unvisitedRecursiveChildPairs) { + auto& oldFlattenedNode = *unvisitedRecursiveChildPair.second; + + // Node unvisited - mark the entire subtree for deletion + if (oldFlattenedNode.isConcreteView && !oldFlattenedNode.inOtherTree()) { + Tag tag = oldFlattenedNode.shadowView.tag; + auto deleteCreateIt = deletionCreationCandidatePairs.find( + oldFlattenedNode.shadowView.tag); + if (deleteCreateIt == deletionCreationCandidatePairs.end()) { + deletionCreationCandidatePairs.insert({tag, &oldFlattenedNode}); + } + } else { + // Node was visited - make sure to remove it from + // "newRemainingPairs" map + auto newRemainingIt = + unvisitedOtherNodes.find(oldFlattenedNode.shadowView.tag); + if (newRemainingIt != unvisitedOtherNodes.end()) { + unvisitedOtherNodes.erase(newRemainingIt); + } + } + } + } +} + +/** + * Final step of `calculateShadowViewMutationsFlattener`: go through + * creation/deletion candidates and delete/create subtrees if they were never + * visited during the execution of the main loop and recursions. Extracted to + * reduce complexity; behavior is unchanged. + */ +static void calculateShadowViewMutationsFlattenerProcessCandidates( + OrderedMutationInstructionContainer& mutationContainer, + ReparentMode reparentMode, + const std::unordered_map& + deletionCreationCandidatePairs, + const CullingContext& cullingContext) { + for (auto& deletionCreationCandidatePair : deletionCreationCandidatePairs) { + auto& treeChildPair = *deletionCreationCandidatePair.second; + + // If node was visited during a flattening/unflattening recursion, + // and the node in the other tree is concrete, that means it was + // already created/deleted and we don't need to do that here. + // It is always the responsibility of the matcher to update subtrees when + // nodes are matched. + if (treeChildPair.inOtherTree()) { + continue; + } + + auto adjustedCullingContext = + cullingContext.adjustCullingContextIfNeeded(treeChildPair); + + if (reparentMode == ReparentMode::Flatten) { + mutationContainer.deleteMutations.push_back( + ShadowViewMutation::DeleteMutation(treeChildPair.shadowView)); + + if (!treeChildPair.flattened) { + ViewNodePairScope innerScope{}; + calculateShadowViewMutations( + innerScope, + mutationContainer.destructiveDownwardMutations, + treeChildPair.shadowView.tag, + sliceChildShadowNodeViewPairsFromViewNodePair( + treeChildPair, innerScope, false, adjustedCullingContext), + {}, + adjustedCullingContext, + {}); + } + } else { + mutationContainer.createMutations.push_back( + ShadowViewMutation::CreateMutation(treeChildPair.shadowView)); + + if (!treeChildPair.flattened) { + ViewNodePairScope innerScope{}; + calculateShadowViewMutations( + innerScope, + mutationContainer.downwardMutations, + treeChildPair.shadowView.tag, + {}, + sliceChildShadowNodeViewPairsFromViewNodePair( + treeChildPair, innerScope, false, adjustedCullingContext), + {}, + adjustedCullingContext); + } + } + } +} + +/** + * Handle the case where one of the children is flattened or unflattened, in the + * context of a parent flattening or unflattening. Dispatches to the same-mode + * recursion (child mode equals parent mode) or to the opposite-mode handler. + * Extracted from `calculateShadowViewMutationsFlattener` to reduce its + * complexity; behavior is unchanged. + */ +static void calculateShadowViewMutationsFlattenerHandleChildReparent( + ViewNodePairScope& scope, + ReparentMode reparentMode, + OrderedMutationInstructionContainer& mutationContainer, + Tag parentTag, + std::unordered_map& unvisitedOtherNodes, + ShadowViewNodePair& treeChildPair, + const ShadowViewNodePair& oldTreeNodePair, + const ShadowViewNodePair& newTreeNodePair, + Tag parentTagForUpdate, + std::unordered_map* subVisitedNewMap, + std::unordered_map* subVisitedOldMap, + const CullingContext& cullingContextForUnvisitedOtherNodes, + const CullingContext& cullingContext, + const CullingContext& adjustedOldCullingContext, + const CullingContext& adjustedNewCullingContext, + std::unordered_map& + deletionCreationCandidatePairs) { + ReparentMode childReparentMode = + (oldTreeNodePair.flattened ? ReparentMode::Unflatten + : ReparentMode::Flatten); + + // Case 1: child mode is the same as parent. + // This is a flatten-flatten, or unflatten-unflatten. + if (childReparentMode == reparentMode) { + calculateShadowViewMutationsFlattener( + scope, + childReparentMode, + mutationContainer, + (reparentMode == ReparentMode::Flatten + ? parentTag + : newTreeNodePair.shadowView.tag), + unvisitedOtherNodes, + treeChildPair, + (reparentMode == ReparentMode::Flatten + ? oldTreeNodePair.shadowView.tag + : (ReactNativeFeatureFlags:: + fixDifferentiatorParentTagForUnflattenCase() + ? parentTagForUpdate + : parentTag)), + subVisitedNewMap, + subVisitedOldMap, + cullingContextForUnvisitedOtherNodes, + cullingContext.adjustCullingContextIfNeeded(treeChildPair)); + } else { + calculateShadowViewMutationsFlattenerChildOppositeReparent( + scope, + reparentMode, + childReparentMode, + mutationContainer, + parentTag, + unvisitedOtherNodes, + oldTreeNodePair, + newTreeNodePair, + parentTagForUpdate, + subVisitedNewMap, + subVisitedOldMap, + adjustedOldCullingContext, + adjustedNewCullingContext, + deletionCreationCandidatePairs); + } +} + +/** + * Mark that node exists in another tree, but only if the tree node is a + * concrete view. Removing the node from the unvisited list prevents the caller + * from taking further action on this node, so make sure to delete/create if the + * concreteness of the node has changed. Extracted from + * `calculateShadowViewMutationsFlattener`; behavior is unchanged. + */ +static void calculateShadowViewMutationsFlattenerAppendConcretenessMutation( + OrderedMutationInstructionContainer& mutationContainer, + const ShadowViewNodePair& oldTreeNodePair, + const ShadowViewNodePair& newTreeNodePair) { + if (newTreeNodePair.isConcreteView != oldTreeNodePair.isConcreteView) { + if (newTreeNodePair.isConcreteView) { + mutationContainer.createMutations.push_back( + ShadowViewMutation::CreateMutation(newTreeNodePair.shadowView)); + } else { + mutationContainer.deleteMutations.push_back( + ShadowViewMutation::DeleteMutation(oldTreeNodePair.shadowView)); + } + } +} + +/** + * Record a concrete tree child that does not exist in the other tree as a + * candidate for full subtree deletion/creation at the end of the flattener. + * Extracted from `calculateShadowViewMutationsFlattener`; behavior is + * unchanged. + */ +static void calculateShadowViewMutationsFlattenerMarkCandidate( + ShadowViewNodePair& treeChildPair, + std::unordered_map& + deletionCreationCandidatePairs) { + // Node does not in exist in other tree. + if (treeChildPair.isConcreteView && !treeChildPair.inOtherTree()) { + auto deletionCreationIt = + deletionCreationCandidatePairs.find(treeChildPair.shadowView.tag); + if (deletionCreationIt == deletionCreationCandidatePairs.end()) { + deletionCreationCandidatePairs.insert( + {treeChildPair.shadowView.tag, &treeChildPair}); + } + } +} + +/** + * Process a single tree child during (un)flattening: if the child is matched in + * the other tree, reconcile the pair and its subtree; otherwise mark it as a + * candidate for deletion/creation. Extracted from + * `calculateShadowViewMutationsFlattener` to reduce its complexity; behavior is + * unchanged. This is invoked as the last statement of the main loop body, so + * the early `return`s here are equivalent to the `continue`s they replaced. + */ +static void calculateShadowViewMutationsFlattenerMatchChild( + ViewNodePairScope& scope, + ReparentMode reparentMode, + OrderedMutationInstructionContainer& mutationContainer, + Tag parentTag, + std::unordered_map& unvisitedOtherNodes, + ShadowViewNodePair& treeChildPair, + Tag parentTagForUpdate, + std::unordered_map* subVisitedNewMap, + std::unordered_map* subVisitedOldMap, + const CullingContext& cullingContextForUnvisitedOtherNodes, + const CullingContext& cullingContext, + std::unordered_map& + deletionCreationCandidatePairs, + bool existsInOtherTree, + ShadowViewNodePair* otherTreeNodePairPtr, + bool alreadyUpdated, + bool foundInUnvisited, + bool foundInSubVisitedNew, + bool foundInSubVisitedOld) { + // Find in other tree + if (existsInOtherTree) { + react_native_assert(otherTreeNodePairPtr != nullptr); + auto& otherTreeNodePair = *otherTreeNodePairPtr; + + auto& newTreeNodePair = + (reparentMode == ReparentMode::Flatten ? otherTreeNodePair + : treeChildPair); + auto& oldTreeNodePair = + (reparentMode == ReparentMode::Flatten ? treeChildPair + : otherTreeNodePair); + + react_native_assert(newTreeNodePair.shadowView.tag != 0); + react_native_assert(oldTreeNodePair.shadowView.tag != 0); + react_native_assert( + oldTreeNodePair.shadowView.tag == newTreeNodePair.shadowView.tag); + + // If we've already done updates, don't repeat it. + if (alreadyUpdated) { + return; + } + + // If we've already done updates on this node, don't repeat. + if (reparentMode == ReparentMode::Flatten && !foundInUnvisited && + foundInSubVisitedOld) { + return; + } else if ( + reparentMode == ReparentMode::Unflatten && !foundInUnvisited && + foundInSubVisitedNew) { + return; + } + + // TODO: compare ShadowNode pointer instead of ShadowView here? + // Or ShadowNode ptr comparison before comparing ShadowView, to allow for + // short-circuiting? ShadowView comparison is relatively expensive vs + // ShadowNode. + if (newTreeNodePair.shadowView != oldTreeNodePair.shadowView && + newTreeNodePair.isConcreteView && oldTreeNodePair.isConcreteView) { + // We execute updates before creates, so pass the current parent in when + // unflattening. + // TODO: whenever we insert, we already update the relevant properties, + // so this update is redundant. We should remove this. + mutationContainer.updateMutations.push_back( + ShadowViewMutation::UpdateMutation( + oldTreeNodePair.shadowView, + newTreeNodePair.shadowView, + parentTagForUpdate)); + } + + auto adjustedOldCullingContext = reparentMode == ReparentMode::Flatten + ? cullingContext.adjustCullingContextIfNeeded(oldTreeNodePair) + : cullingContextForUnvisitedOtherNodes.adjustCullingContextIfNeeded( + oldTreeNodePair); + auto adjustedNewCullingContext = reparentMode == ReparentMode::Flatten + ? cullingContextForUnvisitedOtherNodes.adjustCullingContextIfNeeded( + newTreeNodePair) + : cullingContext.adjustCullingContextIfNeeded(newTreeNodePair); + + // Update children if appropriate. + if (!oldTreeNodePair.flattened && !newTreeNodePair.flattened) { + calculateShadowViewMutationsFlattenerUpdateMatchedGrandchildren( + mutationContainer, + oldTreeNodePair, + newTreeNodePair, + adjustedOldCullingContext, + adjustedNewCullingContext); + } else if (oldTreeNodePair.flattened != newTreeNodePair.flattened) { + // We need to handle one of the children being flattened or + // unflattened, in the context of a parent flattening or unflattening. + calculateShadowViewMutationsFlattenerHandleChildReparent( + scope, + reparentMode, + mutationContainer, + parentTag, + unvisitedOtherNodes, + treeChildPair, + oldTreeNodePair, + newTreeNodePair, + parentTagForUpdate, + subVisitedNewMap, + subVisitedOldMap, + cullingContextForUnvisitedOtherNodes, + cullingContext, + adjustedOldCullingContext, + adjustedNewCullingContext, + deletionCreationCandidatePairs); + } + + // Mark that node exists in another tree, but only if the tree node is a + // concrete view. Removing the node from the unvisited list prevents the + // caller from taking further action on this node, so make sure to + // delete/create if the Concreteness of the node has changed. + calculateShadowViewMutationsFlattenerAppendConcretenessMutation( + mutationContainer, oldTreeNodePair, newTreeNodePair); + + subVisitedNewMap->insert( + {newTreeNodePair.shadowView.tag, &newTreeNodePair}); + subVisitedOldMap->insert( + {oldTreeNodePair.shadowView.tag, &oldTreeNodePair}); + } else { + calculateShadowViewMutationsFlattenerMarkCandidate( + treeChildPair, deletionCreationCandidatePairs); + } +} + +/** + * Result of looking up a tree child in the "other" tree during (un)flattening. + * See `findFlattenerChildMatch`. + */ +struct FlattenerChildMatch { + bool existsInOtherTree{false}; + ShadowViewNodePair* otherTreeNodePairPtr{nullptr}; + bool alreadyUpdated{false}; + bool foundInUnvisited{false}; + bool foundInSubVisitedNew{false}; + bool foundInSubVisitedOld{false}; +}; + +/** + * Try to find a tree child in the other tree (across the unvisited, sub-visited + * new, and sub-visited old maps), and, if found, wire up the `otherTreePair` + * back-pointers. Extracted from `calculateShadowViewMutationsFlattener` to + * reduce its complexity; behavior is unchanged. + */ +static FlattenerChildMatch findFlattenerChildMatch( + ReparentMode reparentMode, + ShadowViewNodePair& treeChildPair, + std::unordered_map& unvisitedOtherNodes, + std::unordered_map* subVisitedNewMap, + std::unordered_map* subVisitedOldMap) { + // Try to find node in other tree + auto unvisitedIt = unvisitedOtherNodes.find(treeChildPair.shadowView.tag); + auto subVisitedOtherNewIt = + (unvisitedIt == unvisitedOtherNodes.end() + ? subVisitedNewMap->find(treeChildPair.shadowView.tag) + : subVisitedNewMap->end()); + auto subVisitedOtherOldIt = + (unvisitedIt == unvisitedOtherNodes.end() && + subVisitedOtherNewIt == subVisitedNewMap->end() + ? subVisitedOldMap->find(treeChildPair.shadowView.tag) + : subVisitedOldMap->end()); + + bool existsInOtherTree = unvisitedIt != unvisitedOtherNodes.end() || + subVisitedOtherNewIt != subVisitedNewMap->end() || + subVisitedOtherOldIt != subVisitedOldMap->end(); + + auto otherTreeNodePairPtr = + (existsInOtherTree + ? (unvisitedIt != unvisitedOtherNodes.end() + ? unvisitedIt->second + : (subVisitedOtherNewIt != subVisitedNewMap->end() + ? subVisitedOtherNewIt->second + : subVisitedOtherOldIt->second)) + : nullptr); + + react_native_assert( + !existsInOtherTree || + (unvisitedIt != unvisitedOtherNodes.end() || + subVisitedOtherNewIt != subVisitedNewMap->end() || + subVisitedOtherOldIt != subVisitedOldMap->end())); + react_native_assert( + unvisitedIt == unvisitedOtherNodes.end() || + unvisitedIt->second->shadowView.tag == treeChildPair.shadowView.tag); + react_native_assert( + subVisitedOtherNewIt == subVisitedNewMap->end() || + subVisitedOtherNewIt->second->shadowView.tag == + treeChildPair.shadowView.tag); + react_native_assert( + subVisitedOtherOldIt == subVisitedOldMap->end() || + subVisitedOtherOldIt->second->shadowView.tag == + treeChildPair.shadowView.tag); + + bool alreadyUpdated = false; + + // Find in other tree and updated `otherTreePair` pointers + if (existsInOtherTree) { + react_native_assert(otherTreeNodePairPtr != nullptr); + auto newTreeNodePair = + (reparentMode == ReparentMode::Flatten ? otherTreeNodePairPtr + : &treeChildPair); + auto oldTreeNodePair = + (reparentMode == ReparentMode::Flatten ? &treeChildPair + : otherTreeNodePairPtr); + + react_native_assert(newTreeNodePair->shadowView.tag != 0); + react_native_assert(oldTreeNodePair->shadowView.tag != 0); + react_native_assert( + oldTreeNodePair->shadowView.tag == newTreeNodePair->shadowView.tag); + + alreadyUpdated = + newTreeNodePair->inOtherTree() || oldTreeNodePair->inOtherTree(); + + // We want to update these values unconditionally. Always do this + // before hitting any "continue" statements. + newTreeNodePair->otherTreePair = oldTreeNodePair; + oldTreeNodePair->otherTreePair = newTreeNodePair; + react_native_assert(treeChildPair.otherTreePair != nullptr); + } + + return FlattenerChildMatch{ + .existsInOtherTree = existsInOtherTree, + .otherTreeNodePairPtr = otherTreeNodePairPtr, + .alreadyUpdated = alreadyUpdated, + .foundInUnvisited = unvisitedIt != unvisitedOtherNodes.end(), + .foundInSubVisitedNew = subVisitedOtherNewIt != subVisitedNewMap->end(), + .foundInSubVisitedOld = subVisitedOtherOldIt != subVisitedOldMap->end()}; +} + /** * Here we flatten or unflatten a subtree, given an unflattened node in either * the old or new tree, and a list of flattened nodes in the other tree. @@ -467,451 +1074,73 @@ static void calculateShadowViewMutationsFlattener( index++) { auto& treeChildPair = *treeChildren[index]; - // Try to find node in other tree - auto unvisitedIt = unvisitedOtherNodes.find(treeChildPair.shadowView.tag); - auto subVisitedOtherNewIt = - (unvisitedIt == unvisitedOtherNodes.end() - ? subVisitedNewMap->find(treeChildPair.shadowView.tag) - : subVisitedNewMap->end()); - auto subVisitedOtherOldIt = - (unvisitedIt == unvisitedOtherNodes.end() && - subVisitedOtherNewIt == subVisitedNewMap->end() - ? subVisitedOldMap->find(treeChildPair.shadowView.tag) - : subVisitedOldMap->end()); - - bool existsInOtherTree = unvisitedIt != unvisitedOtherNodes.end() || - subVisitedOtherNewIt != subVisitedNewMap->end() || - subVisitedOtherOldIt != subVisitedOldMap->end(); - - auto otherTreeNodePairPtr = - (existsInOtherTree - ? (unvisitedIt != unvisitedOtherNodes.end() - ? unvisitedIt->second - : (subVisitedOtherNewIt != subVisitedNewMap->end() - ? subVisitedOtherNewIt->second - : subVisitedOtherOldIt->second)) - : nullptr); - - react_native_assert( - !existsInOtherTree || - (unvisitedIt != unvisitedOtherNodes.end() || - subVisitedOtherNewIt != subVisitedNewMap->end() || - subVisitedOtherOldIt != subVisitedOldMap->end())); - react_native_assert( - unvisitedIt == unvisitedOtherNodes.end() || - unvisitedIt->second->shadowView.tag == treeChildPair.shadowView.tag); - react_native_assert( - subVisitedOtherNewIt == subVisitedNewMap->end() || - subVisitedOtherNewIt->second->shadowView.tag == - treeChildPair.shadowView.tag); - react_native_assert( - subVisitedOtherOldIt == subVisitedOldMap->end() || - subVisitedOtherOldIt->second->shadowView.tag == - treeChildPair.shadowView.tag); - - bool alreadyUpdated = false; - - // Find in other tree and updated `otherTreePair` pointers - if (existsInOtherTree) { - react_native_assert(otherTreeNodePairPtr != nullptr); - auto newTreeNodePair = - (reparentMode == ReparentMode::Flatten ? otherTreeNodePairPtr - : &treeChildPair); - auto oldTreeNodePair = - (reparentMode == ReparentMode::Flatten ? &treeChildPair - : otherTreeNodePairPtr); - - react_native_assert(newTreeNodePair->shadowView.tag != 0); - react_native_assert(oldTreeNodePair->shadowView.tag != 0); - react_native_assert( - oldTreeNodePair->shadowView.tag == newTreeNodePair->shadowView.tag); - - alreadyUpdated = - newTreeNodePair->inOtherTree() || oldTreeNodePair->inOtherTree(); - - // We want to update these values unconditionally. Always do this - // before hitting any "continue" statements. - newTreeNodePair->otherTreePair = oldTreeNodePair; - oldTreeNodePair->otherTreePair = newTreeNodePair; - react_native_assert(treeChildPair.otherTreePair != nullptr); - } + // Try to find node in other tree (and wire up `otherTreePair` pointers). + auto match = findFlattenerChildMatch( + reparentMode, + treeChildPair, + unvisitedOtherNodes, + subVisitedNewMap, + subVisitedOldMap); // Remove all children (non-recursively) of tree being flattened, or // insert children into parent tree if they're being unflattened. // Caller will take care of the corresponding action in the other tree - // (caller will handle DELETE case if we REMOVE here; caller will handle - // CREATE case if we INSERT here). - if (treeChildPair.isConcreteView) { - if (reparentMode == ReparentMode::Flatten) { - // treeChildPair.shadowView represents the "old" view in this case. - // If there's a "new" view, an UPDATE new -> old will be generated - // and will be executed before the REMOVE. Thus, we must actually - // perform a REMOVE (new view) FROM (old index) in this case so that - // we don't hit asserts in StubViewTree's REMOVE path. - // We also only do this if the "other" (newer) view is concrete. If - // it's not concrete, there will be no UPDATE mutation. - react_native_assert(existsInOtherTree == treeChildPair.inOtherTree()); - if (treeChildPair.inOtherTree() && - treeChildPair.otherTreePair->isConcreteView) { - mutationContainer.removeMutations.push_back( - ShadowViewMutation::RemoveMutation( - node.shadowView.tag, - treeChildPair.otherTreePair->shadowView, - static_cast(treeChildPair.mountIndex))); - } else { - mutationContainer.removeMutations.push_back( - ShadowViewMutation::RemoveMutation( - node.shadowView.tag, - treeChildPair.shadowView, - static_cast(treeChildPair.mountIndex))); - } - } else { - // treeChildParent represents the "new" version of the node, so - // we can safely insert it without checking in the other tree - mutationContainer.insertMutations.push_back( - ShadowViewMutation::InsertMutation( - node.shadowView.tag, - treeChildPair.shadowView, - static_cast(treeChildPair.mountIndex))); - } - } - - // Find in other tree - if (existsInOtherTree) { - react_native_assert(otherTreeNodePairPtr != nullptr); - auto& otherTreeNodePair = *otherTreeNodePairPtr; - - auto& newTreeNodePair = - (reparentMode == ReparentMode::Flatten ? otherTreeNodePair - : treeChildPair); - auto& oldTreeNodePair = - (reparentMode == ReparentMode::Flatten ? treeChildPair - : otherTreeNodePair); - - react_native_assert(newTreeNodePair.shadowView.tag != 0); - react_native_assert(oldTreeNodePair.shadowView.tag != 0); - react_native_assert( - oldTreeNodePair.shadowView.tag == newTreeNodePair.shadowView.tag); - - // If we've already done updates, don't repeat it. - if (alreadyUpdated) { - continue; - } - - // If we've already done updates on this node, don't repeat. - if (reparentMode == ReparentMode::Flatten && - unvisitedIt == unvisitedOtherNodes.end() && - subVisitedOtherOldIt != subVisitedOldMap->end()) { - continue; - } else if ( - reparentMode == ReparentMode::Unflatten && - unvisitedIt == unvisitedOtherNodes.end() && - subVisitedOtherNewIt != subVisitedNewMap->end()) { - continue; - } - - // TODO: compare ShadowNode pointer instead of ShadowView here? - // Or ShadowNode ptr comparison before comparing ShadowView, to allow for - // short-circuiting? ShadowView comparison is relatively expensive vs - // ShadowNode. - if (newTreeNodePair.shadowView != oldTreeNodePair.shadowView && - newTreeNodePair.isConcreteView && oldTreeNodePair.isConcreteView) { - // We execute updates before creates, so pass the current parent in when - // unflattening. - // TODO: whenever we insert, we already update the relevant properties, - // so this update is redundant. We should remove this. - mutationContainer.updateMutations.push_back( - ShadowViewMutation::UpdateMutation( - oldTreeNodePair.shadowView, - newTreeNodePair.shadowView, - parentTagForUpdate)); - } - - auto adjustedOldCullingContext = reparentMode == ReparentMode::Flatten - ? cullingContext.adjustCullingContextIfNeeded(oldTreeNodePair) - : cullingContextForUnvisitedOtherNodes.adjustCullingContextIfNeeded( - oldTreeNodePair); - auto adjustedNewCullingContext = reparentMode == ReparentMode::Flatten - ? cullingContextForUnvisitedOtherNodes.adjustCullingContextIfNeeded( - newTreeNodePair) - : cullingContext.adjustCullingContextIfNeeded(newTreeNodePair); - - // Update children if appropriate. - if (!oldTreeNodePair.flattened && !newTreeNodePair.flattened) { - if (oldTreeNodePair.shadowNode != newTreeNodePair.shadowNode || - adjustedOldCullingContext != adjustedNewCullingContext) { - ViewNodePairScope innerScope{}; - auto oldGrandChildPairs = - sliceChildShadowNodeViewPairsFromViewNodePair( - oldTreeNodePair, - innerScope, - false, - adjustedOldCullingContext); - auto newGrandChildPairs = - sliceChildShadowNodeViewPairsFromViewNodePair( - newTreeNodePair, - innerScope, - false, - adjustedNewCullingContext); - - calculateShadowViewMutations( - innerScope, - mutationContainer.downwardMutations, - newTreeNodePair.shadowView.tag, - std::move(oldGrandChildPairs), - std::move(newGrandChildPairs), - adjustedOldCullingContext, - adjustedNewCullingContext); - } - } else if (oldTreeNodePair.flattened != newTreeNodePair.flattened) { - // We need to handle one of the children being flattened or - // unflattened, in the context of a parent flattening or unflattening. - ReparentMode childReparentMode = - (oldTreeNodePair.flattened ? ReparentMode::Unflatten - : ReparentMode::Flatten); - - // Case 1: child mode is the same as parent. - // This is a flatten-flatten, or unflatten-unflatten. - if (childReparentMode == reparentMode) { - calculateShadowViewMutationsFlattener( - scope, - childReparentMode, - mutationContainer, - (reparentMode == ReparentMode::Flatten - ? parentTag - : newTreeNodePair.shadowView.tag), - unvisitedOtherNodes, - treeChildPair, - (reparentMode == ReparentMode::Flatten - ? oldTreeNodePair.shadowView.tag - : (ReactNativeFeatureFlags:: - fixDifferentiatorParentTagForUnflattenCase() - ? parentTagForUpdate - : parentTag)), - subVisitedNewMap, - subVisitedOldMap, - cullingContextForUnvisitedOtherNodes, - cullingContext.adjustCullingContextIfNeeded(treeChildPair)); - } else { - // Get flattened nodes from either new or old tree - auto flattenedNodes = sliceChildShadowNodeViewPairsFromViewNodePair( - (childReparentMode == ReparentMode::Flatten ? newTreeNodePair - : oldTreeNodePair), - scope, - true, - childReparentMode == ReparentMode::Flatten - ? adjustedNewCullingContext - : adjustedOldCullingContext); - // Construct unvisited nodes map - std::unordered_map - unvisitedRecursiveChildPairs; - unvisitedRecursiveChildPairs.reserve(flattenedNodes.size()); - for (auto& flattenedNode : flattenedNodes) { - auto& newChild = *flattenedNode; - - auto unvisitedOtherNodesIt = - unvisitedOtherNodes.find(newChild.shadowView.tag); - if (unvisitedOtherNodesIt != unvisitedOtherNodes.end()) { - auto unvisitedItPair = *unvisitedOtherNodesIt->second; - unvisitedRecursiveChildPairs.insert( - {unvisitedItPair.shadowView.tag, &unvisitedItPair}); - } else { - unvisitedRecursiveChildPairs.insert( - {newChild.shadowView.tag, &newChild}); - } - } - - if (childReparentMode == ReparentMode::Flatten) { - // Unflatten parent, flatten child - react_native_assert(reparentMode == ReparentMode::Unflatten); - auto fixedParentTagForUpdate = newTreeNodePair.shadowView.tag; - // Flatten old tree into new list - // At the end of this loop we still want to know which of these - // children are visited, so we reuse the `newRemainingPairs` map. - calculateShadowViewMutationsFlattener( - scope, - ReparentMode::Flatten, - mutationContainer, - newTreeNodePair.shadowView.tag, - unvisitedRecursiveChildPairs, - oldTreeNodePair, - fixedParentTagForUpdate, - subVisitedNewMap, - subVisitedOldMap, - adjustedNewCullingContext, - adjustedNewCullingContext); - } else { - // Flatten parent, unflatten child - react_native_assert(reparentMode == ReparentMode::Flatten); - // Unflatten old list into new tree - auto fixedParentTagForUpdate = parentTagForUpdate; - calculateShadowViewMutationsFlattener( - scope, - /* reparentMode */ ReparentMode::Unflatten, - mutationContainer, - parentTag, - /* unvisitedOtherNodes */ unvisitedRecursiveChildPairs, - /* node */ newTreeNodePair, - /* parentTagForUpdate */ fixedParentTagForUpdate, - /* parentSubVisitedOtherNewNodes */ subVisitedNewMap, - /* parentSubVisitedOtherOldNodes */ subVisitedOldMap, - /* cullingContextForUnvisitedOtherNodes */ - adjustedOldCullingContext, - /* cullingContext */ adjustedOldCullingContext); - - // If old nodes were not visited, we know that we can delete them - // now. They will be removed from the hierarchy by the outermost - // loop of this function. - for (auto& unvisitedRecursiveChildPair : - unvisitedRecursiveChildPairs) { - auto& oldFlattenedNode = *unvisitedRecursiveChildPair.second; - - // Node unvisited - mark the entire subtree for deletion - if (oldFlattenedNode.isConcreteView && - !oldFlattenedNode.inOtherTree()) { - Tag tag = oldFlattenedNode.shadowView.tag; - auto deleteCreateIt = deletionCreationCandidatePairs.find( - oldFlattenedNode.shadowView.tag); - if (deleteCreateIt == deletionCreationCandidatePairs.end()) { - deletionCreationCandidatePairs.insert( - {tag, &oldFlattenedNode}); - } - } else { - // Node was visited - make sure to remove it from - // "newRemainingPairs" map - auto newRemainingIt = - unvisitedOtherNodes.find(oldFlattenedNode.shadowView.tag); - if (newRemainingIt != unvisitedOtherNodes.end()) { - unvisitedOtherNodes.erase(newRemainingIt); - } - } - } - } - } - } - - // Mark that node exists in another tree, but only if the tree node is a - // concrete view. Removing the node from the unvisited list prevents the - // caller from taking further action on this node, so make sure to - // delete/create if the Concreteness of the node has changed. - if (newTreeNodePair.isConcreteView != oldTreeNodePair.isConcreteView) { - if (newTreeNodePair.isConcreteView) { - mutationContainer.createMutations.push_back( - ShadowViewMutation::CreateMutation(newTreeNodePair.shadowView)); - } else { - mutationContainer.deleteMutations.push_back( - ShadowViewMutation::DeleteMutation(oldTreeNodePair.shadowView)); - } - } - - subVisitedNewMap->insert( - {newTreeNodePair.shadowView.tag, &newTreeNodePair}); - subVisitedOldMap->insert( - {oldTreeNodePair.shadowView.tag, &oldTreeNodePair}); - } else { - // Node does not in exist in other tree. - if (treeChildPair.isConcreteView && !treeChildPair.inOtherTree()) { - auto deletionCreationIt = - deletionCreationCandidatePairs.find(treeChildPair.shadowView.tag); - if (deletionCreationIt == deletionCreationCandidatePairs.end()) { - deletionCreationCandidatePairs.insert( - {treeChildPair.shadowView.tag, &treeChildPair}); - } - } - } - } - - // Final step: go through creation/deletion candidates and delete/create - // subtrees if they were never visited during the execution of the above - // loop and recursions. - for (auto& deletionCreationCandidatePair : deletionCreationCandidatePairs) { - auto& treeChildPair = *deletionCreationCandidatePair.second; - - // If node was visited during a flattening/unflattening recursion, - // and the node in the other tree is concrete, that means it was - // already created/deleted and we don't need to do that here. - // It is always the responsibility of the matcher to update subtrees when - // nodes are matched. - if (treeChildPair.inOtherTree()) { - continue; - } - - auto adjustedCullingContext = - cullingContext.adjustCullingContextIfNeeded(treeChildPair); - - if (reparentMode == ReparentMode::Flatten) { - mutationContainer.deleteMutations.push_back( - ShadowViewMutation::DeleteMutation(treeChildPair.shadowView)); - - if (!treeChildPair.flattened) { - ViewNodePairScope innerScope{}; - calculateShadowViewMutations( - innerScope, - mutationContainer.destructiveDownwardMutations, - treeChildPair.shadowView.tag, - sliceChildShadowNodeViewPairsFromViewNodePair( - treeChildPair, innerScope, false, adjustedCullingContext), - {}, - adjustedCullingContext, - {}); - } - } else { - mutationContainer.createMutations.push_back( - ShadowViewMutation::CreateMutation(treeChildPair.shadowView)); - - if (!treeChildPair.flattened) { - ViewNodePairScope innerScope{}; - calculateShadowViewMutations( - innerScope, - mutationContainer.downwardMutations, - treeChildPair.shadowView.tag, - {}, - sliceChildShadowNodeViewPairsFromViewNodePair( - treeChildPair, innerScope, false, adjustedCullingContext), - {}, - adjustedCullingContext); - } - } - } -} - -static void calculateShadowViewMutations( - ViewNodePairScope& scope, - ShadowViewMutation::List& mutations, - Tag parentTag, - std::vector&& oldChildPairs, - std::vector&& newChildPairs, - const CullingContext& oldCullingContext, - const CullingContext& newCullingContext) { - if (oldChildPairs.empty() && newChildPairs.empty()) { - return; - } - - size_t index = 0; - - // Lists of mutations - auto mutationContainer = OrderedMutationInstructionContainer{}; - - if (ReactNativeFeatureFlags:: - enableDifferentiatorMutationVectorPreallocation()) { - // Pre-allocate mutation sub-vectors based on expected child count to avoid - // repeated reallocations during diffing. - size_t estimatedSize = std::max(oldChildPairs.size(), newChildPairs.size()); - mutationContainer.createMutations.reserve(estimatedSize); - mutationContainer.deleteMutations.reserve(estimatedSize); - mutationContainer.insertMutations.reserve(estimatedSize); - mutationContainer.removeMutations.reserve(estimatedSize); - mutationContainer.updateMutations.reserve(estimatedSize); - mutationContainer.downwardMutations.reserve(estimatedSize); - mutationContainer.destructiveDownwardMutations.reserve(estimatedSize); + // (caller will handle DELETE case if we REMOVE here; caller will handle + // CREATE case if we INSERT here). + calculateShadowViewMutationsFlattenerRemoveOrInsertChild( + mutationContainer, + reparentMode, + node, + treeChildPair, + match.existsInOtherTree); + + // Find in other tree: reconcile the matched pair and its subtree, or mark + // the node as a deletion/creation candidate. + calculateShadowViewMutationsFlattenerMatchChild( + scope, + reparentMode, + mutationContainer, + parentTag, + unvisitedOtherNodes, + treeChildPair, + parentTagForUpdate, + subVisitedNewMap, + subVisitedOldMap, + cullingContextForUnvisitedOtherNodes, + cullingContext, + deletionCreationCandidatePairs, + match.existsInOtherTree, + match.otherTreeNodePairPtr, + match.alreadyUpdated, + match.foundInUnvisited, + match.foundInSubVisitedNew, + match.foundInSubVisitedOld); } - DEBUG_LOGS({ - LOG(ERROR) << "Differ Entry: Child Pairs of node: [" << parentTag << "]"; - LOG(ERROR) << "> Old Child Pairs: " << oldChildPairs; - LOG(ERROR) << "> New Child Pairs: " << newChildPairs; - }); + // Final step: go through creation/deletion candidates and delete/create + // subtrees if they were never visited during the execution of the above + // loop and recursions. + calculateShadowViewMutationsFlattenerProcessCandidates( + mutationContainer, + reparentMode, + deletionCreationCandidatePairs, + cullingContext); +} +/** + * Stage 1 of `calculateShadowViewMutations`: collect `Update` mutations for the + * common prefix where old/new children share tags and (un)flattening hasn't + * changed, recursing into their subtrees. Returns the index at which stage 1 + * stopped. Extracted to reduce complexity; behavior is unchanged. + */ +static size_t calculateShadowViewMutationsStage1( + OrderedMutationInstructionContainer& mutationContainer, + Tag parentTag, + const std::vector& oldChildPairs, + const std::vector& newChildPairs, + const CullingContext& oldCullingContext, + const CullingContext& newCullingContext) { + size_t index = 0; // Stage 1: Collecting `Update` mutations for (index = 0; index < oldChildPairs.size() && index < newChildPairs.size(); index++) { @@ -979,364 +1208,479 @@ static void calculateShadowViewMutations( adjustedNewCullingContext); } } + return index; +} - size_t lastIndexAfterFirstStage = index; +/** + * Branch 2 of `calculateShadowViewMutations`: we've reached the end of the new + * children, so delete+remove the remaining old children and their subtrees. + * Extracted to reduce complexity; behavior is unchanged. + */ +static void calculateShadowViewMutationsRemoveRemaining( + OrderedMutationInstructionContainer& mutationContainer, + Tag parentTag, + const std::vector& oldChildPairs, + size_t startIndex, + const CullingContext& oldCullingContext, + const CullingContext& newCullingContext) { + for (size_t index = startIndex; index < oldChildPairs.size(); index++) { + const auto& oldChildPair = *oldChildPairs[index]; - if (index == newChildPairs.size()) { - // We've reached the end of the new children. We can delete+remove the - // rest. - for (; index < oldChildPairs.size(); index++) { - const auto& oldChildPair = *oldChildPairs[index]; + DEBUG_LOGS({ + LOG(ERROR) << "Differ Branch 2: Deleting Tag/Tree: " << oldChildPair + << " with parent: [" << parentTag << "]"; + }); - DEBUG_LOGS({ - LOG(ERROR) << "Differ Branch 2: Deleting Tag/Tree: " << oldChildPair - << " with parent: [" << parentTag << "]"; - }); + if (!oldChildPair.isConcreteView) { + continue; + } - if (!oldChildPair.isConcreteView) { - continue; - } + mutationContainer.deleteMutations.push_back( + ShadowViewMutation::DeleteMutation(oldChildPair.shadowView)); + mutationContainer.removeMutations.push_back( + ShadowViewMutation::RemoveMutation( + parentTag, + oldChildPair.shadowView, + static_cast(oldChildPair.mountIndex))); + auto oldCullingContextCopy = + oldCullingContext.adjustCullingContextIfNeeded(oldChildPair); - mutationContainer.deleteMutations.push_back( - ShadowViewMutation::DeleteMutation(oldChildPair.shadowView)); - mutationContainer.removeMutations.push_back( - ShadowViewMutation::RemoveMutation( - parentTag, - oldChildPair.shadowView, - static_cast(oldChildPair.mountIndex))); - auto oldCullingContextCopy = - oldCullingContext.adjustCullingContextIfNeeded(oldChildPair); + // We also have to call the algorithm recursively to clean up the entire + // subtree starting from the removed view. + ViewNodePairScope innerScope{}; + calculateShadowViewMutations( + innerScope, + mutationContainer.destructiveDownwardMutations, + oldChildPair.shadowView.tag, + sliceChildShadowNodeViewPairsFromViewNodePair( + oldChildPair, innerScope, false, oldCullingContextCopy), + {}, + oldCullingContextCopy, + newCullingContext); + } +} - // We also have to call the algorithm recursively to clean up the entire - // subtree starting from the removed view. - ViewNodePairScope innerScope{}; - calculateShadowViewMutations( - innerScope, - mutationContainer.destructiveDownwardMutations, - oldChildPair.shadowView.tag, - sliceChildShadowNodeViewPairsFromViewNodePair( - oldChildPair, innerScope, false, oldCullingContextCopy), - {}, - oldCullingContextCopy, - newCullingContext); +/** + * Branch 3 of `calculateShadowViewMutations`: we have no more existing + * children, so create+insert the remaining new children and their subtrees. + * Extracted to reduce complexity; behavior is unchanged. + */ +static void calculateShadowViewMutationsCreateRemaining( + OrderedMutationInstructionContainer& mutationContainer, + Tag parentTag, + const std::vector& newChildPairs, + size_t startIndex, + const CullingContext& oldCullingContext, + const CullingContext& newCullingContext) { + for (size_t index = startIndex; index < newChildPairs.size(); index++) { + const auto& newChildPair = *newChildPairs[index]; + + DEBUG_LOGS({ + LOG(ERROR) << "Differ Branch 3: Creating Tag/Tree: " << newChildPair + << " with parent: [" << parentTag << "]"; + }); + + if (!newChildPair.isConcreteView) { + continue; } - } else if (index == oldChildPairs.size()) { - // If we don't have any more existing children we can choose a fast path - // since the rest will all be create+insert. - for (; index < newChildPairs.size(); index++) { - const auto& newChildPair = *newChildPairs[index]; - DEBUG_LOGS({ - LOG(ERROR) << "Differ Branch 3: Creating Tag/Tree: " << newChildPair - << " with parent: [" << parentTag << "]"; - }); + mutationContainer.insertMutations.push_back( + ShadowViewMutation::InsertMutation( + parentTag, + newChildPair.shadowView, + static_cast(newChildPair.mountIndex))); + mutationContainer.createMutations.push_back( + ShadowViewMutation::CreateMutation(newChildPair.shadowView)); + auto newCullingContextCopy = + newCullingContext.adjustCullingContextIfNeeded(newChildPair); - if (!newChildPair.isConcreteView) { - continue; - } + ViewNodePairScope innerScope{}; + calculateShadowViewMutations( + innerScope, + mutationContainer.downwardMutations, + newChildPair.shadowView.tag, + {}, + sliceChildShadowNodeViewPairsFromViewNodePair( + newChildPair, innerScope, false, newCullingContextCopy), + oldCullingContext, + newCullingContextCopy); + } +} - mutationContainer.insertMutations.push_back( - ShadowViewMutation::InsertMutation( - parentTag, - newChildPair.shadowView, - static_cast(newChildPair.mountIndex))); - mutationContainer.createMutations.push_back( - ShadowViewMutation::CreateMutation(newChildPair.shadowView)); - auto newCullingContextCopy = - newCullingContext.adjustCullingContextIfNeeded(newChildPair); +/** + * Greedy Stage 4 of `calculateShadowViewMutations`: walk both remaining lists + * simultaneously, performing updates, create+insert, remove+delete, and + * remove+insert (move), then emit the deferred deletes and creates. Extracted + * to reduce complexity; behavior is unchanged. + */ +static void calculateShadowViewMutationsGreedyStage4( + ViewNodePairScope& scope, + OrderedMutationInstructionContainer& mutationContainer, + Tag parentTag, + std::vector& oldChildPairs, + std::vector& newChildPairs, + size_t lastIndexAfterFirstStage, + const CullingContext& oldCullingContext, + const CullingContext& newCullingContext) { + // Greedy Stage 4 algorithm. + // Collect map of tags in the new list + auto remainingCount = newChildPairs.size() - lastIndexAfterFirstStage; + std::unordered_map newRemainingPairs; + newRemainingPairs.reserve(remainingCount); + std::unordered_map newInsertedPairs; + newInsertedPairs.reserve(remainingCount); + std::unordered_map deletionCandidatePairs{}; + for (size_t index = lastIndexAfterFirstStage; index < newChildPairs.size(); + index++) { + auto& newChildPair = *newChildPairs[index]; + newRemainingPairs.insert({newChildPair.shadowView.tag, &newChildPair}); + } - ViewNodePairScope innerScope{}; - calculateShadowViewMutations( - innerScope, - mutationContainer.downwardMutations, - newChildPair.shadowView.tag, - {}, - sliceChildShadowNodeViewPairsFromViewNodePair( - newChildPair, innerScope, false, newCullingContextCopy), - oldCullingContext, - newCullingContextCopy); - } - } else { - // Greedy Stage 4 algorithm. - // Collect map of tags in the new list - auto remainingCount = newChildPairs.size() - lastIndexAfterFirstStage; - std::unordered_map newRemainingPairs; - newRemainingPairs.reserve(remainingCount); - std::unordered_map newInsertedPairs; - newInsertedPairs.reserve(remainingCount); - std::unordered_map deletionCandidatePairs{}; - for (index = lastIndexAfterFirstStage; index < newChildPairs.size(); - index++) { - auto& newChildPair = *newChildPairs[index]; - newRemainingPairs.insert({newChildPair.shadowView.tag, &newChildPair}); + // Walk through both lists at the same time + // We will perform updates, create+insert, remove+delete, remove+insert + // (move) here. + size_t oldIndex = lastIndexAfterFirstStage; + size_t newIndex = lastIndexAfterFirstStage; + size_t newSize = newChildPairs.size(); + size_t oldSize = oldChildPairs.size(); + while (newIndex < newSize || oldIndex < oldSize) { + bool haveNewPair = newIndex < newSize; + bool haveOldPair = oldIndex < oldSize; + + // Advance both pointers if pointing to the same element + if (haveNewPair && haveOldPair) { + const auto& oldChildPair = *oldChildPairs[oldIndex]; + const auto& newChildPair = *newChildPairs[newIndex]; + + Tag newTag = newChildPair.shadowView.tag; + Tag oldTag = oldChildPair.shadowView.tag; + + if (newTag == oldTag) { + DEBUG_LOGS({ + LOG(ERROR) << "Differ Branch 4: Matched Tags at indices: " << oldIndex + << " and " << newIndex << ": " << oldChildPair << " and " + << newChildPair << " with parent: [" << parentTag << "]"; + }); + + updateMatchedPair( + mutationContainer, + true, + true, + parentTag, + oldChildPair, + newChildPair); + + updateMatchedPairSubtrees( + scope, + mutationContainer, + newRemainingPairs, + oldChildPairs, + parentTag, + oldChildPair, + newChildPair, + oldCullingContext, + newCullingContext); + + newIndex++; + oldIndex++; + continue; + } } - // Walk through both lists at the same time - // We will perform updates, create+insert, remove+delete, remove+insert - // (move) here. - size_t oldIndex = lastIndexAfterFirstStage; - size_t newIndex = lastIndexAfterFirstStage; - size_t newSize = newChildPairs.size(); - size_t oldSize = oldChildPairs.size(); - while (newIndex < newSize || oldIndex < oldSize) { - bool haveNewPair = newIndex < newSize; - bool haveOldPair = oldIndex < oldSize; - - // Advance both pointers if pointing to the same element - if (haveNewPair && haveOldPair) { - const auto& oldChildPair = *oldChildPairs[oldIndex]; - const auto& newChildPair = *newChildPairs[newIndex]; - - Tag newTag = newChildPair.shadowView.tag; - Tag oldTag = oldChildPair.shadowView.tag; - - if (newTag == oldTag) { - DEBUG_LOGS({ - LOG(ERROR) << "Differ Branch 4: Matched Tags at indices: " - << oldIndex << " and " << newIndex << ": " - << oldChildPair << " and " << newChildPair - << " with parent: [" << parentTag << "]"; - }); - - updateMatchedPair( - mutationContainer, - true, - true, - parentTag, - oldChildPair, - newChildPair); - - updateMatchedPairSubtrees( - scope, - mutationContainer, - newRemainingPairs, - oldChildPairs, - parentTag, - oldChildPair, - newChildPair, - oldCullingContext, - newCullingContext); + // We have an old pair, but we either don't have any remaining new pairs + // or we have one but it's not matched up with the old pair + if (haveOldPair) { + const auto& oldChildPair = *oldChildPairs[oldIndex]; + + Tag oldTag = oldChildPair.shadowView.tag; + + // Was oldTag already inserted? This indicates a reordering, not just + // a move. The new node has already been inserted, we just need to + // remove the node from its old position now, and update the node's + // subtree. + const auto insertedIt = newInsertedPairs.find(oldTag); + if (insertedIt != newInsertedPairs.end()) { + const auto& newChildPair = *insertedIt->second; + + DEBUG_LOGS({ + LOG(ERROR) << "Differ Branch 5: Founded reordered tags at indices: " + << oldIndex << ": " << oldChildPair << " and " + << newChildPair << " with parent: [" << parentTag << "]"; + }); + + updateMatchedPair( + mutationContainer, + true, + false, + parentTag, + oldChildPair, + newChildPair); + + updateMatchedPairSubtrees( + scope, + mutationContainer, + newRemainingPairs, + oldChildPairs, + parentTag, + oldChildPair, + newChildPair, + oldCullingContext, + newCullingContext); - newIndex++; - oldIndex++; - continue; - } + newInsertedPairs.erase(insertedIt); + oldIndex++; + continue; } - // We have an old pair, but we either don't have any remaining new pairs - // or we have one but it's not matched up with the old pair - if (haveOldPair) { - const auto& oldChildPair = *oldChildPairs[oldIndex]; - - Tag oldTag = oldChildPair.shadowView.tag; - - // Was oldTag already inserted? This indicates a reordering, not just - // a move. The new node has already been inserted, we just need to - // remove the node from its old position now, and update the node's - // subtree. - const auto insertedIt = newInsertedPairs.find(oldTag); - if (insertedIt != newInsertedPairs.end()) { - const auto& newChildPair = *insertedIt->second; - - DEBUG_LOGS({ - LOG(ERROR) << "Differ Branch 5: Founded reordered tags at indices: " - << oldIndex << ": " << oldChildPair << " and " - << newChildPair << " with parent: [" << parentTag << "]"; - }); - - updateMatchedPair( - mutationContainer, - true, - false, - parentTag, - oldChildPair, - newChildPair); - - updateMatchedPairSubtrees( - scope, - mutationContainer, - newRemainingPairs, - oldChildPairs, - parentTag, - oldChildPair, - newChildPair, - oldCullingContext, - newCullingContext); + // Should we generate a delete+remove instruction for the old node? + // If there's an old node and it's not found in the "new" list, we + // generate remove+delete for this node and its subtree. + const auto newIt = newRemainingPairs.find(oldTag); + if (newIt == newRemainingPairs.end()) { + oldIndex++; - newInsertedPairs.erase(insertedIt); - oldIndex++; + if (!oldChildPair.isConcreteView) { continue; } - // Should we generate a delete+remove instruction for the old node? - // If there's an old node and it's not found in the "new" list, we - // generate remove+delete for this node and its subtree. - const auto newIt = newRemainingPairs.find(oldTag); - if (newIt == newRemainingPairs.end()) { - oldIndex++; - - if (!oldChildPair.isConcreteView) { - continue; - } - - // From here, we know the oldChildPair is concrete. - // We *probably* need to generate a REMOVE mutation (see edge-case - // notes below). - - DEBUG_LOGS({ - LOG(ERROR) - << "Differ Branch 6: Removing tag that was not re-inserted: " - << oldChildPair << " with parent: [" << parentTag - << "], which is " << (oldChildPair.inOtherTree() ? "" : "not ") - << "in other tree"; - }); - - // Edge case: node is not found in `newRemainingPairs`, due to - // complex (un)flattening cases, but exists in other tree *and* is - // concrete. - if (oldChildPair.inOtherTree() && - oldChildPair.otherTreePair->isConcreteView) { - const ShadowView& otherTreeView = - oldChildPair.otherTreePair->shadowView; - - // Remove, but remove using the *new* node, since we know - // an UPDATE mutation from old -> new has been generated. - // Practically this shouldn't matter for most mounting layer - // implementations, but helps adhere to the invariant that - // for all mutation instructions, "oldViewShadowNode" == "current - // node on mounting layer / stubView". - // Here we do *not" need to generate a potential DELETE mutation - // because we know the view is concrete, and still in the new - // hierarchy. - mutationContainer.removeMutations.push_back( - ShadowViewMutation::RemoveMutation( - parentTag, - otherTreeView, - static_cast(oldChildPair.mountIndex))); - continue; - } - + // From here, we know the oldChildPair is concrete. + // We *probably* need to generate a REMOVE mutation (see edge-case + // notes below). + + DEBUG_LOGS({ + LOG(ERROR) + << "Differ Branch 6: Removing tag that was not re-inserted: " + << oldChildPair << " with parent: [" << parentTag + << "], which is " << (oldChildPair.inOtherTree() ? "" : "not ") + << "in other tree"; + }); + + // Edge case: node is not found in `newRemainingPairs`, due to + // complex (un)flattening cases, but exists in other tree *and* is + // concrete. + if (oldChildPair.inOtherTree() && + oldChildPair.otherTreePair->isConcreteView) { + const ShadowView& otherTreeView = + oldChildPair.otherTreePair->shadowView; + + // Remove, but remove using the *new* node, since we know + // an UPDATE mutation from old -> new has been generated. + // Practically this shouldn't matter for most mounting layer + // implementations, but helps adhere to the invariant that + // for all mutation instructions, "oldViewShadowNode" == "current + // node on mounting layer / stubView". + // Here we do *not" need to generate a potential DELETE mutation + // because we know the view is concrete, and still in the new + // hierarchy. mutationContainer.removeMutations.push_back( ShadowViewMutation::RemoveMutation( parentTag, - oldChildPair.shadowView, + otherTreeView, static_cast(oldChildPair.mountIndex))); - - deletionCandidatePairs.insert( - {oldChildPair.shadowView.tag, &oldChildPair}); - continue; } - } - // At this point, oldTag is -1 or is in the new list, and hasn't been - // inserted or matched yet. We're not sure yet if the new node is in the - // old list - generate an insert instruction for the new node. - auto& newChildPair = *newChildPairs[newIndex]; - DEBUG_LOGS({ - LOG(ERROR) - << "Differ Branch 7: Inserting tag/tree that was not (yet?) removed from hierarchy: " - << newChildPair << " @ " << newIndex << "/" << newSize - << " with parent: [" << parentTag << "]"; - }); - if (newChildPair.isConcreteView) { - mutationContainer.insertMutations.push_back( - ShadowViewMutation::InsertMutation( + mutationContainer.removeMutations.push_back( + ShadowViewMutation::RemoveMutation( parentTag, - newChildPair.shadowView, - static_cast(newChildPair.mountIndex))); - } + oldChildPair.shadowView, + static_cast(oldChildPair.mountIndex))); - // `inOtherTree` is only set to true during flattening/unflattening of - // parent. If the parent isn't (un)flattened, this will always be - // `false`, even if the node is in the other (old) tree. In this case, - // we expect the node to be removed from `newInsertedPairs` when we - // later encounter it in this loop. - if (!newChildPair.inOtherTree()) { - newInsertedPairs.insert({newChildPair.shadowView.tag, &newChildPair}); - } + deletionCandidatePairs.insert( + {oldChildPair.shadowView.tag, &oldChildPair}); - newIndex++; + continue; + } } - // Penultimate step: generate Delete instructions for entirely deleted - // subtrees/nodes. We do this here because we need to traverse the entire - // list to make sure that a node was not reparented into an unflattened - // node that occurs *after* it in the hierarchy, due to zIndex ordering. - for (auto& deletionCandidatePair : deletionCandidatePairs) { - const auto& oldChildPair = *deletionCandidatePair.second; - - DEBUG_LOGS({ - LOG(ERROR) - << "Differ Branch 8: Deleting tag/tree that was not in new hierarchy: " - << oldChildPair - << (oldChildPair.inOtherTree() ? "(in other tree)" : "") - << " with parent: [" << parentTag << "] ##" - << std::hash{}(oldChildPair.shadowView); - }); - - // This can happen when the parent is unflattened - if (!oldChildPair.inOtherTree() && oldChildPair.isConcreteView) { - mutationContainer.deleteMutations.push_back( - ShadowViewMutation::DeleteMutation(oldChildPair.shadowView)); - auto oldCullingContextCopy = - oldCullingContext.adjustCullingContextIfNeeded(oldChildPair); - - // We also have to call the algorithm recursively to clean up the - // entire subtree starting from the removed view. - ViewNodePairScope innerScope{}; - - auto newGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( - oldChildPair, innerScope, false, oldCullingContextCopy); - calculateShadowViewMutations( - innerScope, - mutationContainer.destructiveDownwardMutations, - oldChildPair.shadowView.tag, - std::move(newGrandChildPairs), - {}, - oldCullingContextCopy, - newCullingContext); - } + // At this point, oldTag is -1 or is in the new list, and hasn't been + // inserted or matched yet. We're not sure yet if the new node is in the + // old list - generate an insert instruction for the new node. + auto& newChildPair = *newChildPairs[newIndex]; + DEBUG_LOGS({ + LOG(ERROR) + << "Differ Branch 7: Inserting tag/tree that was not (yet?) removed from hierarchy: " + << newChildPair << " @ " << newIndex << "/" << newSize + << " with parent: [" << parentTag << "]"; + }); + if (newChildPair.isConcreteView) { + mutationContainer.insertMutations.push_back( + ShadowViewMutation::InsertMutation( + parentTag, + newChildPair.shadowView, + static_cast(newChildPair.mountIndex))); } - // Final step: generate Create instructions for entirely new - // subtrees/nodes that are not the result of flattening or unflattening. - for (auto& newInsertedPair : newInsertedPairs) { - const auto& newChildPair = *newInsertedPair.second; + // `inOtherTree` is only set to true during flattening/unflattening of + // parent. If the parent isn't (un)flattened, this will always be + // `false`, even if the node is in the other (old) tree. In this case, + // we expect the node to be removed from `newInsertedPairs` when we + // later encounter it in this loop. + if (!newChildPair.inOtherTree()) { + newInsertedPairs.insert({newChildPair.shadowView.tag, &newChildPair}); + } - DEBUG_LOGS({ - LOG(ERROR) - << "Differ Branch 9: Inserting tag/tree that was not in old hierarchy: " - << newChildPair - << (newChildPair.inOtherTree() ? "(in other tree)" : "") - << " with parent: [" << parentTag << "]"; - }); + newIndex++; + } - if (!newChildPair.isConcreteView) { - continue; - } - if (newChildPair.inOtherTree()) { - continue; - } + // Penultimate step: generate Delete instructions for entirely deleted + // subtrees/nodes. We do this here because we need to traverse the entire + // list to make sure that a node was not reparented into an unflattened + // node that occurs *after* it in the hierarchy, due to zIndex ordering. + for (auto& deletionCandidatePair : deletionCandidatePairs) { + const auto& oldChildPair = *deletionCandidatePair.second; - mutationContainer.createMutations.push_back( - ShadowViewMutation::CreateMutation(newChildPair.shadowView)); + DEBUG_LOGS({ + LOG(ERROR) + << "Differ Branch 8: Deleting tag/tree that was not in new hierarchy: " + << oldChildPair + << (oldChildPair.inOtherTree() ? "(in other tree)" : "") + << " with parent: [" << parentTag << "] ##" + << std::hash{}(oldChildPair.shadowView); + }); - auto newCullingContextCopy = - newCullingContext.adjustCullingContextIfNeeded(newChildPair); + // This can happen when the parent is unflattened + if (!oldChildPair.inOtherTree() && oldChildPair.isConcreteView) { + mutationContainer.deleteMutations.push_back( + ShadowViewMutation::DeleteMutation(oldChildPair.shadowView)); + auto oldCullingContextCopy = + oldCullingContext.adjustCullingContextIfNeeded(oldChildPair); + // We also have to call the algorithm recursively to clean up the + // entire subtree starting from the removed view. ViewNodePairScope innerScope{}; + auto newGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( + oldChildPair, innerScope, false, oldCullingContextCopy); calculateShadowViewMutations( innerScope, - mutationContainer.downwardMutations, - newChildPair.shadowView.tag, + mutationContainer.destructiveDownwardMutations, + oldChildPair.shadowView.tag, + std::move(newGrandChildPairs), {}, - sliceChildShadowNodeViewPairsFromViewNodePair( - newChildPair, innerScope, false, newCullingContextCopy), - oldCullingContext, - newCullingContextCopy); + oldCullingContextCopy, + newCullingContext); + } + } + + // Final step: generate Create instructions for entirely new + // subtrees/nodes that are not the result of flattening or unflattening. + for (auto& newInsertedPair : newInsertedPairs) { + const auto& newChildPair = *newInsertedPair.second; + + DEBUG_LOGS({ + LOG(ERROR) + << "Differ Branch 9: Inserting tag/tree that was not in old hierarchy: " + << newChildPair + << (newChildPair.inOtherTree() ? "(in other tree)" : "") + << " with parent: [" << parentTag << "]"; + }); + + if (!newChildPair.isConcreteView) { + continue; + } + if (newChildPair.inOtherTree()) { + continue; } + + mutationContainer.createMutations.push_back( + ShadowViewMutation::CreateMutation(newChildPair.shadowView)); + + auto newCullingContextCopy = + newCullingContext.adjustCullingContextIfNeeded(newChildPair); + + ViewNodePairScope innerScope{}; + + calculateShadowViewMutations( + innerScope, + mutationContainer.downwardMutations, + newChildPair.shadowView.tag, + {}, + sliceChildShadowNodeViewPairsFromViewNodePair( + newChildPair, innerScope, false, newCullingContextCopy), + oldCullingContext, + newCullingContextCopy); + } +} + +static void calculateShadowViewMutations( + ViewNodePairScope& scope, + ShadowViewMutation::List& mutations, + Tag parentTag, + std::vector&& oldChildPairs, + std::vector&& newChildPairs, + const CullingContext& oldCullingContext, + const CullingContext& newCullingContext) { + if (oldChildPairs.empty() && newChildPairs.empty()) { + return; + } + + size_t index = 0; + + // Lists of mutations + auto mutationContainer = OrderedMutationInstructionContainer{}; + + if (ReactNativeFeatureFlags:: + enableDifferentiatorMutationVectorPreallocation()) { + // Pre-allocate mutation sub-vectors based on expected child count to avoid + // repeated reallocations during diffing. + size_t estimatedSize = std::max(oldChildPairs.size(), newChildPairs.size()); + mutationContainer.createMutations.reserve(estimatedSize); + mutationContainer.deleteMutations.reserve(estimatedSize); + mutationContainer.insertMutations.reserve(estimatedSize); + mutationContainer.removeMutations.reserve(estimatedSize); + mutationContainer.updateMutations.reserve(estimatedSize); + mutationContainer.downwardMutations.reserve(estimatedSize); + mutationContainer.destructiveDownwardMutations.reserve(estimatedSize); + } + + DEBUG_LOGS({ + LOG(ERROR) << "Differ Entry: Child Pairs of node: [" << parentTag << "]"; + LOG(ERROR) << "> Old Child Pairs: " << oldChildPairs; + LOG(ERROR) << "> New Child Pairs: " << newChildPairs; + }); + + // Stage 1: Collecting `Update` mutations + index = calculateShadowViewMutationsStage1( + mutationContainer, + parentTag, + oldChildPairs, + newChildPairs, + oldCullingContext, + newCullingContext); + + size_t lastIndexAfterFirstStage = index; + + if (index == newChildPairs.size()) { + // We've reached the end of the new children. We can delete+remove the + // rest. + calculateShadowViewMutationsRemoveRemaining( + mutationContainer, + parentTag, + oldChildPairs, + index, + oldCullingContext, + newCullingContext); + } else if (index == oldChildPairs.size()) { + // If we don't have any more existing children we can choose a fast path + // since the rest will all be create+insert. + calculateShadowViewMutationsCreateRemaining( + mutationContainer, + parentTag, + newChildPairs, + index, + oldCullingContext, + newCullingContext); + } else { + calculateShadowViewMutationsGreedyStage4( + scope, + mutationContainer, + parentTag, + oldChildPairs, + newChildPairs, + lastIndexAfterFirstStage, + oldCullingContext, + newCullingContext); } if (ReactNativeFeatureFlags:: From 2bcace6008d39546fb52bd1ba18b42a0d8cedca2 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 9 Jun 2026 09:11:17 -0700 Subject: [PATCH 5/6] Simplify `FabricMountingManager::executeMount` (#57154) Summary: `FabricMountingManager::executeMount` (CCN 59) dispatched a large per-mutation-type `switch` with repeated buffer-append boilerplate. This is a pure, behavior-preserving refactor: the five mutation-type case bodies are extracted into file-local `static` helpers backed by a small `MountItemBuffers` struct (holding `maintainMutationOrder` plus references to the output vectors, with an `orderedOr()` helper reproducing the exact `maintainMutationOrder ? common : bucket` selection), and the seven repeated empty-guard + per-item loops collapse into one templated batch writer. Ordering and side effects are preserved exactly (including the Update-case event-emitter quirk and Delete-batch-last ordering). Anonymous-namespace helpers only; no header or public API change. Changelog: [Internal] Differential Revision: D108027810 --- .../react/fabric/FabricMountingManager.cpp | 570 ++++++++++-------- 1 file changed, 305 insertions(+), 265 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index b82bdab278c..b4ddde37f8b 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -565,6 +565,258 @@ inline void writeUpdateOverflowInsetMountItem( overflowInsetBottom}); } +// Bundles together the per-mutation-type output vectors so the mutation +// handlers below can take a single reference. `maintainMutationOrder` +// determines whether mount items are appended to `common` (preserving the +// exact order produced by the differ) or to their type-specific bucket. +struct MountItemBuffers { + bool maintainMutationOrder; + std::vector& common; + std::vector& deletes; + std::vector& updateProps; + std::vector& updateState; + std::vector& updatePadding; + std::vector& updateLayout; + std::vector& updateOverflowInset; + std::vector& updateEventEmitter; + + std::vector& orderedOr(std::vector& bucket) { + return maintainMutationOrder ? common : bucket; + } +}; + +inline void handleCreateMutation( + const ShadowViewMutation& mutation, + MountItemBuffers& buffers, + std::unordered_set& allocatedViewTags) { + const auto& newChildShadowView = mutation.newChildShadowView; + bool shouldCreateView = !allocatedViewTags.contains(newChildShadowView.tag); + + if (shouldCreateView) { + buffers.common.push_back(CppMountItem::CreateMountItem(newChildShadowView)); + allocatedViewTags.insert(newChildShadowView.tag); + } +} + +inline void handleRemoveMutation( + const ShadowViewMutation& mutation, + bool isVirtual, + MountItemBuffers& buffers) { + if (!isVirtual) { + buffers.common.push_back( + CppMountItem::RemoveMountItem( + mutation.parentTag, mutation.oldChildShadowView, mutation.index)); + } +} + +inline void handleDeleteMutation( + const ShadowViewMutation& mutation, + MountItemBuffers& buffers, + std::unordered_set& allocatedViewTags) { + const auto& oldChildShadowView = mutation.oldChildShadowView; + buffers.orderedOr(buffers.deletes) + .push_back(CppMountItem::DeleteMountItem(oldChildShadowView)); + if (allocatedViewTags.erase(oldChildShadowView.tag) != 1) { + LOG(ERROR) << "Emitting delete for unallocated view " + << oldChildShadowView.tag; + } +} + +inline void handleUpdateMutation( + const ShadowViewMutation& mutation, + bool isVirtual, + MountItemBuffers& buffers, + std::unordered_set& allocatedViewTags) { + const auto& oldChildShadowView = mutation.oldChildShadowView; + const auto& newChildShadowView = mutation.newChildShadowView; + auto parentTag = mutation.parentTag; + + if (!isVirtual) { + if (!allocatedViewTags.contains(newChildShadowView.tag)) { + LOG(ERROR) << "Emitting update for unallocated view " + << newChildShadowView.tag; + } + + if (oldChildShadowView.props != newChildShadowView.props) { + buffers.orderedOr(buffers.updateProps) + .push_back( + CppMountItem::UpdatePropsMountItem( + oldChildShadowView, newChildShadowView)); + } + if (oldChildShadowView.state != newChildShadowView.state) { + buffers.orderedOr(buffers.updateState) + .push_back(CppMountItem::UpdateStateMountItem(newChildShadowView)); + } + + // Padding: padding mountItems must be executed before layout props + // are updated in the view. This is necessary to ensure that events + // (resulting from layout changes) are dispatched with the correct + // padding information. + if (oldChildShadowView.layoutMetrics.contentInsets != + newChildShadowView.layoutMetrics.contentInsets) { + buffers.orderedOr(buffers.updatePadding) + .push_back(CppMountItem::UpdatePaddingMountItem(newChildShadowView)); + } + + if (oldChildShadowView.layoutMetrics != newChildShadowView.layoutMetrics) { + buffers.orderedOr(buffers.updateLayout) + .push_back( + CppMountItem::UpdateLayoutMountItem( + mutation.newChildShadowView, parentTag)); + } + + // OverflowInset: This is the values indicating boundaries including + // children of the current view. The layout of current view may not + // change, and we separate this part from layout mount items to not + // pack too much data there. + if ((oldChildShadowView.layoutMetrics.overflowInset != + newChildShadowView.layoutMetrics.overflowInset)) { + buffers.orderedOr(buffers.updateOverflowInset) + .push_back( + CppMountItem::UpdateOverflowInsetMountItem(newChildShadowView)); + } + } + + if (oldChildShadowView.eventEmitter != newChildShadowView.eventEmitter) { + buffers.orderedOr(buffers.updateProps) + .push_back( + CppMountItem::UpdateEventEmitterMountItem( + mutation.newChildShadowView)); + } +} + +inline void handleInsertMutation( + const ShadowViewMutation& mutation, + bool isVirtual, + MountItemBuffers& buffers, + std::unordered_set& allocatedViewTags) { + const auto& newChildShadowView = mutation.newChildShadowView; + auto parentTag = mutation.parentTag; + auto& index = mutation.index; + + if (!isVirtual) { + // Insert item + buffers.common.push_back( + CppMountItem::InsertMountItem(parentTag, newChildShadowView, index)); + + bool shouldCreateView = !allocatedViewTags.contains(newChildShadowView.tag); + if (ReactNativeFeatureFlags::enableAccumulatedUpdatesInRawPropsAndroid()) { + if (shouldCreateView) { + LOG(ERROR) << "Emitting insert for unallocated view " + << newChildShadowView.tag; + } + buffers.orderedOr(buffers.updateProps) + .push_back( + CppMountItem::UpdatePropsMountItem({}, newChildShadowView)); + } else { + if (shouldCreateView) { + LOG(ERROR) << "Emitting insert for unallocated view " + << newChildShadowView.tag; + buffers.orderedOr(buffers.updateProps) + .push_back( + CppMountItem::UpdatePropsMountItem({}, newChildShadowView)); + } + } + + // State + if (newChildShadowView.state) { + buffers.orderedOr(buffers.updateState) + .push_back(CppMountItem::UpdateStateMountItem(newChildShadowView)); + } + + // Padding: padding mountItems must be executed before layout props + // are updated in the view. This is necessary to ensure that events + // (resulting from layout changes) are dispatched with the correct + // padding information. + if (newChildShadowView.layoutMetrics.contentInsets != EdgeInsets::ZERO) { + buffers.orderedOr(buffers.updatePadding) + .push_back(CppMountItem::UpdatePaddingMountItem(newChildShadowView)); + } + + // Layout + buffers.orderedOr(buffers.updateLayout) + .push_back( + CppMountItem::UpdateLayoutMountItem(newChildShadowView, parentTag)); + + // OverflowInset: This is the values indicating boundaries including + // children of the current view. The layout of current view may not + // change, and we separate this part from layout mount items to not + // pack too much data there. + if (newChildShadowView.layoutMetrics.overflowInset != EdgeInsets::ZERO) { + buffers.orderedOr(buffers.updateOverflowInset) + .push_back( + CppMountItem::UpdateOverflowInsetMountItem(newChildShadowView)); + } + } + + // EventEmitter + // On insert we always update the event emitter, as we do not pass + // it in when preallocating views + buffers.orderedOr(buffers.updateEventEmitter) + .push_back( + CppMountItem::UpdateEventEmitterMountItem( + mutation.newChildShadowView)); +} + +// Dispatches a single mount item from the `common` vector to the matching +// writer. Preserves the exact switch behavior, including the FATAL default. +inline void writeCommonMountItem( + InstructionBuffer& buffer, + const CppMountItem& mountItem) { + const auto& mountItemType = mountItem.type; + switch (mountItemType) { + case CppMountItem::Type::Create: + writeCreateMountItem(buffer, mountItem); + break; + case CppMountItem::Type::Delete: + writeDeleteMountItem(buffer, mountItem); + break; + case CppMountItem::Type::Insert: + writeInsertMountItem(buffer, mountItem); + break; + case CppMountItem::Type::Remove: + writeRemoveMountItem(buffer, mountItem); + break; + case CppMountItem::Type::UpdateProps: + writeUpdatePropsMountItem(buffer, mountItem); + break; + case CppMountItem::Type::UpdateState: + writeUpdateStateMountItem(buffer, mountItem); + break; + case CppMountItem::Type::UpdateLayout: + writeUpdateLayoutMountItem(buffer, mountItem); + break; + case CppMountItem::Type::UpdateEventEmitter: + writeUpdateEventEmitterMountItem(buffer, mountItem); + break; + case CppMountItem::Type::UpdatePadding: + writeUpdatePaddingMountItem(buffer, mountItem); + break; + case CppMountItem::Type::UpdateOverflowInset: + writeUpdateOverflowInsetMountItem(buffer, mountItem); + break; + default: + LOG(FATAL) << "Unexpected CppMountItem type: " << mountItemType; + } +} + +// Writes a homogeneous batch of mount items (preamble + each item via +// `writeItem`). No-op for an empty batch, matching the original guards. +template +inline void writeMountItemBatch( + InstructionBuffer& buffer, + CppMountItem::Type mountItemType, + const std::vector& mountItems, + WriteFn&& writeItem) { + if (mountItems.empty()) { + return; + } + writeMountItemPreamble(buffer, mountItemType, mountItems.size()); + for (const auto& mountItem : mountItems) { + writeItem(buffer, mountItem); + } +} + } // namespace void FabricMountingManager::executeMount( @@ -613,191 +865,38 @@ void FabricMountingManager::executeMount( << " was stopped!"; } - for (const auto& mutation : mutations) { - auto parentTag = mutation.parentTag; - const auto& oldChildShadowView = mutation.oldChildShadowView; - const auto& newChildShadowView = mutation.newChildShadowView; - auto& mutationType = mutation.type; - auto& index = mutation.index; + MountItemBuffers buffers{ + maintainMutationOrder, + cppCommonMountItems, + cppDeleteMountItems, + cppUpdatePropsMountItems, + cppUpdateStateMountItems, + cppUpdatePaddingMountItems, + cppUpdateLayoutMountItems, + cppUpdateOverflowInsetMountItems, + cppUpdateEventEmitterMountItems}; + for (const auto& mutation : mutations) { bool isVirtual = mutation.mutatedViewIsVirtual(); - switch (mutationType) { + switch (mutation.type) { case ShadowViewMutation::Create: { - bool shouldCreateView = - !allocatedViewTags.contains(newChildShadowView.tag); - - if (shouldCreateView) { - cppCommonMountItems.push_back( - CppMountItem::CreateMountItem(newChildShadowView)); - allocatedViewTags.insert(newChildShadowView.tag); - } + handleCreateMutation(mutation, buffers, allocatedViewTags); break; } case ShadowViewMutation::Remove: { - if (!isVirtual) { - cppCommonMountItems.push_back( - CppMountItem::RemoveMountItem( - parentTag, oldChildShadowView, index)); - } + handleRemoveMutation(mutation, isVirtual, buffers); break; } case ShadowViewMutation::Delete: { - (maintainMutationOrder ? cppCommonMountItems : cppDeleteMountItems) - .push_back(CppMountItem::DeleteMountItem(oldChildShadowView)); - if (allocatedViewTags.erase(oldChildShadowView.tag) != 1) { - LOG(ERROR) << "Emitting delete for unallocated view " - << oldChildShadowView.tag; - } + handleDeleteMutation(mutation, buffers, allocatedViewTags); break; } case ShadowViewMutation::Update: { - if (!isVirtual) { - if (!allocatedViewTags.contains(newChildShadowView.tag)) { - LOG(ERROR) << "Emitting update for unallocated view " - << newChildShadowView.tag; - } - - if (oldChildShadowView.props != newChildShadowView.props) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePropsMountItems) - .push_back( - CppMountItem::UpdatePropsMountItem( - oldChildShadowView, newChildShadowView)); - } - if (oldChildShadowView.state != newChildShadowView.state) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdateStateMountItems) - .push_back( - CppMountItem::UpdateStateMountItem(newChildShadowView)); - } - - // Padding: padding mountItems must be executed before layout props - // are updated in the view. This is necessary to ensure that events - // (resulting from layout changes) are dispatched with the correct - // padding information. - if (oldChildShadowView.layoutMetrics.contentInsets != - newChildShadowView.layoutMetrics.contentInsets) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePaddingMountItems) - .push_back( - CppMountItem::UpdatePaddingMountItem(newChildShadowView)); - } - - if (oldChildShadowView.layoutMetrics != - newChildShadowView.layoutMetrics) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdateLayoutMountItems) - .push_back( - CppMountItem::UpdateLayoutMountItem( - mutation.newChildShadowView, parentTag)); - } - - // OverflowInset: This is the values indicating boundaries including - // children of the current view. The layout of current view may not - // change, and we separate this part from layout mount items to not - // pack too much data there. - if ((oldChildShadowView.layoutMetrics.overflowInset != - newChildShadowView.layoutMetrics.overflowInset)) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdateOverflowInsetMountItems) - .push_back( - CppMountItem::UpdateOverflowInsetMountItem( - newChildShadowView)); - } - } - - if (oldChildShadowView.eventEmitter != - newChildShadowView.eventEmitter) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePropsMountItems) - .push_back( - CppMountItem::UpdateEventEmitterMountItem( - mutation.newChildShadowView)); - } + handleUpdateMutation(mutation, isVirtual, buffers, allocatedViewTags); break; } case ShadowViewMutation::Insert: { - if (!isVirtual) { - // Insert item - cppCommonMountItems.push_back( - CppMountItem::InsertMountItem( - parentTag, newChildShadowView, index)); - - bool shouldCreateView = - !allocatedViewTags.contains(newChildShadowView.tag); - if (ReactNativeFeatureFlags:: - enableAccumulatedUpdatesInRawPropsAndroid()) { - if (shouldCreateView) { - LOG(ERROR) << "Emitting insert for unallocated view " - << newChildShadowView.tag; - } - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePropsMountItems) - .push_back( - CppMountItem::UpdatePropsMountItem( - {}, newChildShadowView)); - } else { - if (shouldCreateView) { - LOG(ERROR) << "Emitting insert for unallocated view " - << newChildShadowView.tag; - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePropsMountItems) - .push_back( - CppMountItem::UpdatePropsMountItem( - {}, newChildShadowView)); - } - } - - // State - if (newChildShadowView.state) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdateStateMountItems) - .push_back( - CppMountItem::UpdateStateMountItem(newChildShadowView)); - } - - // Padding: padding mountItems must be executed before layout props - // are updated in the view. This is necessary to ensure that events - // (resulting from layout changes) are dispatched with the correct - // padding information. - if (newChildShadowView.layoutMetrics.contentInsets != - EdgeInsets::ZERO) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePaddingMountItems) - .push_back( - CppMountItem::UpdatePaddingMountItem(newChildShadowView)); - } - - // Layout - (maintainMutationOrder ? cppCommonMountItems - : cppUpdateLayoutMountItems) - .push_back( - CppMountItem::UpdateLayoutMountItem( - newChildShadowView, parentTag)); - - // OverflowInset: This is the values indicating boundaries including - // children of the current view. The layout of current view may not - // change, and we separate this part from layout mount items to not - // pack too much data there. - if (newChildShadowView.layoutMetrics.overflowInset != - EdgeInsets::ZERO) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdateOverflowInsetMountItems) - .push_back( - CppMountItem::UpdateOverflowInsetMountItem( - newChildShadowView)); - } - } - - // EventEmitter - // On insert we always update the event emitter, as we do not pass - // it in when preallocating views - (maintainMutationOrder ? cppCommonMountItems - : cppUpdateEventEmitterMountItems) - .push_back( - CppMountItem::UpdateEventEmitterMountItem( - mutation.newChildShadowView)); - + handleInsertMutation(mutation, isVirtual, buffers, allocatedViewTags); break; } default: { @@ -879,96 +978,39 @@ void FabricMountingManager::executeMount( prevMountItemType = mountItemType; } - switch (mountItemType) { - case CppMountItem::Type::Create: - writeCreateMountItem(buffer, mountItem); - break; - case CppMountItem::Type::Delete: - writeDeleteMountItem(buffer, mountItem); - break; - case CppMountItem::Type::Insert: - writeInsertMountItem(buffer, mountItem); - break; - case CppMountItem::Type::Remove: - writeRemoveMountItem(buffer, mountItem); - break; - case CppMountItem::Type::UpdateProps: - writeUpdatePropsMountItem(buffer, mountItem); - break; - case CppMountItem::Type::UpdateState: - writeUpdateStateMountItem(buffer, mountItem); - break; - case CppMountItem::Type::UpdateLayout: - writeUpdateLayoutMountItem(buffer, mountItem); - break; - case CppMountItem::Type::UpdateEventEmitter: - writeUpdateEventEmitterMountItem(buffer, mountItem); - break; - case CppMountItem::Type::UpdatePadding: - writeUpdatePaddingMountItem(buffer, mountItem); - break; - case CppMountItem::Type::UpdateOverflowInset: - writeUpdateOverflowInsetMountItem(buffer, mountItem); - break; - default: - LOG(FATAL) << "Unexpected CppMountItem type: " << mountItemType; - } + writeCommonMountItem(buffer, mountItem); } - if (!cppUpdatePropsMountItems.empty()) { - writeMountItemPreamble( - buffer, - CppMountItem::Type::UpdateProps, - cppUpdatePropsMountItems.size()); - for (const auto& mountItem : cppUpdatePropsMountItems) { - writeUpdatePropsMountItem(buffer, mountItem); - } - } - if (!cppUpdateStateMountItems.empty()) { - writeMountItemPreamble( - buffer, - CppMountItem::Type::UpdateState, - cppUpdateStateMountItems.size()); - for (const auto& mountItem : cppUpdateStateMountItems) { - writeUpdateStateMountItem(buffer, mountItem); - } - } - if (!cppUpdatePaddingMountItems.empty()) { - writeMountItemPreamble( - buffer, - CppMountItem::Type::UpdatePadding, - cppUpdatePaddingMountItems.size()); - for (const auto& mountItem : cppUpdatePaddingMountItems) { - writeUpdatePaddingMountItem(buffer, mountItem); - } - } - if (!cppUpdateLayoutMountItems.empty()) { - writeMountItemPreamble( - buffer, - CppMountItem::Type::UpdateLayout, - cppUpdateLayoutMountItems.size()); - for (const auto& mountItem : cppUpdateLayoutMountItems) { - writeUpdateLayoutMountItem(buffer, mountItem); - } - } - if (!cppUpdateOverflowInsetMountItems.empty()) { - writeMountItemPreamble( - buffer, - CppMountItem::Type::UpdateOverflowInset, - cppUpdateOverflowInsetMountItems.size()); - for (const auto& mountItem : cppUpdateOverflowInsetMountItems) { - writeUpdateOverflowInsetMountItem(buffer, mountItem); - } - } - if (!cppUpdateEventEmitterMountItems.empty()) { - writeMountItemPreamble( - buffer, - CppMountItem::Type::UpdateEventEmitter, - cppUpdateEventEmitterMountItems.size()); - for (const auto& mountItem : cppUpdateEventEmitterMountItems) { - writeUpdateEventEmitterMountItem(buffer, mountItem); - } - } + writeMountItemBatch( + buffer, + CppMountItem::Type::UpdateProps, + cppUpdatePropsMountItems, + writeUpdatePropsMountItem); + writeMountItemBatch( + buffer, + CppMountItem::Type::UpdateState, + cppUpdateStateMountItems, + writeUpdateStateMountItem); + writeMountItemBatch( + buffer, + CppMountItem::Type::UpdatePadding, + cppUpdatePaddingMountItems, + writeUpdatePaddingMountItem); + writeMountItemBatch( + buffer, + CppMountItem::Type::UpdateLayout, + cppUpdateLayoutMountItems, + writeUpdateLayoutMountItem); + writeMountItemBatch( + buffer, + CppMountItem::Type::UpdateOverflowInset, + cppUpdateOverflowInsetMountItems, + writeUpdateOverflowInsetMountItem); + writeMountItemBatch( + buffer, + CppMountItem::Type::UpdateEventEmitter, + cppUpdateEventEmitterMountItems, + writeUpdateEventEmitterMountItem); // Write deletes last - so that all prop updates, etc, for the tag in the same // batch don't fail. Without additional machinery, moving deletes here @@ -977,13 +1019,11 @@ void FabricMountingManager::executeMount( // for space efficiency. // FIXME: this optimization is incorrect when multiple transactions are // merged together - if (!cppDeleteMountItems.empty()) { - writeMountItemPreamble( - buffer, CppMountItem::Type::Delete, cppDeleteMountItems.size()); - for (const auto& mountItem : cppDeleteMountItems) { - writeDeleteMountItem(buffer, mountItem); - } - } + writeMountItemBatch( + buffer, + CppMountItem::Type::Delete, + cppDeleteMountItems, + writeDeleteMountItem); static auto createMountItemsIntBufferBatchContainer = JFabricUIManager::javaClassStatic() From fdac037901895b0ee716fa6b8c9e0734a48a2fe2 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 9 Jun 2026 09:11:17 -0700 Subject: [PATCH 6/6] Simplify `UIManagerBinding::get` host-function dispatch (#57155) Summary: `UIManagerBinding::get` (CCN 71) builds the UIManager JSI host object via one long chain of `if (methodName == "...") return Function::createFromHostFunction(...)`. This is a pure, behavior-preserving refactor: contiguous groups of methods are extracted into file-local `static` helpers that each return `std::optional` (node management, child set, layout/command, measurement, DOM-compat, view-transition, etc.); `get` consults them in order and falls through identically. Method names, arities, and host-function bodies are unchanged. Only the `.cpp` is touched; no header or public API change. Changelog: [Internal] Differential Revision: D108027814 --- .../renderer/uimanager/UIManagerBinding.cpp | 372 ++++++++++++------ 1 file changed, 243 insertions(+), 129 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index c79fc75df5b..38343ba594a 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -17,6 +17,7 @@ #include #include +#include #include namespace facebook::react { @@ -181,40 +182,18 @@ static void validateArgumentCount( } } -jsi::Value UIManagerBinding::get( - jsi::Runtime& runtime, - const jsi::PropNameID& name) { - auto methodName = name.utf8(runtime); - - // Convert shared_ptr to a raw ptr - // Why? Because: - // 1) UIManagerBinding strongly retains UIManager. The JS VM - // strongly retains UIManagerBinding (through the JSI). - // These functions are JSI functions and are only called via - // the JS VM; if the JS VM is torn down, those functions can't - // execute and these lambdas won't execute. - // 2) The UIManager is only deallocated when all references to it - // are deallocated, including the UIManagerBinding. That only - // happens when the JS VM is deallocated. So, the raw pointer - // is safe. - // - // Even if it's safe, why not just use shared_ptr anyway as - // extra insurance? - // 1) Using shared_ptr or weak_ptr when they're not needed is - // a pessimisation. It's more instructions executed without - // any additional value in this case. - // 2) How and when exactly these lambdas is deallocated is - // complex. Adding shared_ptr to them which causes the UIManager - // to potentially live longer is unnecessary, complicated cognitive - // overhead. - // 3) There is a strong suspicion that retaining UIManager from - // these C++ lambdas, which are retained by an object that is held onto - // by the JSI, caused some crashes upon deallocation of the - // Scheduler and JS VM. This could happen if, for instance, C++ - // semantics cause these lambda to not be deallocated until - // a CPU tick (or more) after the JS VM is deallocated. - UIManager* uiManager = uiManager_.get(); +// The following `get*Methods` free functions each handle a contiguous group of +// the JSI host-function methods exposed by `UIManagerBinding::get`. They return +// `std::nullopt` when `methodName` is not one they handle, allowing `get` to be +// a flat dispatch sequence. Splitting the original monolithic function this way +// keeps behavior identical (same method names, arity, captures and bodies) +// while lowering its cyclomatic complexity. +static std::optional getNodeManagementMethods( + UIManager* uiManager, + jsi::Runtime& runtime, + const jsi::PropNameID& name, + const std::string& methodName) { // Semantic: Creates a new node with given pieces. if (methodName == "createNode") { auto paramCount = 5; @@ -275,46 +254,6 @@ jsi::Value UIManagerBinding::get( }); } - if (methodName == "findNodeAtPoint") { - auto paramCount = 4; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto node = Bridging>::fromJs( - runtime, arguments[0]); - auto locationX = (Float)arguments[1].getNumber(); - auto locationY = (Float)arguments[2].getNumber(); - auto onSuccessFunction = - arguments[3].getObject(runtime).getFunction(runtime); - auto targetNode = uiManager->findNodeAtPoint( - node, Point{.x = locationX, .y = locationY}); - - if (!targetNode) { - onSuccessFunction.call(runtime, jsi::Value::null()); - return jsi::Value::undefined(); - } - - auto& eventTarget = targetNode->getEventEmitter()->eventTarget_; - - EventEmitter::DispatchMutex().lock(); - eventTarget->retain(runtime); - auto instanceHandle = eventTarget->getInstanceHandle(runtime); - eventTarget->release(runtime); - EventEmitter::DispatchMutex().unlock(); - - onSuccessFunction.call(runtime, std::move(instanceHandle)); - return jsi::Value::undefined(); - }); - } - // Semantic: Clones the node with *same* props and *given* children. if (methodName == "cloneNodeWithNewChildren") { auto paramCount = 2; @@ -398,6 +337,14 @@ jsi::Value UIManagerBinding::get( }); } + return std::nullopt; +} + +static std::optional getChildSetMethods( + UIManager* uiManager, + jsi::Runtime& runtime, + const jsi::PropNameID& name, + const std::string& methodName) { if (methodName == "appendChild") { auto paramCount = 2; return jsi::Function::createFromHostFunction( @@ -488,27 +435,14 @@ jsi::Value UIManagerBinding::get( }); } - if (methodName == "registerEventHandler") { - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [this, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto eventHandler = - arguments[0].getObject(runtime).getFunction(runtime); - eventHandler_ = - std::make_unique(std::move(eventHandler)); - return jsi::Value::undefined(); - }); - } + return std::nullopt; +} +static std::optional getLayoutAndCommandMethods( + UIManager* uiManager, + jsi::Runtime& runtime, + const jsi::PropNameID& name, + const std::string& methodName) { if (methodName == "getRelativeLayoutMetrics") { auto paramCount = 2; return jsi::Function::createFromHostFunction( @@ -639,6 +573,14 @@ jsi::Value UIManagerBinding::get( }); } + return std::nullopt; +} + +static std::optional getMeasurementMethods( + UIManager* uiManager, + jsi::Runtime& runtime, + const jsi::PropNameID& name, + const std::string& methodName) { if (methodName == "measure") { auto paramCount = 2; return jsi::Function::createFromHostFunction( @@ -739,43 +681,11 @@ jsi::Value UIManagerBinding::get( }); } - if (methodName == "configureNextLayoutAnimation") { - auto paramCount = 3; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - uiManager->configureNextLayoutAnimation( - runtime, - // TODO: pass in JSI value instead of folly::dynamic to RawValue - RawValue(commandArgsFromValue(runtime, arguments[0])), - arguments[1], - arguments[2]); - return jsi::Value::undefined(); - }); - } - - if (methodName == "unstable_getCurrentEventPriority") { - return jsi::Function::createFromHostFunction( - runtime, - name, - 0, - [this]( - jsi::Runtime& /*runtime*/, - const jsi::Value& /*thisValue*/, - const jsi::Value* /*arguments*/, - size_t /*count*/) -> jsi::Value { - return {serialize(currentEventPriority_)}; - }); - } + return std::nullopt; +} +static std::optional getEventPriorityConstants( + const std::string& methodName) { if (methodName == "unstable_DefaultEventPriority") { return {serialize(ReactEventPriority::Default)}; } @@ -792,6 +702,14 @@ jsi::Value UIManagerBinding::get( return {serialize(ReactEventPriority::Idle)}; } + return std::nullopt; +} + +static std::optional getDomCompatMethods( + UIManager* uiManager, + jsi::Runtime& runtime, + const jsi::PropNameID& name, + const std::string& methodName) { if (methodName == "findShadowNodeByTag_DEPRECATED") { auto paramCount = 1; return jsi::Function::createFromHostFunction( @@ -944,6 +862,14 @@ jsi::Value UIManagerBinding::get( }); } + return std::nullopt; +} + +static std::optional getViewTransitionMethods( + UIManager* uiManager, + jsi::Runtime& runtime, + const jsi::PropNameID& name, + const std::string& methodName) { if (methodName == "applyViewTransitionName") { auto paramCount = 3; return jsi::Function::createFromHostFunction( @@ -1046,6 +972,14 @@ jsi::Value UIManagerBinding::get( }); } + return std::nullopt; +} + +static std::optional getViewTransitionLifecycleMethods( + UIManager* uiManager, + jsi::Runtime& runtime, + const jsi::PropNameID& name, + const std::string& methodName) { if (methodName == "restoreViewTransitionName") { auto paramCount = 1; return jsi::Function::createFromHostFunction( @@ -1110,6 +1044,186 @@ jsi::Value UIManagerBinding::get( }); } + return std::nullopt; +} + +jsi::Value UIManagerBinding::get( + jsi::Runtime& runtime, + const jsi::PropNameID& name) { + auto methodName = name.utf8(runtime); + + // Convert shared_ptr to a raw ptr + // Why? Because: + // 1) UIManagerBinding strongly retains UIManager. The JS VM + // strongly retains UIManagerBinding (through the JSI). + // These functions are JSI functions and are only called via + // the JS VM; if the JS VM is torn down, those functions can't + // execute and these lambdas won't execute. + // 2) The UIManager is only deallocated when all references to it + // are deallocated, including the UIManagerBinding. That only + // happens when the JS VM is deallocated. So, the raw pointer + // is safe. + // + // Even if it's safe, why not just use shared_ptr anyway as + // extra insurance? + // 1) Using shared_ptr or weak_ptr when they're not needed is + // a pessimisation. It's more instructions executed without + // any additional value in this case. + // 2) How and when exactly these lambdas is deallocated is + // complex. Adding shared_ptr to them which causes the UIManager + // to potentially live longer is unnecessary, complicated cognitive + // overhead. + // 3) There is a strong suspicion that retaining UIManager from + // these C++ lambdas, which are retained by an object that is held onto + // by the JSI, caused some crashes upon deallocation of the + // Scheduler and JS VM. This could happen if, for instance, C++ + // semantics cause these lambda to not be deallocated until + // a CPU tick (or more) after the JS VM is deallocated. + UIManager* uiManager = uiManager_.get(); + + if (auto result = + getNodeManagementMethods(uiManager, runtime, name, methodName)) { + return std::move(*result); + } + + // Kept inline because the lambda accesses `EventEmitter::eventTarget_`, which + // is only reachable through `UIManagerBinding`'s `friend` access. + if (methodName == "findNodeAtPoint") { + auto paramCount = 4; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [uiManager, methodName, paramCount]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* arguments, + size_t count) { + validateArgumentCount(runtime, methodName, paramCount, count); + + auto node = Bridging>::fromJs( + runtime, arguments[0]); + auto locationX = (Float)arguments[1].getNumber(); + auto locationY = (Float)arguments[2].getNumber(); + auto onSuccessFunction = + arguments[3].getObject(runtime).getFunction(runtime); + auto targetNode = uiManager->findNodeAtPoint( + node, Point{.x = locationX, .y = locationY}); + + if (!targetNode) { + onSuccessFunction.call(runtime, jsi::Value::null()); + return jsi::Value::undefined(); + } + + auto& eventTarget = targetNode->getEventEmitter()->eventTarget_; + + EventEmitter::DispatchMutex().lock(); + eventTarget->retain(runtime); + auto instanceHandle = eventTarget->getInstanceHandle(runtime); + eventTarget->release(runtime); + EventEmitter::DispatchMutex().unlock(); + + onSuccessFunction.call(runtime, std::move(instanceHandle)); + return jsi::Value::undefined(); + }); + } + + if (auto result = getChildSetMethods(uiManager, runtime, name, methodName)) { + return std::move(*result); + } + + if (methodName == "registerEventHandler") { + auto paramCount = 1; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [this, methodName, paramCount]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + + auto eventHandler = + arguments[0].getObject(runtime).getFunction(runtime); + eventHandler_ = + std::make_unique(std::move(eventHandler)); + return jsi::Value::undefined(); + }); + } + + if (auto result = + getLayoutAndCommandMethods(uiManager, runtime, name, methodName)) { + return std::move(*result); + } + + if (auto result = + getMeasurementMethods(uiManager, runtime, name, methodName)) { + return std::move(*result); + } + + // Kept inline because the lambda calls the private + // `UIManager::configureNextLayoutAnimation` and constructs a `RawValue`, both + // of which are only reachable through `UIManagerBinding`'s `friend` access. + if (methodName == "configureNextLayoutAnimation") { + auto paramCount = 3; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [uiManager, methodName, paramCount]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + + uiManager->configureNextLayoutAnimation( + runtime, + // TODO: pass in JSI value instead of folly::dynamic to RawValue + RawValue(commandArgsFromValue(runtime, arguments[0])), + arguments[1], + arguments[2]); + return jsi::Value::undefined(); + }); + } + + if (methodName == "unstable_getCurrentEventPriority") { + return jsi::Function::createFromHostFunction( + runtime, + name, + 0, + [this]( + jsi::Runtime& /*runtime*/, + const jsi::Value& /*thisValue*/, + const jsi::Value* /*arguments*/, + size_t /*count*/) -> jsi::Value { + return {serialize(currentEventPriority_)}; + }); + } + + if (auto result = getEventPriorityConstants(methodName)) { + return std::move(*result); + } + + if (auto result = getDomCompatMethods(uiManager, runtime, name, methodName)) { + return std::move(*result); + } + + if (auto result = + getViewTransitionMethods(uiManager, runtime, name, methodName)) { + return std::move(*result); + } + + if (auto result = getViewTransitionLifecycleMethods( + uiManager, runtime, name, methodName)) { + return std::move(*result); + } + + // Kept inline because the lambda captures the private + // `UIManager::runtimeExecutor_`, which is only reachable through + // `UIManagerBinding`'s `friend` access. if (methodName == "startViewTransition") { auto paramCount = 1; return jsi::Function::createFromHostFunction(