diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 857c62ffba8..8708804f63c 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2031,7 +2031,7 @@ public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/ public fun startInspector ()V public fun stopInspector ()V public fun toggleElementInspector ()V - public fun unstable_updatePerfMonitor (Ljava/lang/String;I)V + public fun unstable_updatePerfMonitor (Ljava/lang/String;III)V } public abstract interface class com/facebook/react/devsupport/DevSupportManagerBase$CallbackWithBundleLoader { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt index a875d38ba14..b855c4b4289 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt @@ -927,10 +927,14 @@ public abstract class DevSupportManagerBase( } override fun unstable_updatePerfMonitor( - interactionName: String, + eventName: String, durationMs: Int, + responsivenessScore: Int, + ttl: Int, ) { - perfMonitorOverlayManager?.update(interactionName, durationMs) + perfMonitorOverlayManager?.update( + PerfMonitorOverlayManager.PerfMonitorUpdateData( + eventName, durationMs, responsivenessScore, ttl)) } override fun setAdditionalOptionForPackager(name: String, value: String) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/PerfMonitorOverlayViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/PerfMonitorOverlayViewManager.kt index e885b99cee8..5c67c90337d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/PerfMonitorOverlayViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/PerfMonitorOverlayViewManager.kt @@ -13,6 +13,8 @@ import android.graphics.Color import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.graphics.drawable.GradientDrawable +import android.os.Handler +import android.os.Looper import android.view.Gravity import android.view.Window import android.view.WindowManager @@ -39,6 +41,8 @@ internal class PerfMonitorOverlayViewManager( private var buttonDialog: Dialog? = null private var interactionNameLabel: TextView? = null private var durationLabel: TextView? = null + private var ttl: Int = 0 + private var hideAfterTimeoutHandler: Handler? = null override fun enable() { UiThreadUtil.runOnUiThread { @@ -63,14 +67,27 @@ internal class PerfMonitorOverlayViewManager( } } - override fun update(interactionName: String, durationMs: Int) { + override fun update(data: PerfMonitorOverlayManager.PerfMonitorUpdateData) { UiThreadUtil.runOnUiThread { ensureInitialized() - interactionNameLabel?.text = interactionName - durationLabel?.text = String.format(Locale.US, "%d ms", durationMs) + interactionNameLabel?.text = data.eventName + durationLabel?.text = String.format(Locale.US, "%d ms", data.durationMs) + durationLabel?.setTextColor(getDurationHighlightColor(data.responsivenessScore)) hasInteractionData = true + this.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()) + } } } } @@ -228,10 +245,20 @@ internal class PerfMonitorOverlayViewManager( } } + private fun getDurationHighlightColor(responsivenessScore: Int): Int { + return when (responsivenessScore) { + 3 -> COLOR_TEXT_RED + 2 -> COLOR_TEXT_YELLOW + else -> COLOR_TEXT_GREEN + } + } + private fun dpToPx(dp: Float): Float = PixelUtil.toPixelFromDIP(dp) companion object { 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_OVERLAY_BORDER = Color.parseColor("#6C6C6C") private val TEXT_SIZE_PRIMARY = 13f private val TEXT_SIZE_ACCESSORY = 9f diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/PerfMonitorOverlayManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/PerfMonitorOverlayManager.kt index d0664c6cb9d..79d7c884720 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/PerfMonitorOverlayManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/PerfMonitorOverlayManager.kt @@ -9,6 +9,13 @@ package com.facebook.react.devsupport.interfaces /** [Experimental] Interface to manage the V2 Perf Monitor overlay. */ internal interface PerfMonitorOverlayManager { + data class PerfMonitorUpdateData( + val eventName: String, + val durationMs: Int, + val responsivenessScore: Int, + val ttl: Int + ) + /** Enable the Perf Monitor overlay. Will be shown when updates are received. */ public fun enable() @@ -19,8 +26,5 @@ internal interface PerfMonitorOverlayManager { public fun reset() /** Update the state of the Perf Monitor overlay. */ - public fun update( - interactionName: String, - durationMs: Int, - ) + public fun update(data: PerfMonitorUpdateData) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/PerfMonitorV2Handler.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/PerfMonitorV2Handler.kt index 04efa3e86a7..540e522888f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/PerfMonitorV2Handler.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/PerfMonitorV2Handler.kt @@ -16,7 +16,9 @@ internal interface PerfMonitorV2Handler { /** [Experimental] Update the V2 Perf Monitor overlay with the given data. */ // FIXME(T233950466): Refactor ReactHostImpl/DevSupport setup to avoid this public API addition public fun unstable_updatePerfMonitor( - interactionName: String, + eventName: String, durationMs: Int, + responsivenessScore: Int, + ttl: Int, ) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 0e3a1abda90..a3423a5697c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -420,9 +420,14 @@ public class ReactHostImpl( } @DoNotStrip - private fun unstable_updatePerfMonitor(interactionName: String, durationMs: Int) { + private fun unstable_updatePerfMonitor( + eventName: String, + durationMs: Int, + responsivenessScore: Int, + ttl: Int + ) { if (devSupportManager is PerfMonitorV2Handler) { - devSupportManager.unstable_updatePerfMonitor(interactionName, durationMs) + devSupportManager.unstable_updatePerfMonitor(eventName, durationMs, responsivenessScore, ttl) } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp index 40988c6a535..2cc5554f6f9 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp @@ -131,7 +131,10 @@ void JReactHostInspectorTarget::unstable_onPerfMonitorUpdate( const PerfMonitorUpdateRequest& request) { if (auto javaReactHostImplStrong = javaReactHostImpl_->get()) { javaReactHostImplStrong->unstable_updatePerfMonitor( - request.interactionName, request.durationMs); + request.activeInteraction.eventName, + request.activeInteraction.duration, + request.activeInteraction.responsivenessScore, + request.activeInteraction.ttl); } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h index bb2f14ab368..6d8a5f54598 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h @@ -40,11 +40,19 @@ struct JReactHostImpl : public jni::JavaClass { void unstable_updatePerfMonitor( const std::string& interactionName, - uint16_t durationMs) { + uint16_t durationMs, + jsinspector_modern::InteractionResponsivenessScore responsivenessScore, + uint16_t ttl) { static auto method = - javaClassStatic()->getMethod, jint)>( - "unstable_updatePerfMonitor"); - method(self(), jni::make_jstring(interactionName), durationMs); + javaClassStatic() + ->getMethod, jint, jint, jint)>( + "unstable_updatePerfMonitor"); + method( + self(), + jni::make_jstring(interactionName), + durationMs, + static_cast(responsivenessScore), + ttl); } jni::local_ref> getHostMetadata() const { @@ -94,7 +102,7 @@ class JReactHostInspectorTarget void onSetPausedInDebuggerMessage( const OverlaySetPausedInDebuggerMessageRequest& request) override; void unstable_onPerfMonitorUpdate( - const PerfMonitorUpdateRequest& request) override; + const jsinspector_modern::PerfMonitorUpdateRequest& request) override; void loadNetworkResource( const jsinspector_modern::LoadNetworkResourceRequest& params, jsinspector_modern::ScopedExecutor< diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp index 45326cd2dba..190d61477da 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp @@ -272,15 +272,13 @@ void HostTarget::sendCommand(HostCommand command) { } void HostTarget::installPerfMetricsBinding() { + perfMonitorUpdateHandler_ = + std::make_unique(delegate_); perfMetricsBinding_ = std::make_unique( *this, // Used immediately "__chromium_devtools_metrics_reporter", [this](const std::string& message) { - auto payload = folly::parseJson(message); - HostTargetDelegate::PerfMonitorUpdateRequest request{ - .interactionName = payload["eventName"].asString(), - .durationMs = static_cast(payload["duration"].asInt())}; - delegate_.unstable_onPerfMonitorUpdate(request); + perfMonitorUpdateHandler_->handlePerfMetricsUpdate(message); }); } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h index 9dfa6270cc3..872af485d42 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h @@ -12,6 +12,7 @@ #include "InspectorInterfaces.h" #include "InstanceTarget.h" #include "NetworkIOAgent.h" +#include "PerfMonitorV2.h" #include "ScopedExecutor.h" #include "WeakList.h" @@ -98,11 +99,6 @@ class HostTargetDelegate : public LoadNetworkResourceDelegate { } }; - struct PerfMonitorUpdateRequest { - std::string interactionName; - uint16_t durationMs; - }; - virtual ~HostTargetDelegate() override; /** @@ -309,6 +305,7 @@ class JSINSPECTOR_EXPORT HostTarget std::shared_ptr executionContextManager_; std::shared_ptr currentInstance_{nullptr}; std::unique_ptr commandSender_; + std::unique_ptr perfMonitorUpdateHandler_; std::unique_ptr perfMetricsBinding_; /** diff --git a/packages/react-native/ReactCommon/jsinspector-modern/PerfMonitorV2.cpp b/packages/react-native/ReactCommon/jsinspector-modern/PerfMonitorV2.cpp new file mode 100644 index 00000000000..1c6a3af12f8 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/PerfMonitorV2.cpp @@ -0,0 +1,89 @@ +/* + * 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. + */ + +#include "PerfMonitorV2.h" +#include "HostTarget.h" + +#include +#include + +namespace facebook::react::jsinspector_modern { + +namespace { + +constexpr uint16_t MIN_DURATION = 10; +constexpr uint16_t DEFAULT_TTL = 4000; +constexpr uint16_t BAD_EVENT_TTL = 20000; + +InteractionResponsivenessScore getInteractionScore(uint16_t duration) { + constexpr uint16_t GOOD_THRESHOLD = 200; + constexpr uint16_t NEEDS_IMPROVEMENT_THRESHOLD = 500; + + if (duration < GOOD_THRESHOLD) { + return InteractionResponsivenessScore::Good; + } else if (duration < NEEDS_IMPROVEMENT_THRESHOLD) { + return InteractionResponsivenessScore::NeedsImprovement; + } else { + return InteractionResponsivenessScore::Poor; + } +} + +} // namespace + +void PerfMonitorUpdateHandler::handlePerfMetricsUpdate( + const std::string& message) { + auto payload = folly::parseJson(message); + + if (payload.isObject()) { + auto duration = static_cast(payload["duration"].asInt()); + + if (duration < MIN_DURATION) { + return; + } + + auto responsivenessScore = getInteractionScore(duration); + auto ttl = responsivenessScore == InteractionResponsivenessScore::Poor + ? BAD_EVENT_TTL + : DEFAULT_TTL; + + InteractionPayload newInteraction{ + payload["eventName"].asString(), + static_cast(payload["startTime"].asInt()), + duration, + responsivenessScore, + ttl}; + + if (shouldOverrideLastInteraction(newInteraction)) { + lastInteraction_ = newInteraction; + delegate_.unstable_onPerfMonitorUpdate( + PerfMonitorUpdateRequest{newInteraction}); + } + } +} + +bool PerfMonitorUpdateHandler::shouldOverrideLastInteraction( + const InteractionPayload& newInteraction) { + if (!lastInteraction_) { + return true; + } + + // Override if last event has expired + if (HighResTimeStamp::now().toDOMHighResTimeStamp() > + lastInteraction_->startTime + lastInteraction_->ttl) { + return true; + } + + // Override if same or greater responsiveness score + if (newInteraction.responsivenessScore >= + lastInteraction_->responsivenessScore) { + return true; + } + + return false; +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/PerfMonitorV2.h b/packages/react-native/ReactCommon/jsinspector-modern/PerfMonitorV2.h new file mode 100644 index 00000000000..8a4ff67ae45 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/PerfMonitorV2.h @@ -0,0 +1,60 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react::jsinspector_modern { + +class HostTargetDelegate; + +/** + * See https://web.dev/articles/inp#good-score. + */ +enum class InteractionResponsivenessScore : int32_t { + Good = 0, + NeedsImprovement = 1, + Poor = 2 +}; + +struct InteractionPayload { + std::string eventName; + uint16_t startTime; + uint16_t duration; + InteractionResponsivenessScore responsivenessScore; + uint16_t ttl; +}; + +struct PerfMonitorUpdateRequest { + InteractionPayload activeInteraction; +}; + +/** + * [Experimental] Utility to handle performance metrics updates received from + * the runtime and forward update events to the V2 Perf Monitor UI. + */ +class PerfMonitorUpdateHandler { + public: + PerfMonitorUpdateHandler(HostTargetDelegate& delegate) + : delegate_(delegate) {} + + /** + * Handle a new "__chromium_devtools_metrics_reporter" message. + */ + void handlePerfMetricsUpdate(const std::string& message); + + private: + HostTargetDelegate& delegate_; + std::optional lastInteraction_; + + bool shouldOverrideLastInteraction(const InteractionPayload& newInteraction); +}; + +} // namespace facebook::react::jsinspector_modern