From 9f24df015d32c80df56951c2fa4f93cf8ed2c89b Mon Sep 17 00:00:00 2001 From: David Vacca Date: Wed, 12 Apr 2023 15:51:01 -0700 Subject: [PATCH] Cache Spannables during rendering of Text (#36885) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36885 Create cache to hold Spannables during measurment and rendering of Text We used to have a similar cache in the past, it was removed because it seemed not have an implact. We are incorporating this again to measure while rendering 1000s of text components changelog: [internal] internal Reviewed By: cipolleschi Differential Revision: D44918981 fbshipit-source-id: 581d22978841e1b62a1709da0fec2baba6c59cea --- .../react/config/ReactFeatureFlags.java | 3 + .../text/TextLayoutManagerMapBuffer.java | 68 +++++++++++-------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index a4dfdbca6f5..2c5adaac55d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -80,6 +80,9 @@ public class ReactFeatureFlags { public static boolean dispatchPointerEvents = false; + /** Feature Flag to enable a cache of Spannable objects used by TextLayoutManagerMapBuffer */ + public static boolean enableTextSpannableCache = false; + /** Feature Flag to enable the pending event queue in fabric before mounting views */ public static boolean enableFabricPendingEventQueue = false; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java index 45ca23c7641..bc0a56384b9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java @@ -7,6 +7,7 @@ package com.facebook.react.views.text; +import static com.facebook.react.config.ReactFeatureFlags.enableTextSpannableCache; import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; @@ -30,6 +31,7 @@ import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.WritableArray; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.common.mapbuffer.MapBuffer; +import com.facebook.react.common.mapbuffer.ReadableMapBuffer; import com.facebook.react.uimanager.PixelUtil; import com.facebook.yoga.YogaConstants; import com.facebook.yoga.YogaMeasureMode; @@ -73,16 +75,20 @@ public class TextLayoutManagerMapBuffer { private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); // Specifies the amount of spannable that are stored into the {@link sSpannableCache}. - private static final short spannableCacheSize = 100; + private static final short spannableCacheSize = 10000; private static final String INLINE_VIEW_PLACEHOLDER = "0"; private static final boolean DEFAULT_INCLUDE_FONT_PADDING = true; - private static final LruCache sSpannableCache = - new LruCache<>(spannableCacheSize); + + private static final Object sCacheLock = new Object(); + private static final ConcurrentHashMap sTagToSpannableCache = new ConcurrentHashMap<>(); + private static final LruCache sSpannableCache = + new LruCache<>(spannableCacheSize); + public static void setCachedSpannabledForTag(int reactTag, @NonNull Spannable sp) { if (ENABLE_MEASURE_LOGGING) { FLog.e(TAG, "Set cached spannable for tag[" + reactTag + "]: " + sp.toString()); @@ -210,9 +216,30 @@ public class TextLayoutManagerMapBuffer { Context context, MapBuffer attributedString, @Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) { + Spannable text = null; + if (attributedString.contains(AS_KEY_CACHE_ID)) { + Integer cacheId = attributedString.getInt(AS_KEY_CACHE_ID); + text = sTagToSpannableCache.get(cacheId); + } else { + if (enableTextSpannableCache && attributedString instanceof ReadableMapBuffer) { + ReadableMapBuffer mapBuffer = (ReadableMapBuffer) attributedString; + synchronized (sCacheLock) { + text = sSpannableCache.get(mapBuffer); + if (text == null) { + text = + createSpannableFromAttributedString( + context, attributedString, reactTextViewManagerCallback); + sSpannableCache.put(mapBuffer, text); + } + } + } else { + text = + createSpannableFromAttributedString( + context, attributedString, reactTextViewManagerCallback); + } + } - return createSpannableFromAttributedString( - context, attributedString, reactTextViewManagerCallback); + return text; } private static Spannable createSpannableFromAttributedString( @@ -350,26 +377,11 @@ public class TextLayoutManagerMapBuffer { @Nullable float[] attachmentsPositions) { // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic) - TextPaint textPaint = sTextPaintInstance; - Spannable text; - if (attributedString.contains(AS_KEY_CACHE_ID)) { - int cacheId = attributedString.getInt(AS_KEY_CACHE_ID); - if (ENABLE_MEASURE_LOGGING) { - FLog.e(TAG, "Get cached spannable for cacheId[" + cacheId + "]"); - } - if (sTagToSpannableCache.containsKey(cacheId)) { - text = sTagToSpannableCache.get(cacheId); - if (ENABLE_MEASURE_LOGGING) { - FLog.e(TAG, "Text for spannable found for cacheId[" + cacheId + "]: " + text); - } - } else { - if (ENABLE_MEASURE_LOGGING) { - FLog.e(TAG, "No cached spannable found for cacheId[" + cacheId + "]"); - } - return 0; - } - } else { - text = getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback); + Spannable text = + getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback); + + if (text == null) { + return 0; } int textBreakStrategy = @@ -383,11 +395,7 @@ public class TextLayoutManagerMapBuffer { TextAttributeProps.getHyphenationFrequency( paragraphAttributes.getString(PA_KEY_HYPHENATION_FREQUENCY)); - if (text == null) { - throw new IllegalStateException("Spannable element has not been prepared in onBeforeLayout"); - } - - BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint); + BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance); Layout layout = createLayout( text,