Implement Perf Monitor event scoring and display timeout (#53169)

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

Improves the experimental Perf Monitor UI sufficient for the initial MVP.

- Sets minimum duration threshold to display an event to 10ms.
- Impelements [responsiveness scoring](https://web.dev/articles/inp#good-score) linked to UI colour and display timeout.

Changelog: [Internal]

Reviewed By: rubennorte

Differential Revision: D79359131

fbshipit-source-id: f08d2b595e885342d841c3a02b9e02456502c926
This commit is contained in:
Alex Hunt
2025-08-19 11:19:09 -07:00
committed by Facebook GitHub Bot
parent defefb19e3
commit df748ba083
12 changed files with 226 additions and 29 deletions
@@ -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 {
@@ -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) {
@@ -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
@@ -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)
}
@@ -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,
)
}
@@ -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)
}
}
@@ -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);
}
}
@@ -40,11 +40,19 @@ struct JReactHostImpl : public jni::JavaClass<JReactHostImpl> {
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<void(jni::local_ref<jni::JString>, jint)>(
"unstable_updatePerfMonitor");
method(self(), jni::make_jstring(interactionName), durationMs);
javaClassStatic()
->getMethod<void(jni::local_ref<jni::JString>, jint, jint, jint)>(
"unstable_updatePerfMonitor");
method(
self(),
jni::make_jstring(interactionName),
durationMs,
static_cast<jint>(responsivenessScore),
ttl);
}
jni::local_ref<jni::JMap<jstring, jstring>> 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<
@@ -272,15 +272,13 @@ void HostTarget::sendCommand(HostCommand command) {
}
void HostTarget::installPerfMetricsBinding() {
perfMonitorUpdateHandler_ =
std::make_unique<PerfMonitorUpdateHandler>(delegate_);
perfMetricsBinding_ = std::make_unique<HostRuntimeBinding>(
*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<uint16_t>(payload["duration"].asInt())};
delegate_.unstable_onPerfMonitorUpdate(request);
perfMonitorUpdateHandler_->handlePerfMetricsUpdate(message);
});
}
@@ -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> executionContextManager_;
std::shared_ptr<InstanceTarget> currentInstance_{nullptr};
std::unique_ptr<HostCommandSender> commandSender_;
std::unique_ptr<PerfMonitorUpdateHandler> perfMonitorUpdateHandler_;
std::unique_ptr<HostRuntimeBinding> perfMetricsBinding_;
/**
@@ -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 <folly/json.h>
#include <react/timing/primitives.h>
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<uint16_t>(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<uint16_t>(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
@@ -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 <cstdint>
#include <optional>
#include <string>
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<InteractionPayload> lastInteraction_;
bool shouldOverrideLastInteraction(const InteractionPayload& newInteraction);
};
} // namespace facebook::react::jsinspector_modern