From c1023c73b010245f2e8182b75cc3bccd112d5e2e Mon Sep 17 00:00:00 2001 From: Xin Chen Date: Thu, 2 Mar 2023 20:04:42 -0800 Subject: [PATCH] Add performance.reactNativeStartupTiming API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This diff adds the `performance.reactNativeStartupTiming` API to the performance global object for RN. This property does not exist in web, and we are free to make up our own list of properties in the startup metrics to track RN app startup process. In our case, we have the following six properties to begin with (we may extend and add more to this list in the future): ``` - `(start|end)Time`: The time ‘zero’ for the startup timing and the end of app startup. RN has no knowledge of app start time, which will be provided by the platform. The `endTime` will be the time when the first JS bundle finishes executing (Note that RN supports multiple JS bundles, which can be loaded async) - `executeJavaScriptBundleEntryPoint(Start|End)`: The time for RN to execute the JS entry point (and finish all sync job) ``` Changelog: [General][Added] - Add new JS performance API to support getting RN app startup timings Reviewed By: rshest Differential Revision: D43326564 fbshipit-source-id: 7b4c7cae70ff64ba1714a1630cd5e183df6c06b0 --- .../WebPerformance/NativePerformance.cpp | 28 ++++++++ Libraries/WebPerformance/NativePerformance.h | 20 ++++++ Libraries/WebPerformance/NativePerformance.js | 8 +++ Libraries/WebPerformance/Performance.js | 11 ++++ .../ReactNativeStartupTiming.js | 65 +++++++++++++++++++ React/CxxBridge/RCTCxxBridge.mm | 3 +- .../src/main/jni/react/jni/JReactMarker.cpp | 4 +- ReactCommon/cxxreact/ReactMarker.cpp | 48 +++++++++++++- ReactCommon/cxxreact/ReactMarker.h | 38 ++++++++++- .../jsiexecutor/jsireact/JSIExecutor.cpp | 4 +- .../jsiexecutor/jsireact/JSINativeModules.cpp | 2 +- .../Performance/PerformanceApiExample.js | 61 ++++++++++++++++- 12 files changed, 279 insertions(+), 13 deletions(-) create mode 100644 Libraries/WebPerformance/ReactNativeStartupTiming.js diff --git a/Libraries/WebPerformance/NativePerformance.cpp b/Libraries/WebPerformance/NativePerformance.cpp index 64d2145023b5ba..c97020d89146a7 100644 --- a/Libraries/WebPerformance/NativePerformance.cpp +++ b/Libraries/WebPerformance/NativePerformance.cpp @@ -7,6 +7,7 @@ #include +#include #include #include "NativePerformance.h" #include "PerformanceEntryReporter.h" @@ -54,4 +55,31 @@ std::unordered_map NativePerformance::getSimpleMemoryInfo( return heapInfoToJs; } +ReactNativeStartupTiming NativePerformance::getReactNativeStartupTiming( + jsi::Runtime &rt) { + ReactNativeStartupTiming result = {0, 0, 0, 0}; + result.startTime = ReactMarker::getAppStartTime(); + + auto startupReactMarkers = + ReactMarker::StartupLogger::getInstance().getStartupReactMarkers(); + for (const auto &startupReactMarker : startupReactMarkers) { + auto time = startupReactMarker.time; + switch (startupReactMarker.markerId) { + case ReactMarker::ReactMarkerId::RUN_JS_BUNDLE_START: + result.executeJavaScriptBundleEntryPointStart = time; + break; + + case ReactMarker::ReactMarkerId::RUN_JS_BUNDLE_STOP: + result.executeJavaScriptBundleEntryPointEnd = time; + result.endTime = time; + break; + + default: + break; + } + } + + return result; +} + } // namespace facebook::react diff --git a/Libraries/WebPerformance/NativePerformance.h b/Libraries/WebPerformance/NativePerformance.h index bdb72b6d5cd50d..4b334d7339b925 100644 --- a/Libraries/WebPerformance/NativePerformance.h +++ b/Libraries/WebPerformance/NativePerformance.h @@ -18,6 +18,22 @@ class PerformanceEntryReporter; #pragma mark - Structs +using ReactNativeStartupTiming = + NativePerformanceCxxBaseReactNativeStartupTiming< + int32_t, // Start time of the RN app startup process + int32_t, // End time of the RN app startup process + int32_t, // Start time that RN app execute the JS bundle + int32_t // End time that RN app execute the JS bundle + >; + +template <> +struct Bridging + : NativePerformanceCxxBaseReactNativeStartupTimingBridging< + int32_t, + int32_t, + int32_t, + int32_t> {}; + #pragma mark - implementation class NativePerformance : public NativePerformanceCxxSpec, @@ -50,6 +66,10 @@ class NativePerformance : public NativePerformanceCxxSpec, // for heap size information, as double's 2^53 sig bytes is large enough. std::unordered_map getSimpleMemoryInfo(jsi::Runtime &rt); + // Collect and return the RN app startup timing information for performance + // tracking. + ReactNativeStartupTiming getReactNativeStartupTiming(jsi::Runtime &rt); + private: }; diff --git a/Libraries/WebPerformance/NativePerformance.js b/Libraries/WebPerformance/NativePerformance.js index 6623aac241f614..9a34b231299e4a 100644 --- a/Libraries/WebPerformance/NativePerformance.js +++ b/Libraries/WebPerformance/NativePerformance.js @@ -14,6 +14,13 @@ import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; export type NativeMemoryInfo = {[key: string]: number}; +export type ReactNativeStartupTiming = {| + startTime: number, + endTime: number, + executeJavaScriptBundleEntryPointStart: number, + executeJavaScriptBundleEntryPointEnd: number, +|}; + export interface Spec extends TurboModule { +mark: (name: string, startTime: number, duration: number) => void; +measure: ( @@ -25,6 +32,7 @@ export interface Spec extends TurboModule { endMark?: string, ) => void; +getSimpleMemoryInfo: () => NativeMemoryInfo; + +getReactNativeStartupTiming: () => ReactNativeStartupTiming; } export default (TurboModuleRegistry.get('NativePerformanceCxx'): ?Spec); diff --git a/Libraries/WebPerformance/Performance.js b/Libraries/WebPerformance/Performance.js index 2f14f9f8a1a01e..0c071583cfb398 100644 --- a/Libraries/WebPerformance/Performance.js +++ b/Libraries/WebPerformance/Performance.js @@ -25,6 +25,7 @@ import { rawToPerformanceEntry, } from './RawPerformanceEntry'; import {RawPerformanceEntryTypeValues} from './RawPerformanceEntry'; +import ReactNativeStartupTiming from './ReactNativeStartupTiming'; type DetailType = mixed; @@ -127,6 +128,16 @@ export default class Performance { return new MemoryInfo(); } + // Startup metrics is not used in web, but only in React Native. + get reactNativeStartupTiming(): ReactNativeStartupTiming { + if (NativePerformance?.getReactNativeStartupTiming) { + return new ReactNativeStartupTiming( + NativePerformance.getReactNativeStartupTiming(), + ); + } + return new ReactNativeStartupTiming(); + } + mark( markName: string, markOptions?: PerformanceMarkOptions, diff --git a/Libraries/WebPerformance/ReactNativeStartupTiming.js b/Libraries/WebPerformance/ReactNativeStartupTiming.js new file mode 100644 index 00000000000000..fbb0fa655d3e93 --- /dev/null +++ b/Libraries/WebPerformance/ReactNativeStartupTiming.js @@ -0,0 +1,65 @@ +/** + * 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. + * + * @flow strict + * @format + * @oncall react_native + */ + +// flowlint unsafe-getters-setters:off + +import type {ReactNativeStartupTiming as ReactNativeStartupTimingType} from './NativePerformance'; + +// Read-only object with RN startup timing information. +// This is returned by the performance.reactNativeStartup API. +export default class ReactNativeStartupTiming { + // All time information here are in ms. To match web spec, + // the default value for timings are zero if not present. + // See https://www.w3.org/TR/performance-timeline/#performancetiming-interface + _startTime = 0; + _endTime = 0; + _executeJavaScriptBundleEntryPointStart = 0; + _executeJavaScriptBundleEntryPointEnd = 0; + + constructor(startUpTiming: ?ReactNativeStartupTimingType) { + if (startUpTiming != null) { + this._startTime = startUpTiming.startTime; + this._endTime = startUpTiming.endTime; + this._executeJavaScriptBundleEntryPointStart = + startUpTiming.executeJavaScriptBundleEntryPointStart; + this._executeJavaScriptBundleEntryPointEnd = + startUpTiming.executeJavaScriptBundleEntryPointEnd; + } + } + + /** + * Start time of the RN app startup process. This is provided by the platform by implementing the `ReactMarker.setAppStartTime` API in the native platform code. + */ + get startTime(): number { + return this._startTime; + } + + /** + * End time of the RN app startup process. This is equal to `executeJavaScriptBundleEntryPointEnd`. + */ + get endTime(): number { + return this._endTime; + } + + /** + * Start time of JS bundle being executed. This indicates the RN JS bundle is loaded and start to be evaluated. + */ + get executeJavaScriptBundleEntryPointStart(): number { + return this._executeJavaScriptBundleEntryPointStart; + } + + /** + * End time of JS bundle being executed. This indicates all the synchronous entry point jobs are finished. + */ + get executeJavaScriptBundleEntryPointEnd(): number { + return this._executeJavaScriptBundleEntryPointEnd; + } +} diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm index 047a7c2c4eb4c1..fdce026179bde8 100644 --- a/React/CxxBridge/RCTCxxBridge.mm +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -170,7 +170,8 @@ static void mapReactMarkerToPerformanceLogger( static void registerPerformanceLoggerHooks(RCTPerformanceLogger *performanceLogger) { __weak RCTPerformanceLogger *weakPerformanceLogger = performanceLogger; - ReactMarker::logTaggedMarker = [weakPerformanceLogger](const ReactMarker::ReactMarkerId markerId, const char *tag) { + ReactMarker::logTaggedMarkerImpl = [weakPerformanceLogger]( + const ReactMarker::ReactMarkerId markerId, const char *tag) { mapReactMarkerToPerformanceLogger(markerId, weakPerformanceLogger, tag); }; } diff --git a/ReactAndroid/src/main/jni/react/jni/JReactMarker.cpp b/ReactAndroid/src/main/jni/react/jni/JReactMarker.cpp index ecb87cd439802d..d0da85929f1d69 100644 --- a/ReactAndroid/src/main/jni/react/jni/JReactMarker.cpp +++ b/ReactAndroid/src/main/jni/react/jni/JReactMarker.cpp @@ -17,8 +17,8 @@ namespace react { void JReactMarker::setLogPerfMarkerIfNeeded() { static std::once_flag flag{}; std::call_once(flag, []() { - ReactMarker::logTaggedMarker = JReactMarker::logPerfMarker; - ReactMarker::logTaggedMarkerBridgeless = + ReactMarker::logTaggedMarkerImpl = JReactMarker::logPerfMarker; + ReactMarker::logTaggedMarkerBridgelessImpl = JReactMarker::logPerfMarkerBridgeless; }); } diff --git a/ReactCommon/cxxreact/ReactMarker.cpp b/ReactCommon/cxxreact/ReactMarker.cpp index 8746a19eb83fed..a37d64020d7a58 100644 --- a/ReactCommon/cxxreact/ReactMarker.cpp +++ b/ReactCommon/cxxreact/ReactMarker.cpp @@ -6,6 +6,7 @@ */ #include "ReactMarker.h" +#include namespace facebook { namespace react { @@ -16,8 +17,9 @@ namespace ReactMarker { #pragma clang diagnostic ignored "-Wglobal-constructors" #endif -LogTaggedMarker logTaggedMarker = nullptr; -LogTaggedMarker logTaggedMarkerBridgeless = nullptr; +LogTaggedMarker logTaggedMarkerImpl = nullptr; +LogTaggedMarker logTaggedMarkerBridgelessImpl = nullptr; +GetAppStartTime getAppStartTimeImpl = nullptr; #if __clang__ #pragma clang diagnostic pop @@ -27,10 +29,52 @@ void logMarker(const ReactMarkerId markerId) { logTaggedMarker(markerId, nullptr); } +void logTaggedMarker(const ReactMarkerId markerId, const char *tag) { + StartupLogger::getInstance().logStartupEvent(markerId, tag); + logTaggedMarkerImpl(markerId, tag); +} + void logMarkerBridgeless(const ReactMarkerId markerId) { logTaggedMarkerBridgeless(markerId, nullptr); } +void logTaggedMarkerBridgeless(const ReactMarkerId markerId, const char *tag) { + StartupLogger::getInstance().logStartupEvent(markerId, tag); + logTaggedMarkerBridgelessImpl(markerId, tag); +} + +double getAppStartTime() { + if (getAppStartTimeImpl == nullptr) { + return 0; + } + + return getAppStartTimeImpl(); +} + +StartupLogger &StartupLogger::getInstance() { + static StartupLogger instance; + return instance; +} + +void StartupLogger::logStartupEvent( + const ReactMarkerId markerId, + const char *tag) { + if (startupStopped) { + return; + } + + if (markerId == ReactMarkerId::RUN_JS_BUNDLE_START || + markerId == ReactMarkerId::RUN_JS_BUNDLE_STOP) { + startupReactMarkers.push_back( + {markerId, tag, JSExecutor::performanceNow()}); + startupStopped = markerId == ReactMarkerId::RUN_JS_BUNDLE_STOP; + } +} + +std::vector StartupLogger::getStartupReactMarkers() { + return startupReactMarkers; +} + } // namespace ReactMarker } // namespace react } // namespace facebook diff --git a/ReactCommon/cxxreact/ReactMarker.h b/ReactCommon/cxxreact/ReactMarker.h index 21d8c2a705bc46..d30720715aace7 100644 --- a/ReactCommon/cxxreact/ReactMarker.h +++ b/ReactCommon/cxxreact/ReactMarker.h @@ -7,6 +7,8 @@ #pragma once +#include + #ifdef __APPLE__ #include #endif @@ -36,21 +38,53 @@ using LogTaggedMarker = std::function; // Bridge only using LogTaggedMarkerBridgeless = std::function; +using GetAppStartTime = std::function; #else typedef void ( *LogTaggedMarker)(const ReactMarkerId, const char *tag); // Bridge only typedef void (*LogTaggedMarkerBridgeless)(const ReactMarkerId, const char *tag); +typedef double (*GetAppStartTime)(); #endif #ifndef RN_EXPORT #define RN_EXPORT __attribute__((visibility("default"))) #endif -extern RN_EXPORT LogTaggedMarker logTaggedMarker; // Bridge only -extern RN_EXPORT LogTaggedMarker logTaggedMarkerBridgeless; +extern RN_EXPORT LogTaggedMarker logTaggedMarkerImpl; // Bridge only +extern RN_EXPORT LogTaggedMarker logTaggedMarkerBridgelessImpl; +extern RN_EXPORT GetAppStartTime getAppStartTimeImpl; extern RN_EXPORT void logMarker(const ReactMarkerId markerId); // Bridge only +extern RN_EXPORT void logTaggedMarker( + const ReactMarkerId markerId, + const char *tag); // Bridge only extern RN_EXPORT void logMarkerBridgeless(const ReactMarkerId markerId); +extern RN_EXPORT void logTaggedMarkerBridgeless( + const ReactMarkerId markerId, + const char *tag); +extern RN_EXPORT double getAppStartTime(); + +struct ReactMarkerEvent { + const ReactMarkerId markerId; + const char *tag; + double time; +}; + +class StartupLogger { + public: + static StartupLogger &getInstance(); + + void logStartupEvent(const ReactMarkerId markerId, const char *tag); + std::vector getStartupReactMarkers(); + + private: + StartupLogger() = default; + StartupLogger(const StartupLogger &) = delete; + StartupLogger &operator=(const StartupLogger &) = delete; + + bool startupStopped; + std::vector startupReactMarkers; +}; } // namespace ReactMarker } // namespace react diff --git a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp index 8e08080230801b..6460c7888983c4 100644 --- a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp +++ b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp @@ -137,7 +137,7 @@ void JSIExecutor::initializeRuntime() { if (runtimeInstaller_) { runtimeInstaller_(*runtime_); } - bool hasLogger(ReactMarker::logTaggedMarker); + bool hasLogger(ReactMarker::logTaggedMarkerImpl); if (hasLogger) { ReactMarker::logMarker(ReactMarker::CREATE_REACT_CONTEXT_STOP); } @@ -148,7 +148,7 @@ void JSIExecutor::loadBundle( std::string sourceURL) { SystraceSection s("JSIExecutor::loadBundle"); - bool hasLogger(ReactMarker::logTaggedMarker); + bool hasLogger(ReactMarker::logTaggedMarkerImpl); std::string scriptName = simpleBasename(sourceURL); if (hasLogger) { ReactMarker::logTaggedMarker( diff --git a/ReactCommon/jsiexecutor/jsireact/JSINativeModules.cpp b/ReactCommon/jsiexecutor/jsireact/JSINativeModules.cpp index 4ccf849cffe488..de51c93dcbfb9a 100644 --- a/ReactCommon/jsiexecutor/jsireact/JSINativeModules.cpp +++ b/ReactCommon/jsiexecutor/jsireact/JSINativeModules.cpp @@ -68,7 +68,7 @@ void JSINativeModules::reset() { std::optional JSINativeModules::createModule( Runtime &rt, const std::string &name) { - bool hasLogger(ReactMarker::logTaggedMarker); + bool hasLogger(ReactMarker::logTaggedMarkerImpl); if (hasLogger) { ReactMarker::logTaggedMarker( ReactMarker::NATIVE_MODULE_SETUP_START, name.c_str()); diff --git a/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js b/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js index 34ac57f847af4a..c75e9b9c35d471 100644 --- a/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js +++ b/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js @@ -10,12 +10,15 @@ */ 'use strict'; +import type ReactNativeStartupTiming from '../../../../../Libraries/WebPerformance/ReactNativeStartupTiming'; import * as React from 'react'; import {StyleSheet, View, Text, Button} from 'react-native'; import RNTesterPage from '../../components/RNTesterPage'; +import Performance from '../../../../../Libraries/WebPerformance/Performance'; -const {useState} = React; +const {useState, useCallback} = React; +const performance = new Performance(); function MemoryExample(): React.Node { // Memory API testing @@ -24,13 +27,13 @@ function MemoryExample(): React.Node { totalJSHeapSize?: ?number, usedJSHeapSize?: ?number, }>({}); - const onGetMemoryInfo = () => { + const onGetMemoryInfo = useCallback(() => { // performance.memory is not included in bom.js yet. // Once we release the change in flow this can be removed. // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-call] setMemoryInfo(performance.memory); - }; + }, []); return ( @@ -63,6 +66,52 @@ function MemoryExample(): React.Node { ); } +function StartupTimingExample(): React.Node { + // React Startup Timing API testing + const [startUpTiming, setStartUpTiming] = + useState(null); + const onGetStartupTiming = useCallback(() => { + // performance.reactNativeStartupTiming is not included in bom.js yet. + // Once we release the change in flow this can be removed. + // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-call] + setStartUpTiming(performance.reactNativeStartupTiming); + }, []); + return ( + + +