Add trace markers to ReactTextView (#46214)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/46214

This tries to represent a few operations which have previously been observed to be costly in a sampling profiler (showing more granularity than the trace events):
1. TextView getting measurements via `onMeasure()` when updating layout metrics during mount, which may [trigger UI-thread text layout](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/TextView.java;l=11217).

2. Text drawing, which may do layout as well

3. State updates, where we construct a new Spannable and set content to it

Changelog: [Internal]

Reviewed By: tdn120, mdvacca

Differential Revision: D61705770

fbshipit-source-id: 199a6c65c18296f2ff948642701a331ba656e9d9
This commit is contained in:
Nick Gerleman
2024-08-26 12:28:20 -07:00
committed by Facebook GitHub Bot
parent b86e8ef95f
commit 1ffe74a5dc
4 changed files with 134 additions and 93 deletions
@@ -7663,6 +7663,7 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
protected fun onDraw (Landroid/graphics/Canvas;)V
public fun onFinishTemporaryDetach ()V
protected fun onLayout (ZIIII)V
protected fun onMeasure (II)V
public fun onStartTemporaryDetach ()V
public fun reactTagForTouch (FF)I
public fun setAdjustFontSizeToFit (Z)V
@@ -0,0 +1,23 @@
/*
* 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.internal
import com.facebook.systrace.Systrace
/**
* Helper to guarantee firing Systrace begin and end markers around a try with resources statement.
*/
public class SystraceSection(sectionName: String) : AutoCloseable {
init {
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, sectionName)
}
override fun close() {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE)
}
}
@@ -35,6 +35,7 @@ import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.internal.SystraceSection;
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
import com.facebook.react.uimanager.BackgroundStyleApplicator;
import com.facebook.react.uimanager.LengthPercentage;
@@ -375,85 +376,96 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
@Override
protected void onDraw(Canvas canvas) {
if (mAdjustsFontSizeToFit && getSpanned() != null && mShouldAdjustSpannableFontSize) {
mShouldAdjustSpannableFontSize = false;
TextLayoutManager.adjustSpannableFontToFit(
getSpanned(),
getWidth(),
YogaMeasureMode.EXACTLY,
getHeight(),
YogaMeasureMode.EXACTLY,
mMinimumFontSize,
mNumberOfLines,
getIncludeFontPadding(),
getBreakStrategy(),
getHyphenationFrequency(),
// always passing ALIGN_NORMAL here should be fine, since this method doesn't depend on
// how exacly lines are aligned, just their width
Layout.Alignment.ALIGN_NORMAL);
setText(getSpanned());
}
if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) {
if (mOverflow != Overflow.VISIBLE) {
BackgroundStyleApplicator.clipToPaddingBox(this, canvas);
try (SystraceSection s = new SystraceSection("ReactTextView.onDraw")) {
if (mAdjustsFontSizeToFit && getSpanned() != null && mShouldAdjustSpannableFontSize) {
mShouldAdjustSpannableFontSize = false;
TextLayoutManager.adjustSpannableFontToFit(
getSpanned(),
getWidth(),
YogaMeasureMode.EXACTLY,
getHeight(),
YogaMeasureMode.EXACTLY,
mMinimumFontSize,
mNumberOfLines,
getIncludeFontPadding(),
getBreakStrategy(),
getHyphenationFrequency(),
// always passing ALIGN_NORMAL here should be fine, since this method doesn't depend on
// how exacly lines are aligned, just their width
Layout.Alignment.ALIGN_NORMAL);
setText(getSpanned());
}
} else {
mReactBackgroundManager.maybeClipToPaddingBox(canvas);
}
super.onDraw(canvas);
if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) {
if (mOverflow != Overflow.VISIBLE) {
BackgroundStyleApplicator.clipToPaddingBox(this, canvas);
}
} else {
mReactBackgroundManager.maybeClipToPaddingBox(canvas);
}
super.onDraw(canvas);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
try (SystraceSection s = new SystraceSection("ReactTextView.onMeasure")) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
public void setText(ReactTextUpdate update) {
mContainsImages = update.containsImages();
// Android's TextView crashes when it tries to relayout if LayoutParams are
// null; explicitly set the LayoutParams to prevent this crash. See:
// https://github.com/facebook/react-native/pull/7011
if (getLayoutParams() == null) {
setLayoutParams(EMPTY_LAYOUT_PARAMS);
}
Spannable spannable = update.getText();
if (mLinkifyMaskType > 0) {
Linkify.addLinks(spannable, mLinkifyMaskType);
setMovementMethod(LinkMovementMethod.getInstance());
}
setText(spannable);
float paddingLeft = update.getPaddingLeft();
float paddingTop = update.getPaddingTop();
float paddingRight = update.getPaddingRight();
float paddingBottom = update.getPaddingBottom();
// In Fabric padding is set by the update of Layout Metrics and not as part of the "setText"
// operation
// TODO T56559197: remove this condition when we migrate 100% to Fabric
if (paddingLeft != ReactConstants.UNSET
&& paddingTop != ReactConstants.UNSET
&& paddingRight != ReactConstants.UNSET
&& paddingBottom != ReactConstants.UNSET) {
setPadding(
(int) Math.floor(paddingLeft),
(int) Math.floor(paddingTop),
(int) Math.floor(paddingRight),
(int) Math.floor(paddingBottom));
}
int nextTextAlign = update.getTextAlign();
if (nextTextAlign != getGravityHorizontal()) {
setGravityHorizontal(nextTextAlign);
}
if (getBreakStrategy() != update.getTextBreakStrategy()) {
setBreakStrategy(update.getTextBreakStrategy());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (getJustificationMode() != update.getJustificationMode()) {
setJustificationMode(update.getJustificationMode());
try (SystraceSection s = new SystraceSection("ReactTextView.setText(ReactTextUpdate)")) {
mContainsImages = update.containsImages();
// Android's TextView crashes when it tries to relayout if LayoutParams are
// null; explicitly set the LayoutParams to prevent this crash. See:
// https://github.com/facebook/react-native/pull/7011
if (getLayoutParams() == null) {
setLayoutParams(EMPTY_LAYOUT_PARAMS);
}
}
Spannable spannable = update.getText();
if (mLinkifyMaskType > 0) {
Linkify.addLinks(spannable, mLinkifyMaskType);
setMovementMethod(LinkMovementMethod.getInstance());
}
setText(spannable);
float paddingLeft = update.getPaddingLeft();
float paddingTop = update.getPaddingTop();
float paddingRight = update.getPaddingRight();
float paddingBottom = update.getPaddingBottom();
// Ensure onLayout is called so the inline views can be repositioned.
requestLayout();
// In Fabric padding is set by the update of Layout Metrics and not as part of the "setText"
// operation
// TODO T56559197: remove this condition when we migrate 100% to Fabric
if (paddingLeft != ReactConstants.UNSET
&& paddingTop != ReactConstants.UNSET
&& paddingRight != ReactConstants.UNSET
&& paddingBottom != ReactConstants.UNSET) {
setPadding(
(int) Math.floor(paddingLeft),
(int) Math.floor(paddingTop),
(int) Math.floor(paddingRight),
(int) Math.floor(paddingBottom));
}
int nextTextAlign = update.getTextAlign();
if (nextTextAlign != getGravityHorizontal()) {
setGravityHorizontal(nextTextAlign);
}
if (getBreakStrategy() != update.getTextBreakStrategy()) {
setBreakStrategy(update.getTextBreakStrategy());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (getJustificationMode() != update.getJustificationMode()) {
setJustificationMode(update.getJustificationMode());
}
}
// Ensure onLayout is called so the inline views can be repositioned.
requestLayout();
}
}
@Override
@@ -17,6 +17,7 @@ import com.facebook.react.R;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.common.mapbuffer.MapBuffer;
import com.facebook.react.internal.SystraceSection;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.IViewManagerWithChildren;
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
@@ -86,24 +87,26 @@ public class ReactTextViewManager
@Override
public void updateExtraData(ReactTextView view, Object extraData) {
ReactTextUpdate update = (ReactTextUpdate) extraData;
Spannable spannable = update.getText();
if (update.containsImages()) {
TextInlineImageSpan.possiblyUpdateInlineImageSpans(spannable, view);
}
view.setText(update);
try (SystraceSection s = new SystraceSection("ReactTextViewManager.updateExtraData")) {
ReactTextUpdate update = (ReactTextUpdate) extraData;
Spannable spannable = update.getText();
if (update.containsImages()) {
TextInlineImageSpan.possiblyUpdateInlineImageSpans(spannable, view);
}
view.setText(update);
// If this text view contains any clickable spans, set a view tag and reset the accessibility
// delegate so that these can be picked up by the accessibility system.
ReactClickableSpan[] clickableSpans =
spannable.getSpans(0, update.getText().length(), ReactClickableSpan.class);
// If this text view contains any clickable spans, set a view tag and reset the accessibility
// delegate so that these can be picked up by the accessibility system.
ReactClickableSpan[] clickableSpans =
spannable.getSpans(0, update.getText().length(), ReactClickableSpan.class);
if (clickableSpans.length > 0) {
view.setTag(
R.id.accessibility_links,
new ReactAccessibilityDelegate.AccessibilityLinks(clickableSpans, spannable));
ReactAccessibilityDelegate.resetDelegate(
view, view.isFocusable(), view.getImportantForAccessibility());
if (clickableSpans.length > 0) {
view.setTag(
R.id.accessibility_links,
new ReactAccessibilityDelegate.AccessibilityLinks(clickableSpans, spannable));
ReactAccessibilityDelegate.resetDelegate(
view, view.isFocusable(), view.getImportantForAccessibility());
}
}
}
@@ -135,11 +138,13 @@ public class ReactTextViewManager
@Override
public Object updateState(
ReactTextView view, ReactStylesDiffMap props, StateWrapper stateWrapper) {
MapBuffer stateMapBuffer = stateWrapper.getStateDataMapBuffer();
if (stateMapBuffer != null) {
return getReactTextUpdate(view, props, stateMapBuffer);
} else {
return null;
try (SystraceSection s = new SystraceSection("ReactTextViewManager.updateState")) {
MapBuffer stateMapBuffer = stateWrapper.getStateDataMapBuffer();
if (stateMapBuffer != null) {
return getReactTextUpdate(view, props, stateMapBuffer);
} else {
return null;
}
}
}