diff --git a/__mocks__/@react-native-firebase/perf.js b/__mocks__/@react-native-firebase/perf.js new file mode 100644 index 0000000000000..2d1ec238274a0 --- /dev/null +++ b/__mocks__/@react-native-firebase/perf.js @@ -0,0 +1 @@ +export default () => {}; diff --git a/src/CONST.js b/src/CONST.js index ac5bda74851cc..303f4f04611b7 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -194,6 +194,7 @@ const CONST = { HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', HOMEPAGE_REPORTS_LOADED: 'homepage_reports_loaded', SWITCH_REPORT: 'switch_report', + SIDEBAR_LOADED: 'sidebar_loaded', COLD: 'cold', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, }, diff --git a/src/Expensify.js b/src/Expensify.js index 87db377bffadb..53cef50574e63 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -37,6 +37,7 @@ Onyx.init({ [ONYXKEYS.IOU]: { loading: false, error: false, creatingIOUTransaction: false, isRetrievingCurrency: false, }, + [ONYXKEYS.IS_SIDEBAR_LOADED]: false, }, registerStorageEventListener: (onStorageEvent) => { listenToStorageEvents(onStorageEvent); @@ -72,6 +73,9 @@ const propTypes = { /** Whether the initial data needed to render the app is ready */ initialReportDataLoaded: PropTypes.bool, + /** Tells us if the sidebar has rendered */ + isSidebarLoaded: PropTypes.bool, + /** List of betas */ betas: PropTypes.arrayOf(PropTypes.string), }; @@ -84,6 +88,7 @@ const defaultProps = { }, updateAvailable: false, initialReportDataLoaded: false, + isSidebarLoaded: false, betas: [], }; @@ -144,7 +149,7 @@ class Expensify extends PureComponent { Navigation.navigate(ROUTES.WORKSPACE_NEW); } - if (this.getAuthToken() && this.props.initialReportDataLoaded) { + if (this.getAuthToken() && this.props.initialReportDataLoaded && this.props.isSidebarLoaded) { BootSplash.getVisibilityStatus() .then((value) => { if (value !== 'visible') { @@ -208,4 +213,7 @@ export default withOnyx({ initialReportDataLoaded: { key: ONYXKEYS.INITIAL_REPORT_DATA_LOADED, }, + isSidebarLoaded: { + key: ONYXKEYS.IS_SIDEBAR_LOADED, + }, })(Expensify); diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index ded87710b8e1b..71b2692907129 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -16,6 +16,9 @@ export default { // Boolean flag set whenever we are waiting for the reconnection callbacks to finish. IS_LOADING_AFTER_RECONNECT: 'isLoadingAfterReconnect', + // Boolean flag set whenever the sidebar has loaded + IS_SIDEBAR_LOADED: 'isSidebarLoaded', + NETWORK_REQUEST_QUEUE: 'networkRequestQueue', // What the active route is for our navigator. Global route that determines what views to display. diff --git a/src/components/OptionsList.js b/src/components/OptionsList.js index dbb6e968da421..cfb25b7e5e19a 100644 --- a/src/components/OptionsList.js +++ b/src/components/OptionsList.js @@ -78,6 +78,9 @@ const propTypes = { /** Whether to disable the interactivity of the list's option row(s) */ disableRowInteractivity: PropTypes.bool, + + /** Callback to execute when the SectionList lays out */ + onLayout: PropTypes.func, }; const defaultProps = { @@ -99,6 +102,7 @@ const defaultProps = { showTitleTooltip: false, optionMode: undefined, disableRowInteractivity: false, + onLayout: undefined, }; class OptionsList extends Component { @@ -109,6 +113,8 @@ class OptionsList extends Component { this.renderSectionHeader = this.renderSectionHeader.bind(this); this.extractKey = this.extractKey.bind(this); this.onScrollToIndexFailed = this.onScrollToIndexFailed.bind(this); + this.viewabilityConfig = {viewAreaCoveragePercentThreshold: 95}; + this.didLayout = false; } shouldComponentUpdate(nextProps) { @@ -228,6 +234,18 @@ class OptionsList extends Component { renderItem={this.renderItem} renderSectionHeader={this.renderSectionHeader} extraData={this.props.focusedIndex} + initialNumToRender={5} + maxToRenderPerBatch={5} + windowSize={5} + viewabilityConfig={this.viewabilityConfig} + onViewableItemsChanged={() => { + if (this.didLayout) { + return; + } + + this.didLayout = true; + this.props.onLayout(); + }} /> ); diff --git a/src/libs/Firebase/index.js b/src/libs/Firebase/index.js new file mode 100644 index 0000000000000..a8c7a34f869fa --- /dev/null +++ b/src/libs/Firebase/index.js @@ -0,0 +1,6 @@ +/** Web does not use Firebase for performance tracing */ + +export default { + startTrace() {}, + stopTrace() {}, +}; diff --git a/src/libs/Firebase/index.native.js b/src/libs/Firebase/index.native.js new file mode 100644 index 0000000000000..af627091a0ae4 --- /dev/null +++ b/src/libs/Firebase/index.native.js @@ -0,0 +1,56 @@ +/* eslint-disable no-unused-vars */ +import perf from '@react-native-firebase/perf'; +import {isDevelopment} from '../Environment/Environment'; +import Log from '../Log'; + +const traceMap = {}; + +/** + * @param {String} customEventName + */ +function startTrace(customEventName) { + const start = global.performance.now(); + if (isDevelopment()) { + return; + } + + if (traceMap[customEventName]) { + return; + } + + perf().startTrace(customEventName) + .then((trace) => { + traceMap[customEventName] = { + trace, + start, + }; + }); +} + +/** + * @param {String} customEventName + */ +function stopTrace(customEventName) { + const stop = global.performance.now(); + + if (isDevelopment()) { + return; + } + + const {trace, start} = traceMap[customEventName]; + if (!trace) { + return; + } + + trace.stop(); + + // Uncomment to inspect logs on release builds + // Log.info(`sidebar_loaded: ${stop - start} ms`, true); + + delete traceMap[customEventName]; +} + +export default { + startTrace, + stopTrace, +}; diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index 9635948348cd7..e3b339fdc7abb 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -1,6 +1,16 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; +import Firebase from '../Firebase'; +import CONST from '../../CONST'; + +let isSidebarLoaded; + +Onyx.connect({ + key: ONYXKEYS.IS_SIDEBAR_LOADED, + callback: val => isSidebarLoaded = val, + initWithStoredValues: false, +}); /** * @param {String} url @@ -17,7 +27,17 @@ function setLocale(locale) { Onyx.merge(ONYXKEYS.NVP_PREFERRED_LOCALE, locale); } +function setSidebarLoaded() { + if (isSidebarLoaded) { + return; + } + + Onyx.set(ONYXKEYS.IS_SIDEBAR_LOADED, true); + Firebase.stopTrace(CONST.TIMING.SIDEBAR_LOADED); +} + export { setCurrentURL, setLocale, + setSidebarLoaded, }; diff --git a/src/libs/actions/Timing.js b/src/libs/actions/Timing.js index 4d1eae31a36fe..a0cbae149360f 100644 --- a/src/libs/actions/Timing.js +++ b/src/libs/actions/Timing.js @@ -1,5 +1,6 @@ import getPlatform from '../getPlatform'; import {Graphite_Timer} from '../API'; +import {isDevelopment} from '../Environment/Environment'; let timestampData = {}; @@ -26,14 +27,19 @@ function end(eventName, secondaryName = '') { : `expensify.cash.${eventName}`; console.debug(`Timing:${grafanaEventName}`, eventTime); + delete timestampData[eventName]; + + // eslint-disable-next-line no-undef + if (isDevelopment()) { + // Don't create traces on dev as this will mess up the accuracy of data in release builds of the app + return; + } Graphite_Timer({ name: grafanaEventName, value: eventTime, platform: `${getPlatform()}`, }); - - delete timestampData[eventName]; } } diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index ae87bc4b69213..dfe7cea28c1b0 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,4 +1,5 @@ import React from 'react'; +import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import styles from '../../styles/styles'; import ReportView from './report/ReportView'; @@ -8,6 +9,7 @@ import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; import {updateCurrentlyViewedReportID} from '../../libs/actions/Report'; +import ONYXKEYS from '../../ONYXKEYS'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -18,6 +20,13 @@ const propTypes = { reportID: PropTypes.string, }).isRequired, }).isRequired, + + /** Tells us if the sidebar has rendered */ + isSidebarLoaded: PropTypes.bool, +}; + +const defaultProps = { + isSidebarLoaded: false, }; class ReportScreen extends React.Component { @@ -84,6 +93,10 @@ class ReportScreen extends React.Component { } render() { + if (!this.props.isSidebarLoaded) { + return null; + } + return ( diff --git a/src/pages/home/sidebar/SidebarScreen.js b/src/pages/home/sidebar/SidebarScreen.js index fcfd60c59950c..b4f9dabeb2ed8 100755 --- a/src/pages/home/sidebar/SidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen.js @@ -23,6 +23,7 @@ import { } from '../../../components/Icon/Expensicons'; import Permissions from '../../../libs/Permissions'; import ONYXKEYS from '../../../ONYXKEYS'; +import Firebase from '../../../libs/Firebase'; const propTypes = { /** Beta features list */ @@ -47,6 +48,10 @@ class SidebarScreen extends Component { }; } + componentDidMount() { + Firebase.startTrace(CONST.TIMING.SIDEBAR_LOADED); + } + /** * Method called when a Create Menu item is selected. */