From f85b30b24b30bfaf84d9b604554a8bdf2a1e615a Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 5 May 2025 16:48:16 -0700 Subject: [PATCH] Underpinnings for Caching Text Layouts (#51065) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51065 This adds infrastructure to let us start storing cached Android text layouts as part of a `ParagraphShadowNode`. After this, we will clear them out, and propagate them to state. Right now, the flag doesn't do much, apart from extra work. This is done by adding `TextLayoutManagerExtended::supportsPreparedLayout()`, and `TextLayoutManager::PreparedLayout` types, to shim between platforms, then on Android, we add a `PreparedLayout`, which is for now just an Android layout, with extra field (`maxNumberOfLines`, for some reason not exposed on recent versions). Android `TextLayoutManager` java side is split a little bit, so that we reuse all the existing logic for prepared layouts. I tried to set up the boundary, so that we don't reserialize a MapBuffer after preparation, and for simplicity, this means source of truth for attachment count, and attachment sizes, now lives on the layout. This means we need to change boundary a bit, where we are no longer able to pass in a buffer to fill from C++ side of attachment positions. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D73970149 fbshipit-source-id: ff71c227e062c16fe52a4eb3ba2acbebf3d96e56 --- .../ReactAndroid/api/ReactAndroid.api | 2 + .../react/fabric/FabricUIManager.java | 47 +++++++++ .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 ++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 ++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../react/views/text/PreparedLayout.kt | 18 ++++ .../react/views/text/TextLayoutManager.java | 45 ++++++++- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 ++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 62 +++++++----- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 ++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../components/text/ParagraphShadowNode.cpp | 60 ++++++++--- .../components/text/ParagraphShadowNode.h | 13 +++ .../TextLayoutManagerExtended.h | 63 ++++++++++++ .../textlayoutmanager/JPreparedLayout.h | 20 ++++ .../textlayoutmanager/TextLayoutManager.cpp | 99 ++++++++++++++++++- .../textlayoutmanager/TextLayoutManager.h | 24 +++++ .../ReactNativeFeatureFlags.config.js | 10 ++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 30 files changed, 530 insertions(+), 59 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayout.kt create mode 100644 packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/JPreparedLayout.h diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 69e8a088065..ba0f4cd8e1f 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2392,12 +2392,14 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid public fun initialize ()V public fun invalidate ()V public fun markActiveTouchForTag (II)V + public fun measurePreparedLayout (Lcom/facebook/react/views/text/PreparedLayout;FFFF)[F public fun onAllAnimationsComplete ()V public fun onAnimationStarted ()V public fun onHostDestroy ()V public fun onHostPause ()V public fun onHostResume ()V public fun onRequestEventBeat ()V + public fun prepareLayout (ILcom/facebook/react/common/mapbuffer/ReadableMapBuffer;Lcom/facebook/react/common/mapbuffer/ReadableMapBuffer;FF)Lcom/facebook/react/views/text/PreparedLayout; public fun prependUIBlock (Lcom/facebook/react/fabric/interop/UIBlock;)V public fun profileNextBatch ()V public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 14b4439b6ca..cd26afb1907 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -22,11 +22,13 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Point; import android.os.SystemClock; +import android.text.Layout; import android.view.View; import android.view.accessibility.AccessibilityEvent; import androidx.annotation.AnyThread; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.core.util.Preconditions; import androidx.core.view.ViewCompat.FocusRealDirection; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; @@ -49,6 +51,7 @@ import com.facebook.react.bridge.UIManager; import com.facebook.react.bridge.UIManagerListener; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.UnstableReactNativeAPI; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.common.mapbuffer.ReadableMapBuffer; @@ -86,6 +89,7 @@ import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.events.FabricEventDispatcher; import com.facebook.react.uimanager.events.RCTEventEmitter; import com.facebook.react.uimanager.events.SynchronousEventReceiver; +import com.facebook.react.views.text.PreparedLayout; import com.facebook.react.views.text.TextLayoutManager; import java.util.ArrayList; import java.util.HashMap; @@ -648,6 +652,49 @@ public class FabricUIManager attachmentsPositions); } + @AnyThread + @ThreadConfined(ANY) + public PreparedLayout prepareLayout( + int surfaceId, + ReadableMapBuffer attributedString, + ReadableMapBuffer paragraphAttributes, + float maxWidth, + float maxHeight) { + SurfaceMountingManager surfaceMountingManager = + mMountingManager.getSurfaceManagerEnforced(surfaceId, "prepareLayout"); + Layout layout = + TextLayoutManager.createLayout( + Preconditions.checkNotNull(surfaceMountingManager.getContext()), + attributedString, + paragraphAttributes, + PixelUtil.toPixelFromDIP(maxWidth), + PixelUtil.toPixelFromDIP(maxHeight), + null /* T219881133: Migrate away from ReactTextViewManagerCallback */); + + int maximumNumberOfLines = + paragraphAttributes.contains(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES) + ? paragraphAttributes.getInt(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES) + : ReactConstants.UNSET; + + return new PreparedLayout(layout, maximumNumberOfLines); + } + + @AnyThread + @ThreadConfined(ANY) + public float[] measurePreparedLayout( + PreparedLayout preparedLayout, + float minWidth, + float maxWidth, + float minHeight, + float maxHeight) { + return TextLayoutManager.measurePreparedLayout( + preparedLayout, + getYogaSize(minWidth, maxWidth), + getYogaMeasureMode(minWidth, maxWidth), + getYogaSize(minHeight, maxHeight), + getYogaMeasureMode(minHeight, maxHeight)); + } + /** * @param surfaceId {@link int} surface ID * @param defaultTextInputPadding {@link float[]} output parameter will contain the default theme diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index b91c62ba7c7..7b1d1b15cca 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<31638ef8ac6992a354785b74af8fbe0d>> + * @generated SignedSource<<453f8c0a593b173c197fcf54ed834a1b>> */ /** @@ -180,6 +180,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableNewBackgroundAndBorderDrawables(): Boolean = accessor.enableNewBackgroundAndBorderDrawables() + /** + * Enables caching text layout artifacts for later reuse + */ + @JvmStatic + public fun enablePreparedTextLayout(): Boolean = accessor.enablePreparedTextLayout() + /** * When enabled, Android will receive prop updates based on the differences between the last rendered shadow node and the last committed shadow node. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 2548e16d9b5..ddf67841e46 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6b382661025db56592b44255f5a8694c>> + * @generated SignedSource<> */ /** @@ -45,6 +45,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableNativeCSSParsingCache: Boolean? = null private var enableNetworkEventReportingCache: Boolean? = null private var enableNewBackgroundAndBorderDrawablesCache: Boolean? = null + private var enablePreparedTextLayoutCache: Boolean? = null private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null private var enableResourceTimingAPICache: Boolean? = null private var enableSynchronousStateUpdatesCache: Boolean? = null @@ -292,6 +293,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enablePreparedTextLayout(): Boolean { + var cached = enablePreparedTextLayoutCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enablePreparedTextLayout() + enablePreparedTextLayoutCache = cached + } + return cached + } + override fun enablePropsUpdateReconciliationAndroid(): Boolean { var cached = enablePropsUpdateReconciliationAndroidCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index e06c5328a2f..f08774f61f3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8276fd1166cdd235f11a0b490bb7d924>> + * @generated SignedSource<<9d0b02395a08331bca956ea600602a31>> */ /** @@ -78,6 +78,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableNewBackgroundAndBorderDrawables(): Boolean + @DoNotStrip @JvmStatic public external fun enablePreparedTextLayout(): Boolean + @DoNotStrip @JvmStatic public external fun enablePropsUpdateReconciliationAndroid(): Boolean @DoNotStrip @JvmStatic public external fun enableResourceTimingAPI(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index 17bca79efb9..08758b67a4f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -73,6 +73,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableNewBackgroundAndBorderDrawables(): Boolean = true + override fun enablePreparedTextLayout(): Boolean = false + override fun enablePropsUpdateReconciliationAndroid(): Boolean = false override fun enableResourceTimingAPI(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 37a4cb6252d..4500426891e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5dc41059d71d3a345be45a6a233b05a0>> + * @generated SignedSource<<4c81ed8a06c192eb4007219d163650e5>> */ /** @@ -49,6 +49,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableNativeCSSParsingCache: Boolean? = null private var enableNetworkEventReportingCache: Boolean? = null private var enableNewBackgroundAndBorderDrawablesCache: Boolean? = null + private var enablePreparedTextLayoutCache: Boolean? = null private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null private var enableResourceTimingAPICache: Boolean? = null private var enableSynchronousStateUpdatesCache: Boolean? = null @@ -321,6 +322,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun enablePreparedTextLayout(): Boolean { + var cached = enablePreparedTextLayoutCache + if (cached == null) { + cached = currentProvider.enablePreparedTextLayout() + accessedFeatureFlags.add("enablePreparedTextLayout") + enablePreparedTextLayoutCache = cached + } + return cached + } + override fun enablePropsUpdateReconciliationAndroid(): Boolean { var cached = enablePropsUpdateReconciliationAndroidCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 59cc950c784..3757c2b065a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<35f6d9ae3c445c81fc4bab7509fd3179>> + * @generated SignedSource<<2482f57e0652cfaa4806b5333c50ad9f>> */ /** @@ -73,6 +73,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableNewBackgroundAndBorderDrawables(): Boolean + @DoNotStrip public fun enablePreparedTextLayout(): Boolean + @DoNotStrip public fun enablePropsUpdateReconciliationAndroid(): Boolean @DoNotStrip public fun enableResourceTimingAPI(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayout.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayout.kt new file mode 100644 index 00000000000..92675a02c28 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayout.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text + +import android.text.Layout +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Encapsulates an {android.text.Layout} along with any additional state needed to render or measure + * it. + */ +@DoNotStrip +internal class PreparedLayout(public val layout: Layout, public val maximumNumberOfLines: Int) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index d1512a64b67..6ab6673a178 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -31,6 +31,7 @@ import com.facebook.react.bridge.ReactNoCrashSoftException; import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.WritableArray; import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.UnstableReactNativeAPI; import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.uimanager.PixelUtil; @@ -503,7 +504,8 @@ public class TextLayoutManager { } } - private static Layout createLayout( + @UnstableReactNativeAPI + public static Layout createLayout( @NonNull Context context, MapBuffer attributedString, MapBuffer paragraphAttributes, @@ -732,6 +734,47 @@ public class TextLayoutManager { return YogaMeasureOutput.make(widthInSP, heightInSP); } + @UnstableReactNativeAPI + public static float[] measurePreparedLayout( + PreparedLayout preparedLayout, + float width, + YogaMeasureMode widthYogaMeasureMode, + float height, + YogaMeasureMode heightYogaMeasureMode) { + Layout layout = preparedLayout.getLayout(); + Spanned text = (Spanned) layout.getText(); + int maximumNumberOfLines = preparedLayout.getMaximumNumberOfLines(); + + int calculatedLineCount = calculateLineCount(layout, maximumNumberOfLines); + float calculatedWidth = + calculateWidth(layout, text, width, widthYogaMeasureMode, calculatedLineCount); + float calculatedHeight = + calculateHeight(layout, text, height, heightYogaMeasureMode, calculatedLineCount); + + ArrayList retList = new ArrayList<>(); + retList.add(PixelUtil.toDIPFromPixel(calculatedWidth)); + retList.add(PixelUtil.toDIPFromPixel(calculatedHeight)); + + AttachmentMetrics metrics = new AttachmentMetrics(); + int lastAttachmentFoundInSpan; + for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) { + lastAttachmentFoundInSpan = + nextAttachmentMetrics(layout, text, calculatedWidth, calculatedLineCount, i, metrics); + if (metrics.wasFound) { + retList.add(PixelUtil.toDIPFromPixel(metrics.top)); + retList.add(PixelUtil.toDIPFromPixel(metrics.left)); + retList.add(PixelUtil.toDIPFromPixel(metrics.width)); + retList.add(PixelUtil.toDIPFromPixel(metrics.height)); + } + } + + float[] ret = new float[retList.size()]; + for (int i = 0; i < retList.size(); i++) { + ret[i] = retList.get(i); + } + return ret; + } + private static int calculateLineCount(Layout layout, int maximumNumberOfLines) { return maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0 ? layout.getLineCount() diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 89fe373a814..23efd9f2393 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<49dbfe02e06cc5d6b12683ed91ea1d13>> */ /** @@ -189,6 +189,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool enablePreparedTextLayout() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enablePreparedTextLayout"); + return method(javaProvider_); + } + bool enablePropsUpdateReconciliationAndroid() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enablePropsUpdateReconciliationAndroid"); @@ -444,6 +450,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableNewBackgroundAndBorderDrawables( return ReactNativeFeatureFlags::enableNewBackgroundAndBorderDrawables(); } +bool JReactNativeFeatureFlagsCxxInterop::enablePreparedTextLayout( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enablePreparedTextLayout(); +} + bool JReactNativeFeatureFlagsCxxInterop::enablePropsUpdateReconciliationAndroid( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid(); @@ -655,6 +666,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableNewBackgroundAndBorderDrawables", JReactNativeFeatureFlagsCxxInterop::enableNewBackgroundAndBorderDrawables), + makeNativeMethod( + "enablePreparedTextLayout", + JReactNativeFeatureFlagsCxxInterop::enablePreparedTextLayout), makeNativeMethod( "enablePropsUpdateReconciliationAndroid", JReactNativeFeatureFlagsCxxInterop::enablePropsUpdateReconciliationAndroid), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index efd5bb770ed..73800336363 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<270b461fa199f8e6365948cded0785ad>> + * @generated SignedSource<> */ /** @@ -105,6 +105,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableNewBackgroundAndBorderDrawables( facebook::jni::alias_ref); + static bool enablePreparedTextLayout( + facebook::jni::alias_ref); + static bool enablePropsUpdateReconciliationAndroid( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 5e74d3f63bf..6c9593434f2 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<74f22c6a0302a9923e99d2455d84e231>> + * @generated SignedSource<<8410561a80edd67b4528181b1f8557fe>> */ /** @@ -126,6 +126,10 @@ bool ReactNativeFeatureFlags::enableNewBackgroundAndBorderDrawables() { return getAccessor().enableNewBackgroundAndBorderDrawables(); } +bool ReactNativeFeatureFlags::enablePreparedTextLayout() { + return getAccessor().enablePreparedTextLayout(); +} + bool ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid() { return getAccessor().enablePropsUpdateReconciliationAndroid(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 3c5f69a2fa3..798bdf289a2 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<8b135b02d868914f6b3487f09e8955ff>> */ /** @@ -164,6 +164,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableNewBackgroundAndBorderDrawables(); + /** + * Enables caching text layout artifacts for later reuse + */ + RN_EXPORT static bool enablePreparedTextLayout(); + /** * When enabled, Android will receive prop updates based on the differences between the last rendered shadow node and the last committed shadow node. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 9d33a09fe34..827bb2fe676 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4efbb3f094edbcc89c6ac4d586169b9a>> + * @generated SignedSource<<18cf18662a540ff1bc0f151cd53bcb0d>> */ /** @@ -479,6 +479,24 @@ bool ReactNativeFeatureFlagsAccessor::enableNewBackgroundAndBorderDrawables() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enablePreparedTextLayout() { + auto flagValue = enablePreparedTextLayout_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(25, "enablePreparedTextLayout"); + + flagValue = currentProvider_->enablePreparedTextLayout(); + enablePreparedTextLayout_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { auto flagValue = enablePropsUpdateReconciliationAndroid_.load(); @@ -488,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(25, "enablePropsUpdateReconciliationAndroid"); + markFlagAsAccessed(26, "enablePropsUpdateReconciliationAndroid"); flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid(); enablePropsUpdateReconciliationAndroid_ = flagValue; @@ -506,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::enableResourceTimingAPI() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(26, "enableResourceTimingAPI"); + markFlagAsAccessed(27, "enableResourceTimingAPI"); flagValue = currentProvider_->enableResourceTimingAPI(); enableResourceTimingAPI_ = flagValue; @@ -524,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSynchronousStateUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(27, "enableSynchronousStateUpdates"); + markFlagAsAccessed(28, "enableSynchronousStateUpdates"); flagValue = currentProvider_->enableSynchronousStateUpdates(); enableSynchronousStateUpdates_ = flagValue; @@ -542,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(28, "enableViewCulling"); + markFlagAsAccessed(29, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -560,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(29, "enableViewRecycling"); + markFlagAsAccessed(30, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -578,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(30, "enableViewRecyclingForText"); + markFlagAsAccessed(31, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -596,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(31, "enableViewRecyclingForView"); + markFlagAsAccessed(32, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -614,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(32, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(33, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -632,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(33, "fuseboxEnabledRelease"); + markFlagAsAccessed(34, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -650,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(34, "fuseboxNetworkInspectionEnabled"); + markFlagAsAccessed(35, "fuseboxNetworkInspectionEnabled"); flagValue = currentProvider_->fuseboxNetworkInspectionEnabled(); fuseboxNetworkInspectionEnabled_ = flagValue; @@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::incorporateMaxLinesDuringAndroidLayout() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(35, "incorporateMaxLinesDuringAndroidLayout"); + markFlagAsAccessed(36, "incorporateMaxLinesDuringAndroidLayout"); flagValue = currentProvider_->incorporateMaxLinesDuringAndroidLayout(); incorporateMaxLinesDuringAndroidLayout_ = flagValue; @@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(36, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(37, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -704,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(37, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(38, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(38, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(39, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(39, "useFabricInterop"); + markFlagAsAccessed(40, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(40, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(41, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -776,7 +794,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimizedEventBatchingOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(41, "useOptimizedEventBatchingOnAndroid"); + markFlagAsAccessed(42, "useOptimizedEventBatchingOnAndroid"); flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid(); useOptimizedEventBatchingOnAndroid_ = flagValue; @@ -794,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::useRawPropsJsiValue() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(42, "useRawPropsJsiValue"); + markFlagAsAccessed(43, "useRawPropsJsiValue"); flagValue = currentProvider_->useRawPropsJsiValue(); useRawPropsJsiValue_ = flagValue; @@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::useShadowNodeStateOnClone() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(43, "useShadowNodeStateOnClone"); + markFlagAsAccessed(44, "useShadowNodeStateOnClone"); flagValue = currentProvider_->useShadowNodeStateOnClone(); useShadowNodeStateOnClone_ = flagValue; @@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(44, "useTurboModuleInterop"); + markFlagAsAccessed(45, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(45, "useTurboModules"); + markFlagAsAccessed(46, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index e2b2d91702d..2bda63c3cb0 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<31b5c2776ad91fd0d3bd2cfc3672575b>> + * @generated SignedSource<<2736c730fcd0b82717faa0fea527d007>> */ /** @@ -57,6 +57,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableNativeCSSParsing(); bool enableNetworkEventReporting(); bool enableNewBackgroundAndBorderDrawables(); + bool enablePreparedTextLayout(); bool enablePropsUpdateReconciliationAndroid(); bool enableResourceTimingAPI(); bool enableSynchronousStateUpdates(); @@ -89,7 +90,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 46> accessedFeatureFlags_; + std::array, 47> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> animatedShouldSignalBatch_; @@ -116,6 +117,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableNativeCSSParsing_; std::atomic> enableNetworkEventReporting_; std::atomic> enableNewBackgroundAndBorderDrawables_; + std::atomic> enablePreparedTextLayout_; std::atomic> enablePropsUpdateReconciliationAndroid_; std::atomic> enableResourceTimingAPI_; std::atomic> enableSynchronousStateUpdates_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 9800c4e3cc9..c957a32385c 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<9cc33ba9f5ef67d6c98700d772bbc0de>> + * @generated SignedSource<> */ /** @@ -127,6 +127,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return true; } + bool enablePreparedTextLayout() override { + return false; + } + bool enablePropsUpdateReconciliationAndroid() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index ed1f9a9319a..4f62d55d35d 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<53e92dc49c9d5b93de14cde2fd319e92>> */ /** @@ -270,6 +270,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::enableNewBackgroundAndBorderDrawables(); } + bool enablePreparedTextLayout() override { + auto value = values_["enablePreparedTextLayout"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enablePreparedTextLayout(); + } + bool enablePropsUpdateReconciliationAndroid() override { auto value = values_["enablePropsUpdateReconciliationAndroid"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 8c46a35b505..ef0d5c3bd15 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<7ad3c1ab9913ee5b20d0f6217580a14c>> */ /** @@ -50,6 +50,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableNativeCSSParsing() = 0; virtual bool enableNetworkEventReporting() = 0; virtual bool enableNewBackgroundAndBorderDrawables() = 0; + virtual bool enablePreparedTextLayout() = 0; virtual bool enablePropsUpdateReconciliationAndroid() = 0; virtual bool enableResourceTimingAPI() = 0; virtual bool enableSynchronousStateUpdates() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index eb2d9d3a3c2..497c2cea403 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<82e2e7118453787377248101925b56d1>> + * @generated SignedSource<> */ /** @@ -169,6 +169,11 @@ bool NativeReactNativeFeatureFlags::enableNewBackgroundAndBorderDrawables( return ReactNativeFeatureFlags::enableNewBackgroundAndBorderDrawables(); } +bool NativeReactNativeFeatureFlags::enablePreparedTextLayout( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enablePreparedTextLayout(); +} + bool NativeReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index f6f7f8a79bb..01abce962fe 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<4169b34b13c62ab90d7976de908b5f16>> */ /** @@ -87,6 +87,8 @@ class NativeReactNativeFeatureFlags bool enableNewBackgroundAndBorderDrawables(jsi::Runtime& runtime); + bool enablePreparedTextLayout(jsi::Runtime& runtime); + bool enablePropsUpdateReconciliationAndroid(jsi::Runtime& runtime); bool enableResourceTimingAPI(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp index 12099f0a4a7..d7fefeeb347 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -165,15 +165,23 @@ void ParagraphShadowNode::updateStateIfNeeded(const Content& content) { Size ParagraphShadowNode::measureContent( const LayoutContext& layoutContext, const LayoutConstraints& layoutConstraints) const { + if constexpr (TextLayoutManagerExtended::supportsPreparedLayout()) { + for (const auto& preparedLayout : preparedLayouts_) { + if (preparedLayout.layoutConstraints == layoutConstraints) { + return preparedLayout.measureSize; + } + } + } + auto content = getContentWithMeasuredAttachments(layoutContext, layoutConstraints); auto attributedString = content.attributedString; if (attributedString.isEmpty()) { - // Note: `zero-width space` is insufficient in some cases (e.g. when we need - // to measure the "height" of the font). - // TODO T67606511: We will redefine the measurement of empty strings as part - // of T67606511 + // Note: `zero-width space` is insufficient in some cases (e.g. when we + // need to measure the "height" of the font). + // TODO T67606511: We will redefine the measurement of empty strings as + // part of T67606511 auto string = BaseTextShadowNode::getEmptyPlaceholder(); auto textAttributes = TextAttributes::defaultTextAttributes(); textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; @@ -186,6 +194,28 @@ Size ParagraphShadowNode::measureContent( .surfaceId = getSurfaceId(), }; + if constexpr (TextLayoutManagerExtended::supportsPreparedLayout()) { + if (ReactNativeFeatureFlags::enablePreparedTextLayout()) { + TextLayoutManagerExtended tme(*textLayoutManager_); + + auto preparedLayout = tme.prepareLayout( + attributedString, + content.paragraphAttributes, + textLayoutContext, + layoutConstraints); + auto mesaurements = tme.measurePreparedLayout( + preparedLayout, textLayoutContext, layoutConstraints); + + preparedLayouts_.push_back(PreparedLayoutResult{ + layoutConstraints, + mesaurements.size, + // PreparedLayout is not trivially copyable on all platforms + // NOLINTNEXTLINE(performance-move-const-arg) + std::move(preparedLayout)}); + return mesaurements.size; + } + } + return textLayoutManager_ ->measure( AttributedStringBox{attributedString}, @@ -206,10 +236,10 @@ Float ParagraphShadowNode::baseline( auto attributedString = content.attributedString; if (attributedString.isEmpty()) { - // Note: `zero-width space` is insufficient in some cases (e.g. when we need - // to measure the "height" of the font). - // TODO T67606511: We will redefine the measurement of empty strings as part - // of T67606511 + // Note: `zero-width space` is insufficient in some cases (e.g. when we + // need to measure the "height" of the font). + // TODO T67606511: We will redefine the measurement of empty strings as + // part of T67606511 auto string = BaseTextShadowNode::getEmptyPlaceholder(); auto textAttributes = TextAttributes::defaultTextAttributes(); textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; @@ -281,8 +311,8 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { // `paragraphShadowNode` that represents clones of `this` object. auto paragraphShadowNode = static_cast(this); // `paragraphOwningShadowNode` is owning pointer to`paragraphShadowNode` - // (besides the initial case when `paragraphShadowNode == this`), we need this - // only to keep it in memory for a while. + // (besides the initial case when `paragraphShadowNode == this`), we need + // this only to keep it in memory for a while. auto paragraphOwningShadowNode = ShadowNode::Unshared{}; react_native_assert( @@ -313,7 +343,7 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { const auto& attachmentMeasurement = measurement.attachments[i]; if (attachmentMeasurement.isClipped) { layoutableShadowNode.setLayoutMetrics( - LayoutMetrics{.displayType = DisplayType::None}); + LayoutMetrics{.frame = {}, .displayType = DisplayType::None}); continue; } @@ -333,15 +363,15 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { layoutableShadowNode.layoutTree( attachmentLayoutContext, attachmentLayoutConstrains); - // Altering the origin of the `ShadowNode` (which is defined by text layout, - // not by internal styles and state). + // Altering the origin of the `ShadowNode` (which is defined by text + // layout, not by internal styles and state). auto attachmentLayoutMetrics = layoutableShadowNode.getLayoutMetrics(); attachmentLayoutMetrics.frame.origin = attachmentOrigin; layoutableShadowNode.setLayoutMetrics(attachmentLayoutMetrics); } - // If we ended up cloning something, we need to update the list of children to - // reflect the changes that we made. + // If we ended up cloning something, we need to update the list of children + // to reflect the changes that we made. if (paragraphShadowNode != this) { this->children_ = static_cast(paragraphShadowNode)->children_; diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h index eb591ced07f..8ef0e7609da 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace facebook::react { @@ -115,6 +116,18 @@ class ParagraphShadowNode final : public ConcreteViewShadowNode< * Cached content of the subtree started from the node. */ mutable std::optional content_{}; + + /* + * Intermediate layout results generated during measurement, that may be + * reused by the platform. + */ + struct PreparedLayoutResult { + LayoutConstraints layoutConstraints; + Size measureSize; + TextLayoutManagerExtended::PreparedLayout preparedLayout{}; + }; + + mutable std::vector preparedLayouts_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h index b8dbdb6c889..6b5309ef775 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include @@ -17,7 +18,36 @@ namespace facebook::react { +template +concept TextLayoutManagerWithPreparedLayout = requires( + TextLayoutManagerT textLayoutManager, + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + TextLayoutContext layoutContext, + LayoutConstraints layoutConstraints, + typename TextLayoutManagerT::PreparedLayout preparedLayout) { + sizeof(typename TextLayoutManagerT::PreparedLayout); + { + textLayoutManager.prepareLayout( + attributedString, paragraphAttributes, layoutContext, layoutConstraints) + } -> std::same_as; + { + textLayoutManager.measurePreparedLayout( + preparedLayout, layoutContext, layoutConstraints) + } -> std::same_as; +}; + namespace detail { +template +struct PreparedLayoutT { + using type = std::nullptr_t; +}; + +template +struct PreparedLayoutT { + using type = typename T::PreparedLayout; +}; + /** * TextLayoutManagerExtended acts as an adapter for TextLayoutManager methods * which may not exist for a specific platform. Callers can check at @@ -36,6 +66,12 @@ class TextLayoutManagerExtended { }; } + static constexpr bool supportsPreparedLayout() { + return TextLayoutManagerWithPreparedLayout; + } + + using PreparedLayout = typename PreparedLayoutT::type; + TextLayoutManagerExtended(const TextLayoutManagerT& textLayoutManager) : textLayoutManager_(textLayoutManager) {} @@ -50,6 +86,33 @@ class TextLayoutManagerExtended { LOG(FATAL) << "Platform TextLayoutManager does not support measureLines"; } + PreparedLayout prepareLayout( + const AttributedString& attributedString, + const ParagraphAttributes& paragraphAttributes, + const TextLayoutContext& layoutContext, + const LayoutConstraints& layoutConstraints) const { + if constexpr (supportsPreparedLayout()) { + return textLayoutManager_.prepareLayout( + attributedString, + paragraphAttributes, + layoutContext, + layoutConstraints); + } + LOG(FATAL) << "Platform TextLayoutManager does not support prepareLayout"; + } + + TextMeasurement measurePreparedLayout( + const PreparedLayout& layout, + const TextLayoutContext& layoutContext, + const LayoutConstraints& layoutConstraints) const { + if constexpr (supportsPreparedLayout()) { + return textLayoutManager_.measurePreparedLayout( + layout, layoutContext, layoutConstraints); + } + LOG(FATAL) + << "Platform TextLayoutManager does not support measurePreparedLayout"; + } + private: const TextLayoutManagerT& textLayoutManager_; }; diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/JPreparedLayout.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/JPreparedLayout.h new file mode 100644 index 00000000000..e3cfc765694 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/JPreparedLayout.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +class JPreparedLayout : public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = + "Lcom/facebook/react/views/text/PreparedLayout;"; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index 3740dc959e7..84764829e8c 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -5,21 +5,25 @@ * LICENSE file in the root directory of this source tree. */ -#include "TextLayoutManager.h" - #include #include #include +#include #include #include #include #include #include #include +#include +#include namespace facebook::react { +static_assert(TextLayoutManagerExtended::supportsLineMeasurement()); +static_assert(TextLayoutManagerExtended::supportsPreparedLayout()); + namespace { int countAttachments(const AttributedString& attributedString) { @@ -289,4 +293,95 @@ LinesMeasurements TextLayoutManager::measureLines( return lineMeasurements; } +TextLayoutManager::PreparedLayout TextLayoutManager::prepareLayout( + const AttributedString& attributedString, + const ParagraphAttributes& paragraphAttributes, + const TextLayoutContext& layoutContext, + const LayoutConstraints& layoutConstraints) const { + const auto& fabricUIManager = + contextContainer_->at>("FabricUIManager"); + + static auto prepareLayout = + jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") + ->getMethod("prepareLayout"); + + auto attributedStringMB = + JReadableMapBuffer::createWithContents(toMapBuffer(attributedString)); + auto paragraphAttributesMB = + JReadableMapBuffer::createWithContents(toMapBuffer(paragraphAttributes)); + + // T222682416: We don't have any global cache here. We should investigate + // whether that is desirable + return {jni::make_global(prepareLayout( + fabricUIManager, + layoutContext.surfaceId, + attributedStringMB.get(), + paragraphAttributesMB.get(), + layoutConstraints.maximumSize.width, + layoutConstraints.maximumSize.height))}; +} + +TextMeasurement TextLayoutManager::measurePreparedLayout( + const PreparedLayout& preparedLayout, + const TextLayoutContext& /*layoutContext*/, + const LayoutConstraints& layoutConstraints) const { + const auto& fabricUIManager = + contextContainer_->at>("FabricUIManager"); + + static auto measurePreparedLayout = + jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") + ->getMethod( + "measurePreparedLayout"); + + auto minimumSize = layoutConstraints.minimumSize; + auto maximumSize = layoutConstraints.maximumSize; + + auto measurementsArr = measurePreparedLayout( + fabricUIManager, + preparedLayout.get(), + minimumSize.width, + maximumSize.width, + minimumSize.height, + maximumSize.height); + auto measurements = measurementsArr->getRegion( + 0, static_cast(measurementsArr->size())); + + react_native_assert(measurementsArr->size() >= 2); + react_native_assert((measurementsArr->size() - 2) % 4 == 0); + + TextMeasurement textMeasurement; + + textMeasurement.size.width = measurements[0]; + textMeasurement.size.height = measurements[1]; + + if (measurementsArr->size() > 2) { + textMeasurement.attachments.reserve((measurementsArr->size() - 2) / 4); + for (size_t i = 2; i < measurementsArr->size(); i += 4) { + auto top = measurements[i]; + auto left = measurements[i + 1]; + auto width = measurements[i + 2]; + auto height = measurements[i + 3]; + + if (std::isnan(top) || std::isnan(left)) { + textMeasurement.attachments.push_back( + TextMeasurement::Attachment{.frame = Rect{}, .isClipped = true}); + } else { + textMeasurement.attachments.push_back(TextMeasurement::Attachment{ + .frame = + {.origin = {.x = left, .y = top}, + .size = {.width = width, .height = height}}, + .isClipped = false}); + } + } + } + + return textMeasurement; +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h index 87f130d7848..249db1eba11 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -7,12 +7,16 @@ #pragma once +#include #include #include #include +#include #include #include #include + +#include #include namespace facebook::react { @@ -25,6 +29,8 @@ class TextLayoutManager; */ class TextLayoutManager { public: + using PreparedLayout = SafeReleaseJniRef>; + TextLayoutManager(const ContextContainer::Shared& contextContainer); /* @@ -67,6 +73,24 @@ class TextLayoutManager { const ParagraphAttributes& paragraphAttributes, const Size& size) const; + /** + * Create a platform representation of fully laid out text, to later be + * reused. + */ + PreparedLayout prepareLayout( + const AttributedString& attributedString, + const ParagraphAttributes& paragraphAttributes, + const TextLayoutContext& layoutContext, + const LayoutConstraints& layoutConstraints) const; + + /** + * Derive text and attachment measurements from a PreparedLayout. + */ + TextMeasurement measurePreparedLayout( + const PreparedLayout& layout, + const TextLayoutContext& layoutContext, + const LayoutConstraints& layoutConstraints) const; + private: std::shared_ptr contextContainer_; TextMeasureCache textMeasureCache_; diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 35de559773c..0cfa569445b 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -308,6 +308,16 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + enablePreparedTextLayout: { + defaultValue: false, + metadata: { + dateAdded: '2025-05-01', + description: 'Enables caching text layout artifacts for later reuse', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, enablePropsUpdateReconciliationAndroid: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 27cd2279f8a..360a2eb2b0d 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<6667fc8e4fdd2db0b54c908155b110cf>> * @flow strict */ @@ -71,6 +71,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ enableNativeCSSParsing: Getter, enableNetworkEventReporting: Getter, enableNewBackgroundAndBorderDrawables: Getter, + enablePreparedTextLayout: Getter, enablePropsUpdateReconciliationAndroid: Getter, enableResourceTimingAPI: Getter, enableSynchronousStateUpdates: Getter, @@ -258,6 +259,10 @@ export const enableNetworkEventReporting: Getter = createNativeFlagGett * Use BackgroundDrawable and BorderDrawable instead of CSSBackgroundDrawable */ export const enableNewBackgroundAndBorderDrawables: Getter = createNativeFlagGetter('enableNewBackgroundAndBorderDrawables', true); +/** + * Enables caching text layout artifacts for later reuse + */ +export const enablePreparedTextLayout: Getter = createNativeFlagGetter('enablePreparedTextLayout', false); /** * When enabled, Android will receive prop updates based on the differences between the last rendered shadow node and the last committed shadow node. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index f28e7682504..6c772803d5f 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<995633c3b12e26369518ff579f006f68>> + * @generated SignedSource<<4bf477efaec82ad8e9e4b1bc17705fd2>> * @flow strict */ @@ -49,6 +49,7 @@ export interface Spec extends TurboModule { +enableNativeCSSParsing?: () => boolean; +enableNetworkEventReporting?: () => boolean; +enableNewBackgroundAndBorderDrawables?: () => boolean; + +enablePreparedTextLayout?: () => boolean; +enablePropsUpdateReconciliationAndroid?: () => boolean; +enableResourceTimingAPI?: () => boolean; +enableSynchronousStateUpdates?: () => boolean;