Skip to content

Commit

Permalink
Cache Spannables during rendering of Text (#36885)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #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
  • Loading branch information
mdvacca authored and pull[bot] committed Dec 31, 2023
1 parent 082054a commit 7d7f839
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,7 @@
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;
Expand Down Expand Up @@ -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<MapBuffer, Spannable> sSpannableCache =
new LruCache<>(spannableCacheSize);

private static final Object sCacheLock = new Object();

private static final ConcurrentHashMap<Integer, Spannable> sTagToSpannableCache =
new ConcurrentHashMap<>();

private static final LruCache<ReadableMapBuffer, Spannable> 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());
Expand Down Expand Up @@ -210,9 +216,30 @@ public static Spannable getOrCreateSpannableForText(
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(
Expand Down Expand Up @@ -350,26 +377,11 @@ public static long measureText(
@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 =
Expand All @@ -383,11 +395,7 @@ public static long measureText(
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,
Expand Down

0 comments on commit 7d7f839

Please sign in to comment.