-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[HOLD for payment 2024-04-15] [Tracking] Metric - App Startup (Performance audit by Callstack) #35234
Comments
Triggered auto assignment to @trjExpensify ( |
Metric: App Startup Flow:
For each subsequent measurement, steps 1-3 are not followed. Which means we loop over from step 5-8 for the measurement cycle. This way we get the app startup of the usual flow that a user faces after the fresh install, uses the app for a while and closes it. And then each time he'll launch the app he follows the same flow as we are following here. Markers:There are already a bunch of markers in place in the Performance class but it's missing some other valuable markers that might come in handy when analysing the measurements. Also, the ending TTI marker wasn't placed precisely as it was being executed even before the splash was hidden. Ideally, we want to end the marker when the splash hides.
Measurements:We see that |
Thanks! |
@mountiny what am I doing with this bud? |
Nothing for now thanks! Wrong labels been used |
Analysis: Replace localeCompare with
|
Analysis: Replace
|
Analysis: Reduce Redundant Operations
Further analysing the hermes profile trace unearths that there is some operations that are being repeated as we have `filter`, `forEach` and `sort` in place for `getOrderedReportIDs`. In there we have following operations that can be improved to avoid redundancy and reduce the execution time of `getOrderedReportIDs` function:
Below is how we can do that in practice: // Previously
function getDisplayNameOrDefault(...args) {
...
const fallbackValue = shouldFallbackToHidden ? Localize.translateLocal('common.hidden') : '';
...
}
// After Optimisation
const hiddenText = Localize.translateLocal('common.hidden');
function getDisplayNameOrDefault(...args) {
...
const fallbackValue = shouldFallbackToHidden ? hiddenText : '';
...
}
In practice, following is the code that helps us achieve the same: const SMS_DOMAIN_PATTERN = 'expensify.sms';
function formatPhoneNumber(number) {
...
// do not parse the string, if it's not a phone number
if (number.indexOf(SMS_DOMAIN_PATTERN) === -1) {
return number;
}
const numberWithoutSMSDomain = Str.removeSMSDomain(number);
const parsedPhoneNumber = parsePhoneNumber(numberWithoutSMSDomain);
...
}
Below is how it will look like in practice: // Previously
function getDisplayNameOrDefault(...args) {
const displayName = passedPersonalDetails?.displayName ? passedPersonalDetails.displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '') : '';
...
}
// After Optimisation
function getDisplayNameOrDefault(...args) {
let displayName = passedPersonalDetails?.displayName ?? '';
if (displayName.startsWith('MERGED_')) {
displayName = displayName.substring(8);
}
...
} This all combined bring us about ~3 seconds of reduction in the App Startup Time. |
Analysis: Improve Onyx’s
|
Analysis: Avoid creating new Array in
|
Analysis: Reduce Report connections to Onyx
With each `onyx.connect` call we are adding additional overhead. Whenever we call it, under the hood `getCollectionDataAndSendAsObject` is called which appears to be costly specially for collection with huge keys like `report_`. In our case, there was 15k keys for this and it was being called for this `report_` for more than 10 times. To test the effect of this, I manually commented out all of the `onyx.connect` calls to `report_` and only kept 2-3 of them alive. The app startup went down by ~2.5 seconds, this is huge.
Below is how this class would look like: import ONYXKEYS from "@src/ONYXKEYS";
import { NativeEventEmitter, NativeModule } from "react-native";
import Onyx from "react-native-onyx";
const nativeModuleTemplate: NativeModule = {
addListener: () => {},
removeListeners: () => {},
};
const eventTypeWait = 'observe_report_wait_';
const eventTypeWithoutWait = 'observe_report_';
class BaseClass {
/**
* @property eventHandler - the NativeEventEmitter instance
*/
protected eventHandler: NativeEventEmitter;
protected constructor() {
this.eventHandler = new NativeEventEmitter(nativeModuleTemplate);
}
}
class ReportCollectionObserverWithWait extends BaseClass {
private static instance: ReportCollectionObserverWithWait;
protected constructor() {
super();
ReportCollectionObserverWithWait.instance = this;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
this.eventHandler.emit(eventTypeWait, value);
},
});
}
public static getInstance(): ReportCollectionObserverWithWait {
// Ensure singleton instance
return ReportCollectionObserverWithWait.instance ?? new ReportCollectionObserverWithWait();
}
addListener(listener: (event: unknown) => void, context?: unknown) {
this.eventHandler.addListener(eventTypeWait, listener, Object(context));
}
}
class ReportCollectionObserverWithOutWait extends BaseClass {
private static instance: ReportCollectionObserverWithOutWait;
protected constructor() {
super();
ReportCollectionObserverWithOutWait.instance = this;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
callback: (value, key) => {
this.eventHandler.emit(eventTypeWithoutWait, value, key);
},
});
}
public static getInstance(): ReportCollectionObserverWithOutWait {
// Ensure singleton instance
return ReportCollectionObserverWithOutWait.instance ?? new ReportCollectionObserverWithOutWait();
}
addListener(listener: (event: unknown) => void, context?: unknown) {
this.eventHandler.addListener(eventTypeWithoutWait, listener, Object(context));
}
}
class ReportCollectionObserver {
public static getInstance(waitForCollectionCallback?: boolean): ReportCollectionObserverWithWait | ReportCollectionObserverWithOutWait {
if (waitForCollectionCallback) {
return ReportCollectionObserverWithWait.getInstance();
}
return ReportCollectionObserverWithOutWait.getInstance();
}
}
export default ReportCollectionObserver; The usage of this class looks like: // For waitForCollectionCallback: true
ReportCollectionObserver.getInstance(true).addListener((value) => {
allReports = value as OnyxCollection<Report>;
});
// For waitForCollectionCallback: false|undefined
ReportCollectionObserver.getInstance().addListener((report, key) => {
if (!key || !report) {
return;
}
const reportID = CollectionUtils.extractCollectionItemID(key);
allReports[reportID] = report;
}); This brings us about ~2.5 seconds of reduction in the App Startup Time. We can further reduce the app startup time by 1-2 seconds if we follow the same approach for other collections that are using |
Analysis: Avoid setting cache value in loop in Onyx
In` Onyx.js` we have `getAllKeys` function which gets the key/value pairs from storage and set it to `OnyxCache` using a loop. Below is what we have currently:
// In Onyx.js
function getAllKeys() {
...
const promise = Storage..getAllKeys().then((keys) => {
_.each(keys, (key) => cache.addKey(key));
return keys;
});
...
} This adds around ~37 ms to the App Startup. We can improve this by setting all the keys at once: // In lib/onyx.js -> getAllKeys
const promise = Storage.getAllKeys().then((keys) => {
cache.addAllKeys(keys);
return keys;
});
// In lib/OnyxCache.js, add a new method
addAllKeys(keys) {
const tempKeys = [...keys];
this.storageKeys = new Set(tempKeys);
this.keysArray = tempKeys;
} Since we are getting all the keys from storage, we can set all the keys in cache directly, without setting them individually. This now takes around 4-5 ms for the execution of |
Analysis: Optimise
|
Analysis: Memoize SidebarLinksData
From the hermes profile trace, we see that `getOrderedReportIDs` from `Sidebarlinks` takes about ~8 seconds and it happens in 3 occurrences. Which means we have `getOrderedReportIDs` being called for roughly 3 times. We can reduce it to 2 times only by adding memoization to `Sidebarlinks` . This reduces the app startup by ~2.5 seconds, which is a huge reduction.
Details for the implementation can be seen in the commit |
Analysis: Remove
|
Final Measurements After Analysis:Before we started the Analysis, we had the TTI about After we implemented all of the Analysis items, we have around It's great that we have managed to reduce the App Startup Time significantly but there's still much to do. Out of this So please let us know what do you think of current analysis and whether you'd like to add/ change some proposed solutions by us. We will be very happy to hear your thoughts 💯 |
@hurali97 @kacper-mikolajczak @adhorodyski I love all the suggestions, its really impressive. I guess it would be great to start as soon as possible with the biggest hitters. replacing the localeCompare though I think we need to make sure that whatever we choose plays well with the Spanish locale too!
This one is as well quite good place to look into, we had recently quite nasty regression where the app was unusable when Search page was opened because of this regex/ checking for the phone number |
This one was missed in posting Analysis: Remove cache and Optimise
|
@mountiny Agree with you 👍 We will need to test the cases associated with each analysis. I will sort them in the order which brings the most improvements and then we can pick the action items and proceed. |
Triggered auto assignment to @mountiny, see https://stackoverflow.com/c/expensify/questions/7972 for more details. |
|
The solution for this issue has been 🚀 deployed to production 🚀 in version 1.4.60-13 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue: If no regressions arise, payment will be issued on 2024-04-15. 🎊 |
Skipping the payment summary for this issue since all the assignees are employees or vendors. If this is incorrect, please manually add the payment summary SO. |
@hurali97 @adhorodyski This one can be closed now as we are working on the new audit right? |
Description:
As a part of the #33070, the issue tracks and summarises phases for App Startup Metric. Here we will share the Measurements, Analysis and Implementation details as we progress through the Audit.
The text was updated successfully, but these errors were encountered: