Skip to content

Commit d544780

Browse files
committed
feat: Automatically call FullyDrawnReporter to improve production PGOs
1 parent c3caea9 commit d544780

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public class com/facebook/react/ReactActivityDelegate {
102102
public fun getReactInstanceManager ()Lcom/facebook/react/ReactInstanceManager;
103103
protected fun getReactNativeHost ()Lcom/facebook/react/ReactNativeHost;
104104
protected fun isFabricEnabled ()Z
105+
protected fun isFullyDrawnReportingEnabled ()Z
105106
protected fun isWideColorGamutEnabled ()Z
106107
protected fun loadApp (Ljava/lang/String;)V
107108
public fun onActivityResult (IILandroid/content/Intent;)V
@@ -138,6 +139,7 @@ public class com/facebook/react/ReactDelegate {
138139
public final fun getReactInstanceManager ()Lcom/facebook/react/ReactInstanceManager;
139140
public final fun getReactRootView ()Lcom/facebook/react/ReactRootView;
140141
protected final fun isFabricEnabled ()Z
142+
public final fun isFullyDrawnReportingEnabled ()Z
141143
public final fun loadApp ()V
142144
public final fun loadApp (Ljava/lang/String;)V
143145
public final fun onActivityResult (IILandroid/content/Intent;Z)V
@@ -152,6 +154,7 @@ public class com/facebook/react/ReactDelegate {
152154
public final fun onUserLeaveHint ()V
153155
public final fun onWindowFocusChanged (Z)V
154156
public final fun reload ()V
157+
public final fun setFullyDrawnReportingEnabled (Z)V
155158
public final fun setReactRootView (Lcom/facebook/react/ReactRootView;)V
156159
public final fun setReactSurface (Lcom/facebook/react/interfaces/fabric/ReactSurface;)V
157160
public final fun shouldShowDevMenuOrReload (ILandroid/view/KeyEvent;)Z

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ protected ReactRootView createRootView() {
168168
}
169169
};
170170
}
171+
mReactDelegate.setFullyDrawnReportingEnabled(isFullyDrawnReportingEnabled());
171172
if (mainComponentName != null) {
172173
LocalNetworkPermissionUtil.requestLocalNetworkAccessIfNeeded(
173174
getPlainActivity(), () -> loadApp(mainComponentName));
@@ -322,4 +323,21 @@ protected boolean isFabricEnabled() {
322323
protected boolean isWideColorGamutEnabled() {
323324
return false;
324325
}
326+
327+
/**
328+
* Controls whether {@link Activity#reportFullyDrawn()} is automatically triggered once the
329+
* initial content of the React surface has appeared, via the activity's {@code
330+
* androidx.activity.FullyDrawnReporter}. Android uses this signal to bound the startup window for
331+
* profile guided compilation (Android 12+) and to report time-to-fully-drawn in Android vitals.
332+
*
333+
* <p>Apps that consider themselves fully drawn only later (e.g. once their initial data has been
334+
* rendered) can keep this enabled and additionally register their own reporter on the activity's
335+
* {@code FullyDrawnReporter} before the content appears, or override this method to return false
336+
* to opt out entirely.
337+
*
338+
* @return true if fully drawn reporting is enabled for this Activity, false otherwise.
339+
*/
340+
protected boolean isFullyDrawnReportingEnabled() {
341+
return true;
342+
}
325343
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.kt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import android.content.Intent
1212
import android.content.res.Configuration
1313
import android.os.Bundle
1414
import android.view.KeyEvent
15+
import androidx.activity.ComponentActivity
1516
import com.facebook.react.bridge.ReactContext
17+
import com.facebook.react.bridge.ReactMarker
18+
import com.facebook.react.bridge.ReactMarkerConstants
1619
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
1720
import com.facebook.react.devsupport.DoubleTapReloadRecognizer
1821
import com.facebook.react.devsupport.ReleaseDevSupportManager
@@ -51,6 +54,27 @@ public open class ReactDelegate {
5154
*/
5255
protected val isFabricEnabled: Boolean = true
5356

57+
/**
58+
* Controls whether this delegate reports the host [Activity] as fully drawn once the initial
59+
* content of the React surface has appeared.
60+
*
61+
* When enabled (the default) and the host [Activity] is a [ComponentActivity], a reporter is
62+
* added to the activity's [androidx.activity.FullyDrawnReporter] when the surface starts loading
63+
* and removed once the first view of the surface is mounted, which calls
64+
* [Activity.reportFullyDrawn] after the next frame is drawn. Android uses this signal to bound
65+
* the startup window for profile guided compilation (Android 12+) and to report
66+
* time-to-fully-drawn in Android vitals.
67+
*
68+
* Apps that consider themselves fully drawn only later (e.g. once their initial data has been
69+
* rendered) can keep this enabled and additionally register their own reporter on the activity's
70+
* [androidx.activity.FullyDrawnReporter] before the content appears.
71+
*
72+
* Must be set before [loadApp] to take effect.
73+
*/
74+
public var isFullyDrawnReportingEnabled: Boolean = true
75+
76+
private var fullyDrawnMarkerListener: ReactMarker.MarkerListener? = null
77+
5478
/**
5579
* Do not use this constructor as it's not accounting for New Architecture at all. You should use
5680
* [ReactDelegate(Activity, ReactNativeHost, String, Bundle, boolean)] as it's the constructor
@@ -318,6 +342,9 @@ public open class ReactDelegate {
318342
* @param appKey The ID of the app to load into the surface.
319343
*/
320344
public fun loadApp(appKey: String) {
345+
if (isFullyDrawnReportingEnabled) {
346+
reportFullyDrawnWhenContentAppears(appKey)
347+
}
321348
// With Bridgeless enabled, create and start the surface
322349
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
323350
val reactHost = reactHost
@@ -340,6 +367,7 @@ public open class ReactDelegate {
340367

341368
/** Stop the React surface started with [ReactDelegate.loadApp]. */
342369
public fun unloadApp() {
370+
cancelFullyDrawnReporting()
343371
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
344372
reactSurface?.stop()
345373
reactSurface = null
@@ -351,6 +379,41 @@ public open class ReactDelegate {
351379
}
352380
}
353381

382+
private fun reportFullyDrawnWhenContentAppears(appKey: String) {
383+
if (fullyDrawnMarkerListener != null) {
384+
return
385+
}
386+
val fullyDrawnReporter = (activity as? ComponentActivity)?.fullyDrawnReporter ?: return
387+
if (fullyDrawnReporter.isFullyDrawnReported) {
388+
return
389+
}
390+
fullyDrawnReporter.addReporter()
391+
val listener =
392+
object : ReactMarker.MarkerListener {
393+
override fun logMarker(name: ReactMarkerConstants, tag: String?, instanceKey: Int) {
394+
if (name == ReactMarkerConstants.CONTENT_APPEARED && tag == appKey) {
395+
// CONTENT_APPEARED is logged on the UI thread, so this cannot race with
396+
// cancelFullyDrawnReporting or a second loadApp.
397+
if (fullyDrawnMarkerListener !== this) {
398+
return
399+
}
400+
fullyDrawnMarkerListener = null
401+
ReactMarker.removeListener(this)
402+
fullyDrawnReporter.removeReporter()
403+
}
404+
}
405+
}
406+
fullyDrawnMarkerListener = listener
407+
ReactMarker.addListener(listener)
408+
}
409+
410+
private fun cancelFullyDrawnReporting() {
411+
val listener = fullyDrawnMarkerListener ?: return
412+
fullyDrawnMarkerListener = null
413+
ReactMarker.removeListener(listener)
414+
(activity as? ComponentActivity)?.fullyDrawnReporter?.removeReporter()
415+
}
416+
354417
public fun setReactSurface(reactSurface: ReactSurface?) {
355418
this.reactSurface = reactSurface
356419
}

0 commit comments

Comments
 (0)