Skip to content

Commit 9a4df1c

Browse files
huntiefacebook-github-bot
authored andcommitted
Add initial UI for Perf Monitor V2 prototype (#52971)
Summary: Pull Request resolved: #52971 Wires up the initial pass of the experimental V2 Perf Monitor UI. Limitations: - Does not yet clear last interaction. - Only lightly tested. Changelog: [Internal] Reviewed By: rubennorte Differential Revision: D78904767 fbshipit-source-id: c51c5f51d9267ec971c17dce465775a2a3e6cb2c
1 parent 41029d8 commit 9a4df1c

8 files changed

Lines changed: 342 additions & 10 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1965,7 +1965,7 @@ public abstract interface class com/facebook/react/devsupport/DevServerHelper$Pa
19651965
public abstract fun onPackagerReloadCommand ()V
19661966
}
19671967

1968-
public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/facebook/react/devsupport/interfaces/DevSupportManager {
1968+
public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/facebook/react/devsupport/interfaces/DevSupportManager, com/facebook/react/devsupport/interfaces/PerfMonitorV2Handler {
19691969
public static final field Companion Lcom/facebook/react/devsupport/DevSupportManagerBase$Companion;
19701970
public fun <init> (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;)V
19711971
public fun addCustomDevOption (Ljava/lang/String;Lcom/facebook/react/devsupport/interfaces/DevOptionHandler;)V
@@ -2023,6 +2023,7 @@ public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/
20232023
public fun startInspector ()V
20242024
public fun stopInspector ()V
20252025
public fun toggleElementInspector ()V
2026+
public fun unstable_updatePerfMonitor (Ljava/lang/String;I)V
20262027
}
20272028

20282029
public abstract interface class com/facebook/react/devsupport/DevSupportManagerBase$CallbackWithBundleLoader {

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

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,12 @@ import com.facebook.react.devsupport.interfaces.ErrorCustomizer
6262
import com.facebook.react.devsupport.interfaces.ErrorType
6363
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback
6464
import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager
65+
import com.facebook.react.devsupport.interfaces.PerfMonitorOverlayManager
66+
import com.facebook.react.devsupport.interfaces.PerfMonitorV2Handler
6567
import com.facebook.react.devsupport.interfaces.RedBoxHandler
6668
import com.facebook.react.devsupport.interfaces.StackFrame
69+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
70+
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
6771
import com.facebook.react.modules.core.RCTNativeAppEventEmitter
6872
import com.facebook.react.modules.debug.interfaces.DeveloperSettings
6973
import com.facebook.react.packagerconnection.RequestHandler
@@ -84,7 +88,7 @@ public abstract class DevSupportManagerBase(
8488
private val surfaceDelegateFactory: SurfaceDelegateFactory?,
8589
public var devLoadingViewManager: DevLoadingViewManager?,
8690
private var pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?,
87-
) : DevSupportManager {
91+
) : DevSupportManager, PerfMonitorV2Handler {
8892

8993
public interface CallbackWithBundleLoader {
9094
public fun onSuccess(bundleLoader: JSBundleLoader)
@@ -176,6 +180,8 @@ public abstract class DevSupportManagerBase(
176180
null
177181
}
178182

183+
private var perfMonitorOverlayManager: PerfMonitorOverlayManager? = null
184+
179185
init {
180186
// We store JS bundle loaded from dev server in a single destination in app's data dir.
181187
// In case when someone schedule 2 subsequent reloads it may happen that JS thread will
@@ -202,6 +208,20 @@ public abstract class DevSupportManagerBase(
202208
context
203209
})
204210
}
211+
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() &&
212+
ReactNativeFeatureFlags.perfMonitorV2Enabled() &&
213+
perfMonitorOverlayManager == null) {
214+
perfMonitorOverlayManager =
215+
PerfMonitorOverlayViewManager(
216+
Supplier {
217+
val context = reactInstanceDevHelper.currentActivity
218+
if (context == null || context.isFinishing) {
219+
return@Supplier null
220+
}
221+
context
222+
},
223+
{ openDebugger() })
224+
}
205225
}
206226

207227
override fun handleException(e: Exception) {
@@ -793,11 +813,14 @@ public abstract class DevSupportManagerBase(
793813
devLoadingViewManager?.showMessage("Reloading...")
794814
}
795815

816+
perfMonitorOverlayManager?.reset()
817+
796818
devServerHelper.openPackagerConnection(
797819
javaClass.simpleName,
798820
object : PackagerCommandListener {
799821
override fun onPackagerConnected() {
800822
isPackagerConnected = true
823+
perfMonitorOverlayManager?.enable()
801824
}
802825

803826
override fun onPackagerDisconnected() {
@@ -837,14 +860,11 @@ public abstract class DevSupportManagerBase(
837860
isReceiverRegistered = false
838861
}
839862

840-
// hide redbox dialog
841863
hideRedboxDialog()
842-
843-
// hide dev options dialog
844864
hideDevOptionsDialog()
845-
846-
// hide loading view
847865
devLoadingViewManager?.hide()
866+
perfMonitorOverlayManager?.reset()
867+
848868
devServerHelper.closePackagerConnection()
849869
}
850870
}
@@ -902,6 +922,13 @@ public abstract class DevSupportManagerBase(
902922
pausedInDebuggerOverlayManager?.hidePausedInDebuggerOverlay()
903923
}
904924

925+
override fun unstable_updatePerfMonitor(
926+
interactionName: String,
927+
durationMs: Int,
928+
) {
929+
perfMonitorOverlayManager?.update(interactionName, durationMs)
930+
}
931+
905932
override fun setAdditionalOptionForPackager(name: String, value: String) {
906933
devSettings.packagerConnectionSettings.setAdditionalOptionForPackager(name, value)
907934
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.devsupport
9+
10+
import android.app.Dialog
11+
import android.content.Context
12+
import android.graphics.Color
13+
import android.graphics.Typeface
14+
import android.graphics.drawable.ColorDrawable
15+
import android.graphics.drawable.GradientDrawable
16+
import android.view.Gravity
17+
import android.view.Window
18+
import android.view.WindowManager
19+
import android.widget.LinearLayout
20+
import android.widget.TextView
21+
import androidx.core.util.Supplier
22+
import androidx.core.view.ViewCompat
23+
import androidx.core.view.WindowInsetsCompat
24+
import com.facebook.react.R
25+
import com.facebook.react.bridge.UiThreadUtil
26+
import com.facebook.react.devsupport.interfaces.PerfMonitorOverlayManager
27+
import com.facebook.react.uimanager.DisplayMetricsHolder
28+
import com.facebook.react.uimanager.PixelUtil
29+
import java.util.Locale
30+
31+
internal class PerfMonitorOverlayViewManager(
32+
private val contextSupplier: Supplier<Context?>,
33+
private val onRequestAnalyzeTrace: () -> Unit
34+
) : PerfMonitorOverlayManager {
35+
private var initialized: Boolean = false
36+
private var enabled: Boolean = false
37+
private var hasInteractionData: Boolean = false
38+
private var interactionDialog: Dialog? = null
39+
private var buttonDialog: Dialog? = null
40+
private var interactionNameLabel: TextView? = null
41+
private var durationLabel: TextView? = null
42+
43+
override fun enable() {
44+
UiThreadUtil.runOnUiThread {
45+
enabled = true
46+
if (hasInteractionData) {
47+
showOverlay()
48+
}
49+
}
50+
}
51+
52+
override fun disable() {
53+
UiThreadUtil.runOnUiThread {
54+
enabled = false
55+
hideOverlay()
56+
}
57+
}
58+
59+
override fun reset() {
60+
UiThreadUtil.runOnUiThread {
61+
hasInteractionData = false
62+
hideOverlay()
63+
}
64+
}
65+
66+
override fun update(interactionName: String, durationMs: Int) {
67+
UiThreadUtil.runOnUiThread {
68+
ensureInitialized()
69+
interactionNameLabel?.text = interactionName
70+
durationLabel?.text = String.format(Locale.US, "%d ms", durationMs)
71+
hasInteractionData = true
72+
if (enabled) {
73+
showOverlay()
74+
}
75+
}
76+
}
77+
78+
private fun ensureInitialized() {
79+
if (initialized) {
80+
return
81+
}
82+
val context = contextSupplier.get() ?: return
83+
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(context)
84+
createDialog(context)
85+
createButton(context)
86+
initialized = true
87+
}
88+
89+
private fun showOverlay() {
90+
interactionDialog?.show()
91+
buttonDialog?.show()
92+
}
93+
94+
private fun hideOverlay() {
95+
interactionDialog?.hide()
96+
buttonDialog?.hide()
97+
}
98+
99+
private fun createDialog(context: Context) {
100+
val containerLayout = createInnerLayout(context)
101+
interactionNameLabel =
102+
TextView(context).apply {
103+
textSize = TEXT_SIZE_PRIMARY
104+
setTextColor(Color.WHITE)
105+
typeface = TYPEFACE_BOLD
106+
}
107+
durationLabel =
108+
TextView(context).apply {
109+
textSize = TEXT_SIZE_PRIMARY
110+
setTextColor(COLOR_TEXT_GREEN)
111+
typeface = TYPEFACE_BOLD
112+
}
113+
containerLayout.addView(interactionNameLabel)
114+
containerLayout.addView(durationLabel)
115+
116+
val dialog =
117+
createAnchoredDialog(context, dpToPx(140f), dpToPx(16f)).apply {
118+
setContentView(containerLayout)
119+
}
120+
dialog.window?.apply {
121+
attributes =
122+
attributes?.apply {
123+
flags =
124+
flags or
125+
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
126+
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
127+
}
128+
}
129+
130+
this.interactionDialog = dialog
131+
}
132+
133+
private fun createButton(context: Context) {
134+
val buttonInner = createInnerLayout(context)
135+
buttonInner.addView(
136+
TextView(context).apply {
137+
text = "Analyze"
138+
textSize = TEXT_SIZE_PRIMARY
139+
setTextColor(Color.WHITE)
140+
typeface = TYPEFACE_BOLD
141+
})
142+
buttonInner.addView(
143+
TextView(context).apply {
144+
text = "cmd + A"
145+
textSize = TEXT_SIZE_ACCESSORY
146+
setTextColor(Color.WHITE)
147+
alpha = 0.7f
148+
typeface = TYPEFACE_BOLD
149+
})
150+
val buttonView =
151+
LinearLayout(context).apply {
152+
orientation = LinearLayout.VERTICAL
153+
setPadding(
154+
dpToPx(8f).toInt(), dpToPx(16f).toInt(), dpToPx(16f).toInt(), dpToPx(8f).toInt())
155+
addView(buttonInner)
156+
setOnClickListener { onRequestAnalyzeTrace() }
157+
}
158+
val dialog =
159+
createAnchoredDialog(context, dpToPx(0f), dpToPx(0f)).apply { setContentView(buttonView) }
160+
dialog.window?.apply {
161+
attributes =
162+
attributes?.apply { flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE }
163+
}
164+
165+
this.buttonDialog = dialog
166+
}
167+
168+
private fun createAnchoredDialog(context: Context, offsetX: Float, offsetY: Float): Dialog {
169+
val dialog =
170+
Dialog(context, R.style.NoAnimationDialog).apply {
171+
requestWindowFeature(Window.FEATURE_NO_TITLE)
172+
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
173+
window?.setDimAmount(0f)
174+
setCancelable(false)
175+
}
176+
dialog.window?.apply {
177+
attributes =
178+
attributes?.apply {
179+
width = WindowManager.LayoutParams.WRAP_CONTENT
180+
height = WindowManager.LayoutParams.WRAP_CONTENT
181+
gravity = Gravity.TOP or Gravity.END
182+
x = offsetX.toInt()
183+
y = offsetY.toInt()
184+
}
185+
}
186+
dialog.window?.decorView?.let { decorView ->
187+
ViewCompat.setOnApplyWindowInsetsListener(decorView) { view, windowInsets ->
188+
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
189+
val layoutParams = (view.layoutParams as WindowManager.LayoutParams)
190+
layoutParams.y = insets.top + offsetY.toInt()
191+
dialog.window?.attributes = layoutParams
192+
WindowInsetsCompat.CONSUMED
193+
}
194+
}
195+
196+
return dialog
197+
}
198+
199+
private fun createInnerLayout(context: Context): LinearLayout {
200+
return LinearLayout(context).apply {
201+
orientation = LinearLayout.HORIZONTAL
202+
gravity = Gravity.CENTER_VERTICAL
203+
val paddingHorizontal = dpToPx(14f).toInt()
204+
val paddingVertical = dpToPx(8f).toInt()
205+
setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
206+
layoutParams =
207+
LinearLayout.LayoutParams(
208+
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
209+
background =
210+
GradientDrawable().apply {
211+
shape = GradientDrawable.RECTANGLE
212+
setColor(Color.BLACK)
213+
cornerRadius = dpToPx(14.5f)
214+
alpha = (0.8 * 255).toInt()
215+
setStroke(dpToPx(1f).toInt(), COLOR_OVERLAY_BORDER)
216+
}
217+
showDividers = LinearLayout.SHOW_DIVIDER_MIDDLE
218+
dividerDrawable =
219+
object : ColorDrawable(Color.TRANSPARENT) {
220+
override fun getIntrinsicWidth(): Int = dpToPx(8f).toInt()
221+
}
222+
}
223+
}
224+
225+
private fun dpToPx(dp: Float): Float = PixelUtil.toPixelFromDIP(dp)
226+
227+
companion object {
228+
private val COLOR_TEXT_GREEN = Color.parseColor("#4AEB2F")
229+
private val COLOR_OVERLAY_BORDER = Color.parseColor("#6C6C6C")
230+
private val TEXT_SIZE_PRIMARY = 13f
231+
private val TEXT_SIZE_ACCESSORY = 9f
232+
private val TYPEFACE_BOLD = Typeface.create("sans-serif", Typeface.BOLD)
233+
}
234+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.devsupport.interfaces
9+
10+
/** [Experimental] Interface to manage the V2 Perf Monitor overlay. */
11+
internal interface PerfMonitorOverlayManager {
12+
/** Enable the Perf Monitor overlay. Will be shown when updates are received. */
13+
public fun enable()
14+
15+
/** Disable the Perf Monitor overlay. Will remain hidden when updates are received. */
16+
public fun disable()
17+
18+
/** Reset the Perf Monitor overlay, e.g. after a reload. */
19+
public fun reset()
20+
21+
/** Update the state of the Perf Monitor overlay. */
22+
public fun update(
23+
interactionName: String,
24+
durationMs: Int,
25+
)
26+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.devsupport.interfaces
9+
10+
/**
11+
* [Experimental] Interface used by [com.facebook.react.devsupport.BridgeDevSupportManagerBase]
12+
* implement the UI for the V2 Perf Monitor overlay.
13+
*/
14+
internal interface PerfMonitorV2Handler {
15+
16+
/** [Experimental] Update the V2 Perf Monitor overlay with the given data. */
17+
// FIXME(T233950466): Refactor ReactHostImpl/DevSupport setup to avoid this public API addition
18+
public fun unstable_updatePerfMonitor(
19+
interactionName: String,
20+
durationMs: Int,
21+
)
22+
}

0 commit comments

Comments
 (0)