diff --git a/.env.example b/.env.example index 499a17fd916c..d884526f76df 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,4 @@ NGROK_URL=https://expensify-user.ngrok.io/ USE_NGROK=false USE_WEB_PROXY=false USE_WDYR=false +CAPTURE_METRICS=false diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 192e76e0dd67..2763ad1d38ac 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -27,12 +27,39 @@ ### Why Did You Render? - Why Did You Render (WDYR) sends console notifications about potentially avoidable component re-renders. - It can also help to simply track when and why a certain component re-renders. -- To enable it, set `USE_WDYR=true` in your `.env` file. +- To enable it, set `USE_WDYR=true` in your `.env` file. - You can add or exclude tracked components by their `displayName` in `wdyr.js`. - Open the browser console to see WDYR notifications. **Suggested** [Why Did You Render docs](https://github.com/welldone-software/why-did-you-render) +### Performance Metrics (Opt-In on local release builds) + +To capture reliable performance metrics for native app launch we must test against a release build. To make this easier for everyone to do we created an opt-in tool (using [`react-native-performance`](https://github.com/oblador/react-native-performance) that will capture metrics and display them in an alert once the app becomes interactive. To set this up just set `CAPTURE_METRICS=true` in your `.env` file then create a release build on iOS or Android. The metrics this tool shows are as follows: + +- `nativeLaunch` - Total time for the native process to intialize +- `runJSBundle` - Total time to parse and execute the JS bundle +- `timeToInteractive` - Rough TTI (Time to Interactive). Includes native init time + sidebar UI partially loaded + +#### How to create a Release Build on Android + +- Create a keystore by running `keytool -genkey -v -keystore your_key_name.keystore -alias your_key_alias -keyalg RSA -keysize 2048 -validity 10000` +- Fill out all the prompts with any info and give it a password +- Drag the generated keystore to `/android/app` +- Hardcode the values to the gradle config like so: + +``` +signingConfigs { + release { + storeFile file('your_key_name.keystore') + storePassword 'Password1' + keyAlias 'your_key_alias' + keyPassword 'Password1' + } +``` +- Delete any existing apps off emulator or device +- Run `react-native run-android --variant release` + ## Reconciliation React is pretty smart and in many cases is able to tell if something needs to update. The process by which React goes about updating the "tree" or view heirarchy is called reconciliation. If React thinks something needs to update it will render it again. React also assumes that if a parent component rendered then it's child should also re-render. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 02d3ed08d31b..3f434d1f589b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -420,6 +420,8 @@ PODS: - React-Core - react-native-pdf (6.2.2): - React-Core + - react-native-performance (2.0.0): + - React-Core - react-native-plaid-link-sdk (7.0.5): - Plaid (~> 2.1.2) - React-Core @@ -638,6 +640,7 @@ DEPENDENCIES: - react-native-image-picker (from `../node_modules/react-native-image-picker`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pdf (from `../node_modules/react-native-pdf`) + - react-native-performance (from `../node_modules/react-native-performance/ios`) - react-native-plaid-link-sdk (from `../node_modules/react-native-plaid-link-sdk`) - "react-native-progress-bar-android (from `../node_modules/@react-native-community/progress-bar-android`)" - "react-native-progress-view (from `../node_modules/@react-native-community/progress-view`)" @@ -772,6 +775,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/netinfo" react-native-pdf: :path: "../node_modules/react-native-pdf" + react-native-performance: + :path: "../node_modules/react-native-performance/ios" react-native-plaid-link-sdk: :path: "../node_modules/react-native-plaid-link-sdk" react-native-progress-bar-android: @@ -872,7 +877,7 @@ SPEC CHECKSUMS: DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de EXHaptics: 337c160c148baa6f0e7166249f368965906e346b FBLazyVector: 7b423f9e248eae65987838148c36eec1dbfe0b53 - FBReactNativeSpec: c783a75db87c963c60afcd461fc38358805fe5ba + FBReactNativeSpec: 884d4cc2b011759361797a4035c47e10099393b5 Firebase: 54cdc8bc9c9b3de54f43dab86e62f5a76b47034f FirebaseABTesting: 4cb61aeeb50f60680af1c01fff781dfaf9293916 FirebaseAnalytics: 4751d6a49598a2b58da678cc07df696bcd809ab9 @@ -922,6 +927,7 @@ SPEC CHECKSUMS: react-native-image-picker: 4089335b89b625d4e34d53fb249c48a7a791b3ea react-native-netinfo: 52cf0ee8342548a485e28f4b09e56b477567244d react-native-pdf: 4b5a9e4465a6a3b399e91dc4838eb44ddf716d1f + react-native-performance: 8edfa2bbc9a2af4a02f01d342118e413a95145e0 react-native-plaid-link-sdk: 1a6593e2d3d790e8113c29178d883eb883f8c032 react-native-progress-bar-android: ce95a69f11ac580799021633071368d08aaf9ad8 react-native-progress-view: 5816e8a6be812c2b122c6225a2a3db82d9008640 diff --git a/package-lock.json b/package-lock.json index 745f32d27bc1..8731bee7675a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36169,6 +36169,11 @@ "prop-types": "^15.7.2" } }, + "react-native-performance": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-native-performance/-/react-native-performance-2.0.0.tgz", + "integrity": "sha512-jKM9Qg0SkL9D9ad377nxb1VV+OXJSyYyIrBHKmM6CABNxfrLVA5xkQMEibjmZQde7b0ndJOZoQAiObgJjjc4VQ==" + }, "react-native-permissions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.0.1.tgz", diff --git a/package.json b/package.json index c7b9aec25855..91f257b40024 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "react-native-modal": "^11.10.0", "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#d7553b95e982ab78f6bb2064f6b0549f0ace94c2", "react-native-pdf": "^6.2.2", + "react-native-performance": "^2.0.0", "react-native-permissions": "^3.0.1", "react-native-picker-select": "8.0.4", "react-native-plaid-link-sdk": "^7.0.5", diff --git a/src/CONFIG.js b/src/CONFIG.js index b43cbc0309c7..2b6a840690b6 100644 --- a/src/CONFIG.js +++ b/src/CONFIG.js @@ -60,4 +60,5 @@ export default { DEFAULT: '/favicon.png', UNREAD: '/favicon-unread.png', }, + CAPTURE_METRICS: lodashGet(Config, 'CAPTURE_METRICS', false), }; diff --git a/src/libs/Performance.js b/src/libs/Performance.js index f391c706b998..ba6805fd2e57 100644 --- a/src/libs/Performance.js +++ b/src/libs/Performance.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import lodashTransform from 'lodash/transform'; +import canCapturePerformanceMetrics from './canCapturePerformanceMetrics'; /** * Deep diff between two objects. Useful for figuring out what changed about an object from one render to the next so @@ -24,7 +25,43 @@ function diffObject(object, base) { return changes(object, base); } +/** + * Sets up an observer to capture events recorded in the native layer before the app fully initializes. + */ +function setupPerformanceObserver() { + if (!canCapturePerformanceMetrics()) { + return; + } + + const performance = require('react-native-performance').default; + const PerformanceObserver = require('react-native-performance').PerformanceObserver; + new PerformanceObserver((list) => { + if (list.getEntries().find(entry => entry.name === 'nativeLaunchEnd')) { + performance.measure('nativeLaunch', 'nativeLaunchStart', 'nativeLaunchEnd'); + + // eslint-disable-next-line no-undef + if (__DEV__) { + performance.measure('jsBundleDownload', 'downloadStart', 'downloadEnd'); + } else { + performance.measure('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd'); + } + } + }).observe({type: 'react-native-mark', buffered: true}); +} + +/** + * Outputs performance stats. We alert these so that they are easy to access in release builds. + */ +function printPerformanceMetrics() { + const performance = require('react-native-performance').default; + const entries = _.map(performance.getEntriesByType('measure'), entry => ({ + name: entry.name, duration: Math.floor(entry.duration), + })); + alert(JSON.stringify(entries, null, 4)); +} + export { - // eslint-disable-next-line import/prefer-default-export diffObject, + printPerformanceMetrics, + setupPerformanceObserver, }; diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index cad9bcce3e23..dcab886f39b2 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -7,6 +7,8 @@ import CONST from '../../CONST'; import CONFIG from '../../CONFIG'; import Firebase from '../Firebase'; import ROUTES from '../../ROUTES'; +import {printPerformanceMetrics} from '../Performance'; +import canCapturePerformanceMetrics from '../canCapturePerformanceMetrics'; let currentUserAccountID; Onyx.connect({ @@ -58,6 +60,15 @@ function setSidebarLoaded() { Onyx.set(ONYXKEYS.IS_SIDEBAR_LOADED, true); Firebase.stopTrace(CONST.TIMING.SIDEBAR_LOADED); + + if (!canCapturePerformanceMetrics()) { + return; + } + + const performance = require('react-native-performance').default; + performance.mark('sidebarLoadEnd'); + performance.measure('timeToInteractive', 'nativeLaunchStart', 'sidebarLoadEnd'); + printPerformanceMetrics(); } export { diff --git a/src/libs/canCapturePerformanceMetrics/index.js b/src/libs/canCapturePerformanceMetrics/index.js new file mode 100644 index 000000000000..1dc112f46dec --- /dev/null +++ b/src/libs/canCapturePerformanceMetrics/index.js @@ -0,0 +1,2 @@ +// We don't capture performance metrics on web as there are enough tools available +export default () => false; diff --git a/src/libs/canCapturePerformanceMetrics/index.native.js b/src/libs/canCapturePerformanceMetrics/index.native.js new file mode 100644 index 000000000000..a324c28a607c --- /dev/null +++ b/src/libs/canCapturePerformanceMetrics/index.native.js @@ -0,0 +1,10 @@ +import CONFIG from '../../CONFIG'; + +/** + * Enables capturing performance stats. + * + * @returns {Boolean} + */ +export default function canCapturePerformanceMetrics() { + return Boolean(CONFIG.CAPTURE_METRICS); +} diff --git a/src/setup/index.native.js b/src/setup/index.native.js index 3e6e8990599d..e2c92bd9108d 100644 --- a/src/setup/index.native.js +++ b/src/setup/index.native.js @@ -1,3 +1,5 @@ +import {setupPerformanceObserver} from '../libs/Performance'; + // Setup Flipper plugins when on dev export default function () { // eslint-disable-next-line no-undef @@ -7,4 +9,6 @@ export default function () { const AsyncStorage = require('@react-native-async-storage/async-storage').default; RNAsyncStorageFlipper(AsyncStorage); } + + setupPerformanceObserver(); }