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 ( + + +