From c9a0ee97f4258d6c829ca22ecc0e181b329c9429 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Wed, 27 Aug 2025 10:49:00 +0100 Subject: [PATCH 1/3] report when all initial resources are loaded --- front_end/core/host/RNPerfMetrics.ts | 24 ++++++++++++++++++++---- front_end/core/sdk/PageResourceLoader.ts | 10 ++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/front_end/core/host/RNPerfMetrics.ts b/front_end/core/host/RNPerfMetrics.ts index 1955fd039c41..3169eee9e789 100644 --- a/front_end/core/host/RNPerfMetrics.ts +++ b/front_end/core/host/RNPerfMetrics.ts @@ -186,6 +186,15 @@ class RNPerfMetrics { }); } + allInitialDeveloperResourcesLoadingFinished(count: number): void { + this.sendEvent({ + eventName: 'DeveloperResource.AllInitialLoadingFinished', + params: { + count, + }, + }); + } + fuseboxSetClientMetadataStarted(): void { this.sendEvent({eventName: 'FuseboxSetClientMetadataStarted'}); } @@ -417,6 +426,13 @@ export type DeveloperResourceLoadingFinishedEvent = Readonly<{ }>, }>; +export type AllInitialDeveloperResourcesLoadingFinished = Readonly<{ + eventName: 'DeveloperResource.AllInitialLoadingFinished', + params: Readonly<{ + count: number, + }>, +}>; + export type FuseboxSetClientMetadataStartedEvent = Readonly<{ eventName: 'FuseboxSetClientMetadataStarted', }>; @@ -492,9 +508,9 @@ export type StackTraceFrameUrlResolutionFailed = Readonly<{ export type ReactNativeChromeDevToolsEvent = EntrypointLoadingStartedEvent|EntrypointLoadingFinishedEvent|DebuggerReadyEvent|BrowserVisibilityChangeEvent| BrowserErrorEvent|RemoteDebuggingTerminatedEvent|DeveloperResourceLoadingStartedEvent| - DeveloperResourceLoadingFinishedEvent|FuseboxSetClientMetadataStartedEvent|FuseboxSetClientMetadataFinishedEvent| - MemoryPanelActionStartedEvent|MemoryPanelActionFinishedEvent|PanelShownEvent|PanelClosedEvent| - StackTraceSymbolicationSucceeded|StackTraceSymbolicationFailed|StackTraceFrameUrlResolutionSucceeded| - StackTraceFrameUrlResolutionFailed; + DeveloperResourceLoadingFinishedEvent|AllInitialDeveloperResourcesLoadingFinished|FuseboxSetClientMetadataStartedEvent| + FuseboxSetClientMetadataFinishedEvent|MemoryPanelActionStartedEvent|MemoryPanelActionFinishedEvent| + PanelShownEvent|PanelClosedEvent|StackTraceSymbolicationSucceeded|StackTraceSymbolicationFailed| + StackTraceFrameUrlResolutionSucceeded|StackTraceFrameUrlResolutionFailed; export type DecoratedReactNativeChromeDevToolsEvent = CommonEventFields&ReactNativeChromeDevToolsEvent; diff --git a/front_end/core/sdk/PageResourceLoader.ts b/front_end/core/sdk/PageResourceLoader.ts index bc36fa966d1c..91ce23f27a88 100644 --- a/front_end/core/sdk/PageResourceLoader.ts +++ b/front_end/core/sdk/PageResourceLoader.ts @@ -82,6 +82,7 @@ interface LoadQueueEntry { */ export class PageResourceLoader extends Common.ObjectWrapper.ObjectWrapper { #currentlyLoading = 0; + #reportedAllInitialResourcesLoaded = false; #currentlyLoadingPerTarget = new Map(); readonly #maxConcurrentLoads: number; #pageResources = new Map(); @@ -354,6 +355,15 @@ export class PageResourceLoader extends Common.ObjectWrapper.ObjectWrapper Date: Wed, 27 Aug 2025 12:01:25 +0100 Subject: [PATCH 2/3] report on first steady ping --- front_end/core/host/RNPerfMetrics.ts | 12 +++++- .../inspector_main/InspectorMain.ts | 42 +++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/front_end/core/host/RNPerfMetrics.ts b/front_end/core/host/RNPerfMetrics.ts index 3169eee9e789..9cc16f73ea36 100644 --- a/front_end/core/host/RNPerfMetrics.ts +++ b/front_end/core/host/RNPerfMetrics.ts @@ -215,6 +215,12 @@ class RNPerfMetrics { } } + firstSteadyPing(): void { + this.sendEvent({ + eventName: 'FirstSteadyPing', + }); + } + heapSnapshotStarted(): void { this.sendEvent({ eventName: 'MemoryPanelActionStarted', @@ -505,12 +511,16 @@ export type StackTraceFrameUrlResolutionFailed = Readonly<{ }>, }>; +export type FirstSteadyPing = Readonly<{ + eventName: 'FirstSteadyPing', +}>; + export type ReactNativeChromeDevToolsEvent = EntrypointLoadingStartedEvent|EntrypointLoadingFinishedEvent|DebuggerReadyEvent|BrowserVisibilityChangeEvent| BrowserErrorEvent|RemoteDebuggingTerminatedEvent|DeveloperResourceLoadingStartedEvent| DeveloperResourceLoadingFinishedEvent|AllInitialDeveloperResourcesLoadingFinished|FuseboxSetClientMetadataStartedEvent| FuseboxSetClientMetadataFinishedEvent|MemoryPanelActionStartedEvent|MemoryPanelActionFinishedEvent| PanelShownEvent|PanelClosedEvent|StackTraceSymbolicationSucceeded|StackTraceSymbolicationFailed| - StackTraceFrameUrlResolutionSucceeded|StackTraceFrameUrlResolutionFailed; + StackTraceFrameUrlResolutionSucceeded|StackTraceFrameUrlResolutionFailed|FirstSteadyPing; export type DecoratedReactNativeChromeDevToolsEvent = CommonEventFields&ReactNativeChromeDevToolsEvent; diff --git a/front_end/entrypoints/inspector_main/InspectorMain.ts b/front_end/entrypoints/inspector_main/InspectorMain.ts index ca16bfe80930..f7f45e104705 100644 --- a/front_end/entrypoints/inspector_main/InspectorMain.ts +++ b/front_end/entrypoints/inspector_main/InspectorMain.ts @@ -15,6 +15,9 @@ import * as UI from '../../ui/legacy/legacy.js'; import nodeIconStyles from './nodeIcon.css.js'; +const COOLDOWN_BETWEEN_PINGS = 3000; +const LOW_PING_THRESHOLD = 200; + const UIStrings = { /** * @description Text that refers to the main target. The main target is the primary webpage that @@ -46,6 +49,8 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let inspectorMainImplInstance: InspectorMainImpl; export class InspectorMainImpl implements Common.Runnable.Runnable { + #consecutiveLowPing = 0; + static instance(opts: { forceNew: boolean|null, } = {forceNew: null}): InspectorMainImpl { @@ -57,6 +62,28 @@ export class InspectorMainImpl implements Common.Runnable.Runnable { return inspectorMainImplInstance; } + async #measureMainConnectionPing(debuggerModel: SDK.DebuggerModel.DebuggerModel): Promise { + if (!debuggerModel.debuggerEnabled()) { + return; + } + + const startMs = Date.now(); + await debuggerModel.syncDebuggerId(); + const ping = Date.now() - startMs; + + if (ping > LOW_PING_THRESHOLD) { + this.#consecutiveLowPing = 0; + } else { + this.#consecutiveLowPing++; + } + + if (this.#consecutiveLowPing > 1) { + Host.rnPerfMetrics.firstSteadyPing(); + } else { + setTimeout(() => void this.#measureMainConnectionPing(debuggerModel), COOLDOWN_BETWEEN_PINGS); + } + } + async run(): Promise { let firstCall = true; await SDK.Connections.initMainConnection(async () => { @@ -94,13 +121,14 @@ export class InspectorMainImpl implements Common.Runnable.Runnable { } firstCall = false; - if (waitForDebuggerInPage) { - const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); - if (debuggerModel) { - if (!debuggerModel.isReadyToPause()) { - await debuggerModel.once(SDK.DebuggerModel.Events.DebuggerIsReadyToPause); - } - debuggerModel.pause(); + const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); + if (debuggerModel) { + void this.#measureMainConnectionPing(debuggerModel); + if (waitForDebuggerInPage) { + if (!debuggerModel.isReadyToPause()) { + await debuggerModel.once(SDK.DebuggerModel.Events.DebuggerIsReadyToPause); + } + debuggerModel.pause(); } } From 67572ef8d64176c9228fbc131d6415cfd9770565 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Wed, 27 Aug 2025 12:42:25 +0100 Subject: [PATCH 3/3] report first steady ping after initial resources loaded --- front_end/core/host/RNPerfMetrics.ts | 57 ++++++++++++------- front_end/core/sdk/PageResourceLoader.ts | 26 ++++++--- .../inspector_main/InspectorMain.ts | 38 ++++++++----- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/front_end/core/host/RNPerfMetrics.ts b/front_end/core/host/RNPerfMetrics.ts index 9cc16f73ea36..0aab48b0d261 100644 --- a/front_end/core/host/RNPerfMetrics.ts +++ b/front_end/core/host/RNPerfMetrics.ts @@ -29,6 +29,7 @@ class RNPerfMetrics { #telemetryInfo: Object = {}; // map of panel location to panel name #currentPanels = new Map(); + #initialResourcesLoadedInfo: null|{count: number, time: number} = null; isEnabled(): boolean { return globalThis.enableReactNativePerfMetrics === true; @@ -186,13 +187,10 @@ class RNPerfMetrics { }); } - allInitialDeveloperResourcesLoadingFinished(count: number): void { - this.sendEvent({ - eventName: 'DeveloperResource.AllInitialLoadingFinished', - params: { - count, - }, - }); + initialResourcesLoaded(info: {count: number, time: number}): void { + // eslint-disable-next-line no-console + console.info('Initial %d resources are loaded at %sms since launch', info.count, info.time); + this.#initialResourcesLoadedInfo = info; } fuseboxSetClientMetadataStarted(): void { @@ -215,10 +213,30 @@ class RNPerfMetrics { } } - firstSteadyPing(): void { + tryReportingCdpLowRoundtrip(cdpLowRoundtripStartTime: number): boolean { + if (this.#initialResourcesLoadedInfo === null) { + return false; + } + + // if the roundtrip is fine for a long time, just take the initial resources loading time + // if it got better only after the initial resources were loaded, take the cdp low roundtrip time instead + const startupTime = Math.max(cdpLowRoundtripStartTime, this.#initialResourcesLoadedInfo.time); + + // eslint-disable-next-line no-console + console.info('The app had a low CDP roundtrip at %sms since launch', cdpLowRoundtripStartTime); + // eslint-disable-next-line no-console + console.info('Startup time is %sms', startupTime); + this.sendEvent({ - eventName: 'FirstSteadyPing', + eventName: 'StartUpFinished', + params: { + bundleCount: this.#initialResourcesLoadedInfo.count, + duration: startupTime, + initialResourcesLoadedTime: this.#initialResourcesLoadedInfo.time, + cdpLowRoundtripStartTime, + } }); + return true; } heapSnapshotStarted(): void { @@ -432,13 +450,6 @@ export type DeveloperResourceLoadingFinishedEvent = Readonly<{ }>, }>; -export type AllInitialDeveloperResourcesLoadingFinished = Readonly<{ - eventName: 'DeveloperResource.AllInitialLoadingFinished', - params: Readonly<{ - count: number, - }>, -}>; - export type FuseboxSetClientMetadataStartedEvent = Readonly<{ eventName: 'FuseboxSetClientMetadataStarted', }>; @@ -511,16 +522,22 @@ export type StackTraceFrameUrlResolutionFailed = Readonly<{ }>, }>; -export type FirstSteadyPing = Readonly<{ - eventName: 'FirstSteadyPing', +export type StartUpFinished = Readonly<{ + eventName: 'StartUpFinished', + params: Readonly<{ + bundleCount: number, + duration: number, + initialResourcesLoadedTime: number, + cdpLowRoundtripStartTime: number, + }>, }>; export type ReactNativeChromeDevToolsEvent = EntrypointLoadingStartedEvent|EntrypointLoadingFinishedEvent|DebuggerReadyEvent|BrowserVisibilityChangeEvent| BrowserErrorEvent|RemoteDebuggingTerminatedEvent|DeveloperResourceLoadingStartedEvent| - DeveloperResourceLoadingFinishedEvent|AllInitialDeveloperResourcesLoadingFinished|FuseboxSetClientMetadataStartedEvent| + DeveloperResourceLoadingFinishedEvent|FuseboxSetClientMetadataStartedEvent| FuseboxSetClientMetadataFinishedEvent|MemoryPanelActionStartedEvent|MemoryPanelActionFinishedEvent| PanelShownEvent|PanelClosedEvent|StackTraceSymbolicationSucceeded|StackTraceSymbolicationFailed| - StackTraceFrameUrlResolutionSucceeded|StackTraceFrameUrlResolutionFailed|FirstSteadyPing; + StackTraceFrameUrlResolutionSucceeded|StackTraceFrameUrlResolutionFailed|StartUpFinished; export type DecoratedReactNativeChromeDevToolsEvent = CommonEventFields&ReactNativeChromeDevToolsEvent; diff --git a/front_end/core/sdk/PageResourceLoader.ts b/front_end/core/sdk/PageResourceLoader.ts index 91ce23f27a88..4ff47347b96b 100644 --- a/front_end/core/sdk/PageResourceLoader.ts +++ b/front_end/core/sdk/PageResourceLoader.ts @@ -29,6 +29,8 @@ const UIStrings = { const str_ = i18n.i18n.registerUIStrings('core/sdk/PageResourceLoader.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); +const MS_WAIT_ENSURING_ALL_RESOUCES_ARE_LOADED = 3000; + export interface ExtensionInitiator { target: null; frameId: null; @@ -82,7 +84,8 @@ interface LoadQueueEntry { */ export class PageResourceLoader extends Common.ObjectWrapper.ObjectWrapper { #currentlyLoading = 0; - #reportedAllInitialResourcesLoaded = false; + #initialResourcesLoadedTimeout: number|null = null; + #reportedInitialResourcesLoaded = false; #currentlyLoadingPerTarget = new Map(); readonly #maxConcurrentLoads: number; #pageResources = new Map(); @@ -356,13 +359,22 @@ export class PageResourceLoader extends Common.ObjectWrapper.ObjectWrapper { + const allResourcesLoaded = this.#currentlyLoading === 0; + if (allResourcesLoaded && !this.#reportedInitialResourcesLoaded) { + Host.rnPerfMetrics.initialResourcesLoaded({ + count: this.getNumberOfResources().resources, + time: Math.round(resourceLoadingTime) + }); + this.#reportedInitialResourcesLoaded = true; + } + }, MS_WAIT_ENSURING_ALL_RESOUCES_ARE_LOADED); return result; } diff --git a/front_end/entrypoints/inspector_main/InspectorMain.ts b/front_end/entrypoints/inspector_main/InspectorMain.ts index f7f45e104705..6cb5b60733f7 100644 --- a/front_end/entrypoints/inspector_main/InspectorMain.ts +++ b/front_end/entrypoints/inspector_main/InspectorMain.ts @@ -15,8 +15,8 @@ import * as UI from '../../ui/legacy/legacy.js'; import nodeIconStyles from './nodeIcon.css.js'; -const COOLDOWN_BETWEEN_PINGS = 3000; -const LOW_PING_THRESHOLD = 200; +const MS_BETWEEN_ROUNDTRIP_MEASUREMENTS = 3000; +const MS_MAX_LOW_ROUNDTRIP = 200; const UIStrings = { /** @@ -49,7 +49,7 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let inspectorMainImplInstance: InspectorMainImpl; export class InspectorMainImpl implements Common.Runnable.Runnable { - #consecutiveLowPing = 0; + #consecutiveLowRoundtrips = 0; static instance(opts: { forceNew: boolean|null, @@ -62,25 +62,37 @@ export class InspectorMainImpl implements Common.Runnable.Runnable { return inspectorMainImplInstance; } - async #measureMainConnectionPing(debuggerModel: SDK.DebuggerModel.DebuggerModel): Promise { + async #measureMainConnectionRoundtrip(debuggerModel: SDK.DebuggerModel.DebuggerModel): Promise { if (!debuggerModel.debuggerEnabled()) { return; } const startMs = Date.now(); + // Issues and waits for a response from a simple "Debugger.enable" when the debugger is enabled + // which noops and retuns a truthy response: + // https://github.com/facebook/hermes/blob/ae235193b9329867afaa2838183cbffa34aca098/API/hermes/cdp/DebuggerDomainAgent.cpp#L224-L228 + // https://github.com/facebook/hermes/blob/ae235193b9329867afaa2838183cbffa34aca098/API/hermes/cdp/DebuggerDomainAgent.cpp#L183-L185 + // It measures the round trip time for CDP message after being queued in the CDP queue in each direction. await debuggerModel.syncDebuggerId(); - const ping = Date.now() - startMs; + const roundtripTime = Date.now() - startMs; - if (ping > LOW_PING_THRESHOLD) { - this.#consecutiveLowPing = 0; + if (roundtripTime > MS_MAX_LOW_ROUNDTRIP) { + this.#consecutiveLowRoundtrips = 0; } else { - this.#consecutiveLowPing++; + this.#consecutiveLowRoundtrips++; } - if (this.#consecutiveLowPing > 1) { - Host.rnPerfMetrics.firstSteadyPing(); - } else { - setTimeout(() => void this.#measureMainConnectionPing(debuggerModel), COOLDOWN_BETWEEN_PINGS); + let reportedLowRoundrip = false; + if (this.#consecutiveLowRoundtrips >= 2) { + reportedLowRoundrip = Host.rnPerfMetrics.tryReportingCdpLowRoundtrip( + Math.round(performance.now() - ((this.#consecutiveLowRoundtrips - 1) * MS_BETWEEN_ROUNDTRIP_MEASUREMENTS)) + ); + } + + if (!reportedLowRoundrip) { + setTimeout(() => { + void this.#measureMainConnectionRoundtrip(debuggerModel); + }, MS_BETWEEN_ROUNDTRIP_MEASUREMENTS); } } @@ -123,7 +135,7 @@ export class InspectorMainImpl implements Common.Runnable.Runnable { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); if (debuggerModel) { - void this.#measureMainConnectionPing(debuggerModel); + void this.#measureMainConnectionRoundtrip(debuggerModel); if (waitForDebuggerInPage) { if (!debuggerModel.isReadyToPause()) { await debuggerModel.once(SDK.DebuggerModel.Events.DebuggerIsReadyToPause);