Switch Perf Monitor metric to Long Tasks (#53297)

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

Pivots our display metric for the V2 Perf Monitor experiment by switching to Long Tasks.

- Implements a new "__ReactNative__LongTask" metrics event (note: prefixed, since this sits outside the Web Vitals spec).

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D79556595

fbshipit-source-id: 239cf44884f67bf62295b92e2262ae1811d17e4a
This commit is contained in:
Alex Hunt
2025-08-21 03:06:52 -07:00
committed by Facebook GitHub Bot
parent 70d5c97ab4
commit 62b8a7b4ef
14 changed files with 70 additions and 55 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;III)V
public fun unstable_updatePerfMonitor (III)V
}
public abstract interface class com/facebook/react/devsupport/DevSupportManagerBase$CallbackWithBundleLoader {
@@ -937,18 +937,12 @@ public abstract class DevSupportManagerBase(
}
override fun unstable_updatePerfMonitor(
eventName: String,
durationMs: Int,
longTaskDuration: Int,
responsivenessScore: Int,
ttl: Int,
) {
perfMonitorOverlayManager?.update(
PerfMonitorOverlayManager.PerfMonitorUpdateData(
eventName,
durationMs,
responsivenessScore,
ttl,
)
PerfMonitorOverlayManager.PerfMonitorUpdateData(longTaskDuration, responsivenessScore, ttl)
)
}
@@ -39,7 +39,6 @@ internal class PerfMonitorOverlayViewManager(
private var hasInteractionData: Boolean = false
private var interactionDialog: Dialog? = null
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
@@ -70,7 +69,6 @@ internal class PerfMonitorOverlayViewManager(
override fun update(data: PerfMonitorOverlayManager.PerfMonitorUpdateData) {
UiThreadUtil.runOnUiThread {
ensureInitialized()
interactionNameLabel?.text = data.eventName
durationLabel?.text = String.format(Locale.US, "%d ms", data.durationMs)
durationLabel?.setTextColor(getDurationHighlightColor(data.responsivenessScore))
hasInteractionData = true
@@ -115,9 +113,10 @@ internal class PerfMonitorOverlayViewManager(
private fun createDialog(context: Context) {
val containerLayout = createInnerLayout(context)
interactionNameLabel =
val longTaskLabel =
TextView(context).apply {
textSize = TEXT_SIZE_PRIMARY
text = "Long Task"
setTextColor(Color.WHITE)
typeface = TYPEFACE_BOLD
}
@@ -127,7 +126,7 @@ internal class PerfMonitorOverlayViewManager(
setTextColor(COLOR_TEXT_GREEN)
typeface = TYPEFACE_BOLD
}
containerLayout.addView(interactionNameLabel)
containerLayout.addView(longTaskLabel)
containerLayout.addView(durationLabel)
val dialog =
@@ -9,12 +9,7 @@ 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,
)
data class PerfMonitorUpdateData(val durationMs: Int, val responsivenessScore: Int, val ttl: Int)
/** Enable the Perf Monitor overlay. Will be shown when updates are received. */
public fun enable()
@@ -16,8 +16,7 @@ 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(
eventName: String,
durationMs: Int,
longTaskDuration: Int,
responsivenessScore: Int,
ttl: Int,
)
@@ -421,13 +421,12 @@ public class ReactHostImpl(
@DoNotStrip
private fun unstable_updatePerfMonitor(
eventName: String,
durationMs: Int,
longTaskDuration: Int,
responsivenessScore: Int,
ttl: Int,
) {
if (devSupportManager is PerfMonitorV2Handler) {
devSupportManager.unstable_updatePerfMonitor(eventName, durationMs, responsivenessScore, ttl)
devSupportManager.unstable_updatePerfMonitor(longTaskDuration, responsivenessScore, ttl)
}
}
@@ -131,7 +131,6 @@ void JReactHostInspectorTarget::unstable_onPerfMonitorUpdate(
const PerfMonitorUpdateRequest& request) {
if (auto javaReactHostImplStrong = javaReactHostImpl_->get()) {
javaReactHostImplStrong->unstable_updatePerfMonitor(
request.activeInteraction.eventName,
request.activeInteraction.duration,
request.activeInteraction.responsivenessScore,
request.activeInteraction.ttl);
@@ -39,20 +39,13 @@ struct JReactHostImpl : public jni::JavaClass<JReactHostImpl> {
}
void unstable_updatePerfMonitor(
const std::string& interactionName,
uint16_t durationMs,
uint16_t longTaskDuration,
jsinspector_modern::InteractionResponsivenessScore responsivenessScore,
uint16_t ttl) {
static auto method =
javaClassStatic()
->getMethod<void(jni::local_ref<jni::JString>, jint, jint, jint)>(
"unstable_updatePerfMonitor");
static auto method = javaClassStatic()->getMethod<void(jint, jint, jint)>(
"unstable_updatePerfMonitor");
method(
self(),
jni::make_jstring(interactionName),
durationMs,
static_cast<jint>(responsivenessScore),
ttl);
self(), longTaskDuration, static_cast<jint>(responsivenessScore), ttl);
}
jni::local_ref<jni::JMap<jstring, jstring>> getHostMetadata() const {
@@ -15,7 +15,7 @@ namespace facebook::react::jsinspector_modern {
namespace {
constexpr uint16_t MIN_DURATION = 10;
constexpr uint16_t MIN_DURATION = 200;
constexpr uint16_t DEFAULT_TTL = 4000;
constexpr uint16_t BAD_EVENT_TTL = 20000;
@@ -39,6 +39,10 @@ void PerfMonitorUpdateHandler::handlePerfMetricsUpdate(
auto payload = folly::parseJson(message);
if (payload.isObject()) {
if (payload["name"] != "__ReactNative__LongTask") {
return;
}
auto duration = static_cast<uint16_t>(payload["duration"].asInt());
if (duration < MIN_DURATION) {
@@ -50,36 +54,34 @@ void PerfMonitorUpdateHandler::handlePerfMetricsUpdate(
? BAD_EVENT_TTL
: DEFAULT_TTL;
InteractionPayload newInteraction{
payload["eventName"].asString(),
LongTaskPayload newEvent{
static_cast<uint16_t>(payload["startTime"].asInt()),
duration,
responsivenessScore,
ttl};
if (shouldOverrideLastInteraction(newInteraction)) {
lastInteraction_ = newInteraction;
if (shouldOverrideLastEvent(newEvent)) {
lastEvent_ = newEvent;
delegate_.unstable_onPerfMonitorUpdate(
PerfMonitorUpdateRequest{newInteraction});
PerfMonitorUpdateRequest{newEvent});
}
}
}
bool PerfMonitorUpdateHandler::shouldOverrideLastInteraction(
const InteractionPayload& newInteraction) {
if (!lastInteraction_) {
bool PerfMonitorUpdateHandler::shouldOverrideLastEvent(
const LongTaskPayload& newEvent) {
if (!lastEvent_) {
return true;
}
// Override if last event has expired
if (HighResTimeStamp::now().toDOMHighResTimeStamp() >
lastInteraction_->startTime + lastInteraction_->ttl) {
lastEvent_->startTime + lastEvent_->ttl) {
return true;
}
// Override if same or greater responsiveness score
if (newInteraction.responsivenessScore >=
lastInteraction_->responsivenessScore) {
if (newEvent.responsivenessScore >= lastEvent_->responsivenessScore) {
return true;
}
@@ -24,8 +24,7 @@ enum class InteractionResponsivenessScore : int32_t {
Poor = 2
};
struct InteractionPayload {
std::string eventName;
struct LongTaskPayload {
uint16_t startTime;
uint16_t duration;
InteractionResponsivenessScore responsivenessScore;
@@ -33,7 +32,7 @@ struct InteractionPayload {
};
struct PerfMonitorUpdateRequest {
InteractionPayload activeInteraction;
LongTaskPayload activeInteraction;
};
/**
@@ -42,7 +41,7 @@ struct PerfMonitorUpdateRequest {
*/
class PerfMonitorUpdateHandler {
public:
PerfMonitorUpdateHandler(HostTargetDelegate& delegate)
explicit PerfMonitorUpdateHandler(HostTargetDelegate& delegate)
: delegate_(delegate) {}
/**
@@ -52,9 +51,9 @@ class PerfMonitorUpdateHandler {
private:
HostTargetDelegate& delegate_;
std::optional<InteractionPayload> lastInteraction_;
std::optional<LongTaskPayload> lastEvent_;
bool shouldOverrideLastInteraction(const InteractionPayload& newInteraction);
bool shouldOverrideLastEvent(const LongTaskPayload& newInteraction);
};
} // namespace facebook::react::jsinspector_modern
@@ -72,4 +72,27 @@ void CdpMetricsReporter::onEventTimingEntry(
});
}
void CdpMetricsReporter::onLongTaskEntry(
const PerformanceLongTaskTiming& entry) {
runtimeExecutor_([entry = std::move(entry)](jsi::Runtime& runtime) {
auto global = runtime.global();
if (!global.hasProperty(runtime, metricsReporterName.data())) {
return;
}
folly::dynamic jsonPayload = folly::dynamic::object;
jsonPayload["name"] = "__ReactNative__LongTask";
jsonPayload["duration"] =
static_cast<int>(entry.duration.toDOMHighResTimeStamp());
jsonPayload["startTime"] =
static_cast<int>(entry.startTime.toDOMHighResTimeStamp());
auto jsonString = folly::toJson(jsonPayload);
auto jsiString = jsi::String::createFromUtf8(runtime, jsonString);
auto metricsReporter =
global.getPropertyAsFunction(runtime, metricsReporterName.data());
metricsReporter.call(runtime, jsiString);
});
}
} // namespace facebook::react
@@ -28,6 +28,7 @@ class CdpMetricsReporter : public PerformanceEntryReporterEventTimingListener {
explicit CdpMetricsReporter(RuntimeExecutor runtimeExecutor);
void onEventTimingEntry(const PerformanceEventTiming& entry) override;
void onLongTaskEntry(const PerformanceLongTaskTiming& entry) override;
private:
const RuntimeExecutor runtimeExecutor_{};
@@ -297,6 +297,16 @@ void PerformanceEntryReporter::reportLongTask(
}
observerRegistry_->queuePerformanceEntry(entry);
std::vector<PerformanceEntryReporterEventTimingListener*> listenersCopy;
{
std::shared_lock lock(listenersMutex_);
listenersCopy = eventTimingListeners_;
}
for (auto* listener : listenersCopy) {
listener->onLongTaskEntry(entry);
}
}
void PerformanceEntryReporter::reportResourceTiming(
@@ -18,7 +18,9 @@ class PerformanceEntryReporterEventTimingListener {
public:
virtual ~PerformanceEntryReporterEventTimingListener() = default;
virtual void onEventTimingEntry(const PerformanceEventTiming& entry) = 0;
virtual void onEventTimingEntry(const PerformanceEventTiming& /*entry*/) {}
virtual void onLongTaskEntry(const PerformanceLongTaskTiming& /*entry*/) {}
};
} // namespace facebook::react