@@ -12,7 +12,10 @@ import android.content.Intent
1212import android.content.res.Configuration
1313import android.os.Bundle
1414import android.view.KeyEvent
15+ import androidx.activity.ComponentActivity
1516import com.facebook.react.bridge.ReactContext
17+ import com.facebook.react.bridge.ReactMarker
18+ import com.facebook.react.bridge.ReactMarkerConstants
1619import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
1720import com.facebook.react.devsupport.DoubleTapReloadRecognizer
1821import 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