diff --git a/front_end/core/host/RNPerfMetrics.ts b/front_end/core/host/RNPerfMetrics.ts index 1955fd039c41..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,6 +187,12 @@ class RNPerfMetrics { }); } + 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 { this.sendEvent({eventName: 'FuseboxSetClientMetadataStarted'}); } @@ -206,6 +213,32 @@ class RNPerfMetrics { } } + 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: 'StartUpFinished', + params: { + bundleCount: this.#initialResourcesLoadedInfo.count, + duration: startupTime, + initialResourcesLoadedTime: this.#initialResourcesLoadedInfo.time, + cdpLowRoundtripStartTime, + } + }); + return true; + } + heapSnapshotStarted(): void { this.sendEvent({ eventName: 'MemoryPanelActionStarted', @@ -489,12 +522,22 @@ export type StackTraceFrameUrlResolutionFailed = Readonly<{ }>, }>; +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|FuseboxSetClientMetadataStartedEvent|FuseboxSetClientMetadataFinishedEvent| - MemoryPanelActionStartedEvent|MemoryPanelActionFinishedEvent|PanelShownEvent|PanelClosedEvent| - StackTraceSymbolicationSucceeded|StackTraceSymbolicationFailed|StackTraceFrameUrlResolutionSucceeded| - StackTraceFrameUrlResolutionFailed; + DeveloperResourceLoadingFinishedEvent|FuseboxSetClientMetadataStartedEvent| + FuseboxSetClientMetadataFinishedEvent|MemoryPanelActionStartedEvent|MemoryPanelActionFinishedEvent| + PanelShownEvent|PanelClosedEvent|StackTraceSymbolicationSucceeded|StackTraceSymbolicationFailed| + 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 bc36fa966d1c..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,6 +84,8 @@ interface LoadQueueEntry { */ export class PageResourceLoader extends Common.ObjectWrapper.ObjectWrapper { #currentlyLoading = 0; + #initialResourcesLoadedTimeout: number|null = null; + #reportedInitialResourcesLoaded = false; #currentlyLoadingPerTarget = new Map(); readonly #maxConcurrentLoads: number; #pageResources = new Map(); @@ -354,6 +358,24 @@ 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 ca16bfe80930..6cb5b60733f7 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 MS_BETWEEN_ROUNDTRIP_MEASUREMENTS = 3000; +const MS_MAX_LOW_ROUNDTRIP = 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 { + #consecutiveLowRoundtrips = 0; + static instance(opts: { forceNew: boolean|null, } = {forceNew: null}): InspectorMainImpl { @@ -57,6 +62,40 @@ export class InspectorMainImpl implements Common.Runnable.Runnable { return inspectorMainImplInstance; } + 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 roundtripTime = Date.now() - startMs; + + if (roundtripTime > MS_MAX_LOW_ROUNDTRIP) { + this.#consecutiveLowRoundtrips = 0; + } else { + this.#consecutiveLowRoundtrips++; + } + + 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); + } + } + async run(): Promise { let firstCall = true; await SDK.Connections.initMainConnection(async () => { @@ -94,13 +133,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.#measureMainConnectionRoundtrip(debuggerModel); + if (waitForDebuggerInPage) { + if (!debuggerModel.isReadyToPause()) { + await debuggerModel.once(SDK.DebuggerModel.Events.DebuggerIsReadyToPause); + } + debuggerModel.pause(); } }