Wire up background tracing and actions in Perf Monitor (#53460)

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

Completes the first pass of adding background performance trace controls in the V2 Perf Monitor UI.

Key changes:
- Initiates background trace on `PerfMonitorOverlayManager::init`.
- Wires up background trace recording states in button dialog, and connects `pauseAndAnalyzeBackgroundTrace` and `resumeBackgroundTrace` actions.
- Moves UI manager/view classes into `perfmonitor` subpackage.
- Fixes `responsivenessScore` determination/UI coloring.
- Adds tooltip UI to the overlay view.

Changelog: [Internal]

Reviewed By: javache

Differential Revision: D80807554

fbshipit-source-id: 53360c2d454adfbba40fd795d400b28d90ff9e61
This commit is contained in:
Alex Hunt
2025-09-09 10:31:04 -07:00
committed by Facebook GitHub Bot
parent 226d2ba160
commit cdc4a8ead4
9 changed files with 250 additions and 140 deletions
@@ -83,6 +83,6 @@ internal class BridgelessDevSupportManager(
}
fun tracingState(): TracingState {
return TracingState.DISABLED
return TracingState.ENABLEDINCDPMODE
}
}
@@ -68,6 +68,7 @@ import com.facebook.react.devsupport.interfaces.StackFrame
import com.facebook.react.devsupport.interfaces.TracingState
import com.facebook.react.devsupport.interfaces.TracingStateProvider
import com.facebook.react.devsupport.perfmonitor.PerfMonitorDevHelper
import com.facebook.react.devsupport.perfmonitor.PerfMonitorOverlayManager
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
import com.facebook.react.modules.core.RCTNativeAppEventEmitter
@@ -77,6 +78,7 @@ import java.io.File
import java.net.MalformedURLException
import java.net.URL
import java.util.Locale
import javax.inject.Provider
public abstract class DevSupportManagerBase(
protected val applicationContext: Context,
@@ -182,7 +184,7 @@ public abstract class DevSupportManagerBase(
null
}
private var perfMonitorOverlayManager: PerfMonitorOverlayViewManager? = null
private var perfMonitorOverlayManager: PerfMonitorOverlayManager? = null
private var tracingStateProvider: TracingStateProvider? = null
init {
@@ -215,19 +217,19 @@ public abstract class DevSupportManagerBase(
if (
ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() &&
ReactNativeFeatureFlags.perfMonitorV2Enabled() &&
reactInstanceDevHelper is PerfMonitorDevHelper &&
perfMonitorOverlayManager == null
reactInstanceDevHelper is PerfMonitorDevHelper
) {
perfMonitorOverlayManager =
PerfMonitorOverlayViewManager(
Supplier {
PerfMonitorOverlayManager(
reactInstanceDevHelper,
Provider {
val context = reactInstanceDevHelper.currentActivity
if (context == null || context.isFinishing) {
return@Supplier null
return@Provider null
}
context
},
reactInstanceDevHelper.inspectorTarget,
{ openDebugger(DebuggerFrontendPanelName.PERFORMANCE.toString()) },
)
}
}
@@ -548,10 +550,11 @@ public abstract class DevSupportManagerBase(
override fun onNewReactContextCreated(reactContext: ReactContext) {
resetCurrentContext(reactContext)
if (perfMonitorOverlayManager != null && reactInstanceDevHelper is PerfMonitorDevHelper) {
if (reactInstanceDevHelper is PerfMonitorDevHelper) {
perfMonitorOverlayManager?.let { manager ->
reactInstanceDevHelper.inspectorTarget?.addPerfMonitorListener(manager)
}
perfMonitorOverlayManager?.enable()
}
}
@@ -882,6 +885,7 @@ public abstract class DevSupportManagerBase(
override fun onPackagerDisconnected() {
isPackagerConnected = false
perfMonitorOverlayManager?.disable()
}
override fun onPackagerReloadCommand() {
@@ -1,20 +0,0 @@
/*
* 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.devsupport.interfaces
/** [Experimental] Interface to manage the V2 Perf Monitor overlay. */
internal interface PerfMonitorOverlayManager {
/** Enable the Perf Monitor overlay. Will be shown when updates are received. */
public fun enable()
/** Disable the Perf Monitor overlay. Will remain hidden when updates are received. */
public fun disable()
/** Reset the Perf Monitor overlay, e.g. after a reload. */
public fun reset()
}
@@ -7,11 +7,16 @@
package com.facebook.react.devsupport.perfmonitor
import com.facebook.react.devsupport.interfaces.TracingState
/**
* [Experimental] Interface implemented by [com.facebook.react.runtime.ReactHostInspectorTarget]
* exposing actions for the V2 Perf Monitor.
*/
internal interface PerfMonitorInspectorTargetBinding {
/** Get the current CDP or background performance tracing state. */
public fun getTracingState(): TracingState
/** Attempt to pause the current background performance trace, and open in DevTools. */
public fun pauseAndAnalyzeBackgroundTrace()
@@ -0,0 +1,86 @@
/*
* 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.devsupport.perfmonitor
import android.content.Context
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.devsupport.interfaces.TracingState
import javax.inject.Provider
internal class PerfMonitorOverlayManager(
private val devHelper: PerfMonitorDevHelper,
private val contextProvider: Provider<Context?>,
private val onRequestOpenDevTools: () -> Unit,
) : PerfMonitorUpdateListener {
private var enabled: Boolean = false
private var initialized: Boolean = false
private var view: PerfMonitorOverlayView? = null
private var tracingState: TracingState = TracingState.ENABLEDINCDPMODE
private fun init() {
if (initialized || !enabled) {
return
}
UiThreadUtil.runOnUiThread {
val context = contextProvider.get() ?: return@runOnUiThread
view = PerfMonitorOverlayView(context, ::handleRecordingButtonPress)
// Start background tracing
devHelper.inspectorTarget?.resumeBackgroundTrace()
view?.show()
initialized = true
}
}
/** Enable the Perf Monitor overlay. Will be shown when updates are received. */
fun enable() {
enabled = true
init()
UiThreadUtil.runOnUiThread { view?.show() }
}
/** Disable the Perf Monitor overlay. Will remain hidden when updates are received. */
fun disable() {
UiThreadUtil.runOnUiThread { view?.hide() }
enabled = false
}
/** Reset the Perf Monitor overlay, e.g. after a reload. */
fun reset() {
UiThreadUtil.runOnUiThread { view?.resetState() }
// Update with current recording state
onRecordingStateChanged(
devHelper.inspectorTarget?.getTracingState() ?: TracingState.ENABLEDINCDPMODE
)
}
override fun onNewFocusedEvent(data: PerfMonitorUpdateListener.LongTaskEventData) {
view?.updateFocusedEvent(data)
}
override fun onRecordingStateChanged(state: TracingState) {
tracingState = state
view?.updateRecordingState(state)
}
private fun handleRecordingButtonPress() {
when (tracingState) {
TracingState.ENABLEDINBACKGROUNDMODE -> {
devHelper.inspectorTarget?.pauseAndAnalyzeBackgroundTrace()
onRequestOpenDevTools()
}
TracingState.DISABLED -> {
devHelper.inspectorTarget?.resumeBackgroundTrace()
}
TracingState.ENABLEDINCDPMODE -> Unit
}
}
}
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.devsupport
package com.facebook.react.devsupport.perfmonitor
import android.app.Dialog
import android.content.Context
@@ -20,107 +20,110 @@ import android.view.Window
import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.util.Supplier
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.facebook.react.R
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.devsupport.interfaces.PerfMonitorOverlayManager
import com.facebook.react.devsupport.interfaces.TracingState
import com.facebook.react.devsupport.perfmonitor.PerfMonitorInspectorTargetBinding
import com.facebook.react.devsupport.perfmonitor.PerfMonitorUpdateListener
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.PixelUtil
import java.util.Locale
internal class PerfMonitorOverlayViewManager(
private val contextSupplier: Supplier<Context?>,
private val inspectorTarget: PerfMonitorInspectorTargetBinding?,
) : PerfMonitorOverlayManager, PerfMonitorUpdateListener {
private var initialized: Boolean = false
private var enabled: Boolean = false
private var hasInteractionData: Boolean = false
private var interactionDialog: Dialog? = null
private var buttonDialog: Dialog? = null
private var durationLabel: TextView? = null
internal class PerfMonitorOverlayView(
private val context: Context,
private val onButtonPress: () -> Unit,
) {
private var hidden: Boolean = true
private var hasEventData: Boolean = false
private val metricsDialog: Dialog
private val toolbarDialog: Dialog
private val tooltipDialog: Dialog
private lateinit var buttonLabel: TextView
private lateinit var recordingStateLabel: TextView
private lateinit var durationLabel: TextView
private lateinit var tooltipLabel: TextView
private var ttl: Int = 0
private var hideAfterTimeoutHandler: Handler? = null
override fun enable() {
UiThreadUtil.runOnUiThread {
enabled = true
if (hasInteractionData) {
showOverlay()
}
}
}
override fun disable() {
UiThreadUtil.runOnUiThread {
enabled = false
hideOverlay()
}
}
override fun reset() {
UiThreadUtil.runOnUiThread {
hasInteractionData = false
hideOverlay()
}
}
override fun onRecordingStateChanged(state: TracingState) {
// recordingState = state
// view?.updateRecordingState(state)
}
override fun onNewFocusedEvent(data: PerfMonitorUpdateListener.LongTaskEventData) {
UiThreadUtil.runOnUiThread {
ensureInitialized()
durationLabel?.text = String.format(Locale.US, "%d ms", data.durationMs)
durationLabel?.setTextColor(getDurationHighlightColor(data.responsivenessScore))
hasInteractionData = true
ttl = data.ttl
hideAfterTimeoutHandler?.removeCallbacksAndMessages(null)
if (enabled) {
showOverlay()
// Schedule hiding overlay after ttl milliseconds
if (ttl > 0) {
if (hideAfterTimeoutHandler == null) {
hideAfterTimeoutHandler = Handler(Looper.getMainLooper())
}
hideAfterTimeoutHandler?.postDelayed({ hideOverlay() }, ttl.toLong())
}
}
}
}
private fun ensureInitialized() {
if (initialized) {
return
}
val context = contextSupplier.get() ?: return
init {
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(context)
createDialog(context)
createButton(context)
initialized = true
tooltipDialog = createTooltipDialog()
metricsDialog = createMetricsDialog()
toolbarDialog = createToolbarDialog()
}
private fun showOverlay() {
interactionDialog?.show()
buttonDialog?.show()
fun show() {
toolbarDialog.show()
tooltipDialog.show()
if (hasEventData) {
toolbarDialog.window?.decorView?.post { updateMetricsDialogPosition() }
metricsDialog.show()
}
hidden = false
}
private fun hideOverlay() {
interactionDialog?.hide()
buttonDialog?.hide()
fun hide() {
metricsDialog.hide()
toolbarDialog.hide()
tooltipDialog.hide()
hidden = true
}
private fun createDialog(context: Context) {
val containerLayout = createInnerLayout(context)
fun resetState() {
hasEventData = false
metricsDialog.hide()
}
fun updateFocusedEvent(data: PerfMonitorUpdateListener.LongTaskEventData) {
durationLabel.text = String.format(Locale.US, "%d ms", data.durationMs)
durationLabel.setTextColor(getDurationHighlightColor(data.responsivenessScore))
hasEventData = true
ttl = data.ttl
hideAfterTimeoutHandler?.removeCallbacksAndMessages(null)
if (!hidden) {
metricsDialog.show()
// Schedule hiding metrics overlay after ttl milliseconds
if (ttl > 0) {
if (hideAfterTimeoutHandler == null) {
hideAfterTimeoutHandler = Handler(Looper.getMainLooper())
}
hideAfterTimeoutHandler?.postDelayed({ metricsDialog.hide() }, ttl.toLong())
}
}
}
fun updateRecordingState(state: TracingState) {
recordingStateLabel.text =
when (state) {
TracingState.ENABLEDINBACKGROUNDMODE -> "Profiling: ON"
TracingState.DISABLED -> "Profiling: OFF"
TracingState.ENABLEDINCDPMODE -> "Profiling: DISABLED"
}
buttonLabel.text =
when (state) {
TracingState.ENABLEDINBACKGROUNDMODE -> "Open ↗️"
TracingState.DISABLED -> "Start"
TracingState.ENABLEDINCDPMODE -> ""
}
tooltipLabel.text =
when (state) {
TracingState.ENABLEDINBACKGROUNDMODE -> "Dev Menu > Finish performance trace"
TracingState.DISABLED -> "Dev Menu > Start performance trace"
TracingState.ENABLEDINCDPMODE -> ""
}
if (state == TracingState.ENABLEDINCDPMODE) {
tooltipDialog.hide()
} else {
tooltipDialog.show()
}
toolbarDialog.window?.decorView?.post { updateMetricsDialogPosition() }
}
private fun createMetricsDialog(): Dialog {
val containerLayout = createInnerLayout()
val longTaskLabel =
TextView(context).apply {
textSize = TEXT_SIZE_PRIMARY
@@ -138,7 +141,7 @@ internal class PerfMonitorOverlayViewManager(
containerLayout.addView(durationLabel)
val dialog =
createAnchoredDialog(context, dpToPx(140f), dpToPx(16f)).apply {
createAnchoredDialog(getMetricsDialogOffsetX(), dpToPx(16f)).apply {
setContentView(containerLayout)
}
dialog.window?.apply {
@@ -151,30 +154,25 @@ internal class PerfMonitorOverlayViewManager(
}
}
this.interactionDialog = dialog
return dialog
}
fun updateRecordingState(state: TracingState) {}
private fun createButton(context: Context) {
val buttonInner = createInnerLayout(context)
buttonInner.addView(
private fun createToolbarDialog(): Dialog {
val buttonInner = createInnerLayout()
recordingStateLabel =
TextView(context).apply {
text = "Analyze"
textSize = TEXT_SIZE_PRIMARY
setTextColor(Color.WHITE)
typeface = TYPEFACE_BOLD
}
)
buttonInner.addView(
buttonInner.addView(recordingStateLabel)
buttonLabel =
TextView(context).apply {
text = "cmd + A"
textSize = TEXT_SIZE_ACCESSORY
setTextColor(Color.WHITE)
alpha = 0.7f
textSize = TEXT_SIZE_PRIMARY
setTextColor(COLOR_TEXT_BLUE)
typeface = TYPEFACE_BOLD
}
)
buttonInner.addView(buttonLabel)
val buttonView =
LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
@@ -185,19 +183,43 @@ internal class PerfMonitorOverlayViewManager(
dpToPx(8f).toInt(),
)
addView(buttonInner)
setOnClickListener { inspectorTarget?.pauseAndAnalyzeBackgroundTrace() }
setOnClickListener { onButtonPress() }
}
val dialog =
createAnchoredDialog(context, dpToPx(0f), dpToPx(0f)).apply { setContentView(buttonView) }
val dialog = createAnchoredDialog(dpToPx(0f), dpToPx(0f)).apply { setContentView(buttonView) }
dialog.window?.apply {
attributes =
attributes?.apply { flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE }
}
this.buttonDialog = dialog
return dialog
}
private fun createAnchoredDialog(context: Context, offsetX: Float, offsetY: Float): Dialog {
private fun createTooltipDialog(): Dialog {
val containerLayout = createInnerLayout()
tooltipLabel =
TextView(context).apply {
textSize = TEXT_SIZE_ACCESSORY
setTextColor(Color.WHITE)
}
containerLayout.addView(tooltipLabel)
val dialog =
createAnchoredDialog(dpToPx(16f), dpToPx(52f)).apply { setContentView(containerLayout) }
dialog.window?.apply {
attributes =
attributes?.apply {
flags =
flags or
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
}
}
return dialog
}
private fun createAnchoredDialog(offsetX: Float, offsetY: Float): Dialog {
val dialog =
Dialog(context, R.style.NoAnimationDialog).apply {
requestWindowFeature(Window.FEATURE_NO_TITLE)
@@ -228,12 +250,12 @@ internal class PerfMonitorOverlayViewManager(
return dialog
}
private fun createInnerLayout(context: Context): LinearLayout {
private fun createInnerLayout(): LinearLayout {
return LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
val paddingHorizontal = dpToPx(14f).toInt()
val paddingVertical = dpToPx(8f).toInt()
val paddingVertical = dpToPx(7f).toInt()
setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
layoutParams =
LinearLayout.LayoutParams(
@@ -256,10 +278,21 @@ internal class PerfMonitorOverlayViewManager(
}
}
private fun getMetricsDialogOffsetX(): Float {
val toolbarWidth = toolbarDialog?.window?.decorView?.width ?: 0
return toolbarWidth.toFloat()
}
private fun updateMetricsDialogPosition() {
metricsDialog?.window?.apply {
attributes = attributes?.apply { x = getMetricsDialogOffsetX().toInt() }
}
}
private fun getDurationHighlightColor(responsivenessScore: Int): Int {
return when (responsivenessScore) {
3 -> COLOR_TEXT_RED
2 -> COLOR_TEXT_YELLOW
2 -> COLOR_TEXT_RED
1 -> COLOR_TEXT_YELLOW
else -> COLOR_TEXT_GREEN
}
}
@@ -270,6 +303,7 @@ internal class PerfMonitorOverlayViewManager(
private val COLOR_TEXT_GREEN = Color.parseColor("#4AEB2F")
private val COLOR_TEXT_YELLOW = Color.parseColor("#FFAA00")
private val COLOR_TEXT_RED = Color.parseColor("#FF0000")
private val COLOR_TEXT_BLUE = Color.parseColor("#00B0FF")
private val COLOR_OVERLAY_BORDER = Color.parseColor("#6C6C6C")
private val TEXT_SIZE_PRIMARY = 13f
private val TEXT_SIZE_ACCESSORY = 9f
@@ -81,6 +81,6 @@ internal class ReactHostImplDevHelper(private val delegate: ReactHostImpl) :
delegate.loadBundle(bundleLoader)
override fun getTracingState(): TracingState {
return delegate.reactHostInspectorTarget?.tracingState() ?: TracingState.DISABLED
return delegate.reactHostInspectorTarget?.getTracingState() ?: TracingState.ENABLEDINCDPMODE
}
}
@@ -43,7 +43,7 @@ internal class ReactHostInspectorTarget(reactHostImpl: ReactHostImpl) :
external fun tracingStateAsInt(): Int
fun tracingState(): TracingState {
override fun getTracingState(): TracingState {
return TracingState.entries[tracingStateAsInt()]
}
@@ -50,7 +50,8 @@ void PerfMonitorUpdateHandler::handlePerfMetricsUpdate(
}
auto responsivenessScore = getInteractionScore(duration);
auto ttl = responsivenessScore == InteractionResponsivenessScore::Poor
auto ttl =
responsivenessScore == InteractionResponsivenessScore::NeedsImprovement
? BAD_EVENT_TTL
: DEFAULT_TTL;