Skip to content

Commit 8b10c1c

Browse files
author
Tim Szot
authored
Merge pull request #4640 from Expensify/marcaaron-performanceStatsDump
[No QA] Simplify capturing performance metrics on release builds
2 parents 5acd236 + d52fd83 commit 8b10c1c

File tree

11 files changed

+108
-3
lines changed

11 files changed

+108
-3
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ NGROK_URL=https://expensify-user.ngrok.io/
99
USE_NGROK=false
1010
USE_WEB_PROXY=false
1111
USE_WDYR=false
12+
CAPTURE_METRICS=false

PERFORMANCE.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,39 @@
2727
### Why Did You Render?
2828
- Why Did You Render (WDYR) sends console notifications about potentially avoidable component re-renders.
2929
- It can also help to simply track when and why a certain component re-renders.
30-
- To enable it, set `USE_WDYR=true` in your `.env` file.
30+
- To enable it, set `USE_WDYR=true` in your `.env` file.
3131
- You can add or exclude tracked components by their `displayName` in `wdyr.js`.
3232
- Open the browser console to see WDYR notifications.
3333

3434
**Suggested** [Why Did You Render docs](https://github.com/welldone-software/why-did-you-render)
3535

36+
### Performance Metrics (Opt-In on local release builds)
37+
38+
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:
39+
40+
- `nativeLaunch` - Total time for the native process to intialize
41+
- `runJSBundle` - Total time to parse and execute the JS bundle
42+
- `timeToInteractive` - Rough TTI (Time to Interactive). Includes native init time + sidebar UI partially loaded
43+
44+
#### How to create a Release Build on Android
45+
46+
- Create a keystore by running `keytool -genkey -v -keystore your_key_name.keystore -alias your_key_alias -keyalg RSA -keysize 2048 -validity 10000`
47+
- Fill out all the prompts with any info and give it a password
48+
- Drag the generated keystore to `/android/app`
49+
- Hardcode the values to the gradle config like so:
50+
51+
```
52+
signingConfigs {
53+
release {
54+
storeFile file('your_key_name.keystore')
55+
storePassword 'Password1'
56+
keyAlias 'your_key_alias'
57+
keyPassword 'Password1'
58+
}
59+
```
60+
- Delete any existing apps off emulator or device
61+
- Run `react-native run-android --variant release`
62+
3663
## Reconciliation
3764

3865
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.

ios/Podfile.lock

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ PODS:
420420
- React-Core
421421
- react-native-pdf (6.2.2):
422422
- React-Core
423+
- react-native-performance (2.0.0):
424+
- React-Core
423425
- react-native-plaid-link-sdk (7.0.5):
424426
- Plaid (~> 2.1.2)
425427
- React-Core
@@ -638,6 +640,7 @@ DEPENDENCIES:
638640
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
639641
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
640642
- react-native-pdf (from `../node_modules/react-native-pdf`)
643+
- react-native-performance (from `../node_modules/react-native-performance/ios`)
641644
- react-native-plaid-link-sdk (from `../node_modules/react-native-plaid-link-sdk`)
642645
- "react-native-progress-bar-android (from `../node_modules/@react-native-community/progress-bar-android`)"
643646
- "react-native-progress-view (from `../node_modules/@react-native-community/progress-view`)"
@@ -772,6 +775,8 @@ EXTERNAL SOURCES:
772775
:path: "../node_modules/@react-native-community/netinfo"
773776
react-native-pdf:
774777
:path: "../node_modules/react-native-pdf"
778+
react-native-performance:
779+
:path: "../node_modules/react-native-performance/ios"
775780
react-native-plaid-link-sdk:
776781
:path: "../node_modules/react-native-plaid-link-sdk"
777782
react-native-progress-bar-android:
@@ -872,7 +877,7 @@ SPEC CHECKSUMS:
872877
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
873878
EXHaptics: 337c160c148baa6f0e7166249f368965906e346b
874879
FBLazyVector: 7b423f9e248eae65987838148c36eec1dbfe0b53
875-
FBReactNativeSpec: c783a75db87c963c60afcd461fc38358805fe5ba
880+
FBReactNativeSpec: 884d4cc2b011759361797a4035c47e10099393b5
876881
Firebase: 54cdc8bc9c9b3de54f43dab86e62f5a76b47034f
877882
FirebaseABTesting: 4cb61aeeb50f60680af1c01fff781dfaf9293916
878883
FirebaseAnalytics: 4751d6a49598a2b58da678cc07df696bcd809ab9
@@ -922,6 +927,7 @@ SPEC CHECKSUMS:
922927
react-native-image-picker: 4089335b89b625d4e34d53fb249c48a7a791b3ea
923928
react-native-netinfo: 52cf0ee8342548a485e28f4b09e56b477567244d
924929
react-native-pdf: 4b5a9e4465a6a3b399e91dc4838eb44ddf716d1f
930+
react-native-performance: 8edfa2bbc9a2af4a02f01d342118e413a95145e0
925931
react-native-plaid-link-sdk: 1a6593e2d3d790e8113c29178d883eb883f8c032
926932
react-native-progress-bar-android: ce95a69f11ac580799021633071368d08aaf9ad8
927933
react-native-progress-view: 5816e8a6be812c2b122c6225a2a3db82d9008640

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"react-native-modal": "^11.10.0",
8787
"react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#2908a47dee13d99ce756bebb75740ed2f27c2d2e",
8888
"react-native-pdf": "^6.2.2",
89+
"react-native-performance": "^2.0.0",
8990
"react-native-permissions": "^3.0.1",
9091
"react-native-picker-select": "8.0.4",
9192
"react-native-plaid-link-sdk": "^7.0.5",

src/CONFIG.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,5 @@ export default {
6060
DEFAULT: '/favicon.png',
6161
UNREAD: '/favicon-unread.png',
6262
},
63+
CAPTURE_METRICS: lodashGet(Config, 'CAPTURE_METRICS', false),
6364
};

src/libs/Performance.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import _ from 'underscore';
22
import lodashTransform from 'lodash/transform';
3+
import canCapturePerformanceMetrics from './canCapturePerformanceMetrics';
34

45
/**
56
* 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) {
2425
return changes(object, base);
2526
}
2627

28+
/**
29+
* Sets up an observer to capture events recorded in the native layer before the app fully initializes.
30+
*/
31+
function setupPerformanceObserver() {
32+
if (!canCapturePerformanceMetrics()) {
33+
return;
34+
}
35+
36+
const performance = require('react-native-performance').default;
37+
const PerformanceObserver = require('react-native-performance').PerformanceObserver;
38+
new PerformanceObserver((list) => {
39+
if (list.getEntries().find(entry => entry.name === 'nativeLaunchEnd')) {
40+
performance.measure('nativeLaunch', 'nativeLaunchStart', 'nativeLaunchEnd');
41+
42+
// eslint-disable-next-line no-undef
43+
if (__DEV__) {
44+
performance.measure('jsBundleDownload', 'downloadStart', 'downloadEnd');
45+
} else {
46+
performance.measure('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd');
47+
}
48+
}
49+
}).observe({type: 'react-native-mark', buffered: true});
50+
}
51+
52+
/**
53+
* Outputs performance stats. We alert these so that they are easy to access in release builds.
54+
*/
55+
function printPerformanceMetrics() {
56+
const performance = require('react-native-performance').default;
57+
const entries = _.map(performance.getEntriesByType('measure'), entry => ({
58+
name: entry.name, duration: Math.floor(entry.duration),
59+
}));
60+
alert(JSON.stringify(entries, null, 4));
61+
}
62+
2763
export {
28-
// eslint-disable-next-line import/prefer-default-export
2964
diffObject,
65+
printPerformanceMetrics,
66+
setupPerformanceObserver,
3067
};

src/libs/actions/App.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import CONST from '../../CONST';
77
import CONFIG from '../../CONFIG';
88
import Firebase from '../Firebase';
99
import ROUTES from '../../ROUTES';
10+
import {printPerformanceMetrics} from '../Performance';
11+
import canCapturePerformanceMetrics from '../canCapturePerformanceMetrics';
1012

1113
let currentUserAccountID;
1214
Onyx.connect({
@@ -58,6 +60,15 @@ function setSidebarLoaded() {
5860

5961
Onyx.set(ONYXKEYS.IS_SIDEBAR_LOADED, true);
6062
Firebase.stopTrace(CONST.TIMING.SIDEBAR_LOADED);
63+
64+
if (!canCapturePerformanceMetrics()) {
65+
return;
66+
}
67+
68+
const performance = require('react-native-performance').default;
69+
performance.mark('sidebarLoadEnd');
70+
performance.measure('timeToInteractive', 'nativeLaunchStart', 'sidebarLoadEnd');
71+
printPerformanceMetrics();
6172
}
6273

6374
export {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// We don't capture performance metrics on web as there are enough tools available
2+
export default () => false;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import CONFIG from '../../CONFIG';
2+
3+
/**
4+
* Enables capturing performance stats.
5+
*
6+
* @returns {Boolean}
7+
*/
8+
export default function canCapturePerformanceMetrics() {
9+
return Boolean(CONFIG.CAPTURE_METRICS);
10+
}

src/setup/index.native.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {setupPerformanceObserver} from '../libs/Performance';
2+
13
// Setup Flipper plugins when on dev
24
export default function () {
35
// eslint-disable-next-line no-undef
@@ -7,4 +9,6 @@ export default function () {
79
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
810
RNAsyncStorageFlipper(AsyncStorage);
911
}
12+
13+
setupPerformanceObserver();
1014
}

0 commit comments

Comments
 (0)