Skip to content

Commit c89e04a

Browse files
committed
Merge branch 'perf/lhn-baselines' into perf/lhn-options-cache
2 parents 17ee5be + 32d7ed1 commit c89e04a

5 files changed

Lines changed: 800 additions & 8 deletions

File tree

src/libs/actions/IOU/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10302,19 +10302,15 @@ function retractReport(
1030210302
onyxMethod: Onyx.METHOD.MERGE,
1030310303
key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
1030410304
value: {
10305-
// @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
1030610305
stateNum: expenseReport.stateNum,
10307-
// @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
1030810306
statusNum: expenseReport.stateNum,
10309-
// @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
1031010307
hasReportBeenRetracted: false,
1031110308
nextStep: expenseReport.nextStep ?? null,
1031210309
pendingFields: {
10313-
// @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
1031410310
partial: null,
1031510311
nextStep: null,
1031610312
},
10317-
},
10313+
} as NullishDeep<OnyxTypes.Report>,
1031810314
},
1031910315
{
1032010316
onyxMethod: Onyx.METHOD.MERGE,
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
import {screen} from '@testing-library/react-native';
2+
import type * as Navigation from '@react-navigation/native';
3+
import Onyx from 'react-native-onyx';
4+
import type {OnyxCollection} from 'react-native-onyx';
5+
import {measureRenders} from 'reassure';
6+
import CONST from '@src/CONST';
7+
import ONYXKEYS from '@src/ONYXKEYS';
8+
import type {OnyxValues} from '@src/ONYXKEYS';
9+
import type {PersonalDetails, Report, ReportActions, ReportMetadata, ReportNameValuePairs} from '@src/types/onyx';
10+
import type Policy from '@src/types/onyx/Policy';
11+
import createPersonalDetails from '../utils/collections/personalDetails';
12+
import createRandomPolicy from '../utils/collections/policies';
13+
import createRandomReportAction from '../utils/collections/reportActions';
14+
import {createRandomReport} from '../utils/collections/reports';
15+
import * as LHNTestUtils from '../utils/LHNTestUtils';
16+
import * as TestHelper from '../utils/TestHelper';
17+
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
18+
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
19+
import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates';
20+
21+
jest.mock('@libs/Permissions');
22+
jest.mock('@src/languages/IntlStore');
23+
jest.mock('@src/libs/Localize', () => ({
24+
translate: (_locale: string | undefined, key: string | string[]) => {
25+
return Array.isArray(key) ? key.join('.') : key;
26+
},
27+
translateLocal: (key: string | string[]) => {
28+
return Array.isArray(key) ? key.join('.') : key;
29+
},
30+
formatList: (components: string[]) => components.join(', '),
31+
formatMessageElementList: (elements: unknown[]) => elements,
32+
getDevicePreferredLocale: () => 'en',
33+
}));
34+
jest.mock('@libs/Localize', () => ({
35+
translate: (_locale: string | undefined, key: string | string[]) => {
36+
return Array.isArray(key) ? key.join('.') : key;
37+
},
38+
translateLocal: (key: string | string[]) => {
39+
return Array.isArray(key) ? key.join('.') : key;
40+
},
41+
formatList: (components: string[]) => components.join(', '),
42+
formatMessageElementList: (elements: unknown[]) => elements,
43+
getDevicePreferredLocale: () => 'en',
44+
}));
45+
jest.mock('@src/libs/actions/Session', () => ({
46+
beginSignIn: jest.fn(),
47+
signIn: jest.fn(),
48+
}));
49+
jest.mock('../../src/libs/Navigation/Navigation', () => ({
50+
navigate: jest.fn(),
51+
isActiveRoute: jest.fn(),
52+
getTopmostReportId: jest.fn(),
53+
getActiveRoute: jest.fn(),
54+
getTopmostReportActionId: jest.fn(),
55+
isNavigationReady: jest.fn(() => Promise.resolve()),
56+
isDisplayedInModal: jest.fn(() => false),
57+
}));
58+
jest.mock('../../src/libs/Navigation/navigationRef', () => ({
59+
getState: () => ({
60+
routes: [{name: 'Report'}],
61+
}),
62+
getRootState: () => ({
63+
routes: [],
64+
}),
65+
addListener: () => () => {},
66+
isReady: () => true,
67+
}));
68+
jest.mock('@components/Icon/Expensicons');
69+
jest.mock('@react-navigation/native', () => {
70+
const actualNav = jest.requireActual<typeof Navigation>('@react-navigation/native');
71+
72+
return {
73+
...actualNav,
74+
useNavigationState: () => true,
75+
useRoute: jest.fn(),
76+
useFocusEffect: jest.fn(),
77+
useIsFocused: () => true,
78+
useNavigation: () => ({
79+
navigate: jest.fn(),
80+
addListener: jest.fn(),
81+
}),
82+
createNavigationContainerRef: jest.fn(),
83+
};
84+
});
85+
jest.mock('@src/hooks/useLHNEstimatedListSize/index.native.ts');
86+
87+
const REPORTS_COUNT = 150;
88+
const ACTIONS_PER_REPORT = 50;
89+
90+
const createMockReportActions = (reportID: string, count: number): ReportActions => {
91+
const actions: ReportActions = {};
92+
for (let i = 0; i < count; i++) {
93+
actions[`${reportID}_${i}`] = createRandomReportAction(i);
94+
}
95+
return actions;
96+
};
97+
98+
const createReportsWithActions = (count: number) => {
99+
const reports: OnyxCollection<Report> = {};
100+
const reportActions: OnyxCollection<ReportActions> = {};
101+
const reportNameValuePairs: OnyxCollection<ReportNameValuePairs> = {};
102+
const policies: OnyxCollection<Policy> = {};
103+
const personalDetails: OnyxCollection<PersonalDetails> = {};
104+
const reportMetadata: OnyxCollection<ReportMetadata> = {};
105+
106+
const basePolicy = createRandomPolicy(1);
107+
policies[`${ONYXKEYS.COLLECTION.POLICY}${basePolicy.id}`] = basePolicy;
108+
109+
for (let i = 1; i <= count; i++) {
110+
const reportID = String(i);
111+
const report = createRandomReport(i, undefined);
112+
113+
const isArchived = i % 10 === 0;
114+
const reportTypeMod = i % 4;
115+
let reportType: string;
116+
if (reportTypeMod === 0) {
117+
reportType = CONST.REPORT.TYPE.IOU;
118+
} else if (reportTypeMod === 1) {
119+
reportType = CONST.REPORT.TYPE.EXPENSE;
120+
} else {
121+
reportType = CONST.REPORT.TYPE.CHAT;
122+
}
123+
124+
reports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] = {
125+
...report,
126+
type: reportType,
127+
};
128+
reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = createMockReportActions(reportID, ACTIONS_PER_REPORT);
129+
reportNameValuePairs[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`] = {
130+
private_isArchived: isArchived ? 'true' : 'false',
131+
};
132+
133+
const lastActorAccountID = report.lastActorAccountID ?? i;
134+
personalDetails[String(lastActorAccountID)] = createPersonalDetails(lastActorAccountID);
135+
136+
if (i % 5 === 0) {
137+
reportMetadata[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`] = {
138+
lastVisitTime: new Date().toISOString(),
139+
};
140+
}
141+
}
142+
143+
return {
144+
reports,
145+
reportActions,
146+
reportNameValuePairs,
147+
policies,
148+
personalDetails,
149+
reportMetadata,
150+
};
151+
};
152+
153+
describe('LHN Component Performance Baseline', () => {
154+
beforeAll(() => {
155+
Onyx.init({
156+
keys: ONYXKEYS,
157+
evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
158+
});
159+
});
160+
161+
beforeEach(async () => {
162+
global.fetch = TestHelper.getGlobalFetchMock();
163+
wrapOnyxWithWaitForBatchedUpdates(Onyx);
164+
Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false});
165+
await TestHelper.signInWithTestUser(1, 'email1@test.com', undefined, undefined, 'One');
166+
await waitForBatchedUpdatesWithAct();
167+
});
168+
169+
afterEach(() => {
170+
Onyx.clear();
171+
});
172+
173+
test('[LHN Component] Initial render with 150 reports', async () => {
174+
const {reports, reportActions, reportNameValuePairs, policies, personalDetails, reportMetadata} = createReportsWithActions(REPORTS_COUNT);
175+
176+
const scenario = async () => {
177+
await screen.findByTestId('lhn-options-list');
178+
};
179+
180+
await Onyx.multiSet({
181+
[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
182+
[ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
183+
[ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD,
184+
[ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
185+
...reports,
186+
...reportActions,
187+
...reportNameValuePairs,
188+
...policies,
189+
...reportMetadata,
190+
} as Partial<OnyxValues>);
191+
192+
await waitForBatchedUpdatesWithAct();
193+
194+
await measureRenders(<LHNTestUtils.MockedSidebarLinks />, {scenario});
195+
});
196+
197+
test('[LHN Component] Re-render when single report action changes', async () => {
198+
const {reports, reportActions, reportNameValuePairs, policies, personalDetails, reportMetadata} = createReportsWithActions(REPORTS_COUNT);
199+
200+
await Onyx.multiSet({
201+
[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
202+
[ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
203+
[ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD,
204+
[ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
205+
...reports,
206+
...reportActions,
207+
...reportNameValuePairs,
208+
...policies,
209+
...reportMetadata,
210+
} as Partial<OnyxValues>);
211+
212+
await waitForBatchedUpdates();
213+
214+
const scenario = async () => {
215+
await screen.findByTestId('lhn-options-list');
216+
const firstReportID = '1';
217+
const firstReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${firstReportID}`];
218+
if (firstReportActions) {
219+
const newAction = createRandomReportAction(999);
220+
const updatedActions = {
221+
...firstReportActions,
222+
[`${firstReportID}_999`]: newAction,
223+
};
224+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${firstReportID}`, updatedActions);
225+
await waitForBatchedUpdatesWithAct();
226+
}
227+
};
228+
229+
await measureRenders(<LHNTestUtils.MockedSidebarLinks />, {scenario});
230+
});
231+
232+
test('[LHN Component] Re-render when multiple reports change', async () => {
233+
const {reports, reportActions, reportNameValuePairs, policies, personalDetails, reportMetadata} = createReportsWithActions(REPORTS_COUNT);
234+
235+
await Onyx.multiSet({
236+
[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
237+
[ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
238+
[ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD,
239+
[ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
240+
...reports,
241+
...reportActions,
242+
...reportNameValuePairs,
243+
...policies,
244+
...reportMetadata,
245+
} as Partial<OnyxValues>);
246+
247+
await waitForBatchedUpdates();
248+
249+
const scenario = async () => {
250+
await screen.findByTestId('lhn-options-list');
251+
const updates: Record<string, ReportActions> = {};
252+
for (let i = 1; i <= 10; i++) {
253+
const reportID = String(i);
254+
const existingActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
255+
if (existingActions) {
256+
const newAction = createRandomReportAction(1000 + i);
257+
updates[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = {
258+
...existingActions,
259+
[`${reportID}_new`]: newAction,
260+
};
261+
}
262+
}
263+
await Onyx.multiSet(updates as Partial<OnyxValues>);
264+
await waitForBatchedUpdatesWithAct();
265+
};
266+
267+
await measureRenders(<LHNTestUtils.MockedSidebarLinks />, {scenario});
268+
});
269+
270+
test('[LHN Component] Re-render when report metadata changes', async () => {
271+
const {reports, reportActions, reportNameValuePairs, policies, personalDetails, reportMetadata} = createReportsWithActions(REPORTS_COUNT);
272+
273+
await Onyx.multiSet({
274+
[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
275+
[ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
276+
[ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD,
277+
[ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
278+
...reports,
279+
...reportActions,
280+
...reportNameValuePairs,
281+
...policies,
282+
...reportMetadata,
283+
} as Partial<OnyxValues>);
284+
285+
await waitForBatchedUpdates();
286+
287+
const scenario = async () => {
288+
await screen.findByTestId('lhn-options-list');
289+
const firstReportID = '1';
290+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${firstReportID}`, {
291+
lastVisitTime: new Date().toISOString(),
292+
});
293+
await waitForBatchedUpdatesWithAct();
294+
};
295+
296+
await measureRenders(<LHNTestUtils.MockedSidebarLinks />, {scenario});
297+
});
298+
299+
test('[LHN Component] Re-render when report archived status changes', async () => {
300+
const {reports, reportActions, reportNameValuePairs, policies, personalDetails, reportMetadata} = createReportsWithActions(REPORTS_COUNT);
301+
302+
await Onyx.multiSet({
303+
[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
304+
[ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
305+
[ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD,
306+
[ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
307+
...reports,
308+
...reportActions,
309+
...reportNameValuePairs,
310+
...policies,
311+
...reportMetadata,
312+
} as Partial<OnyxValues>);
313+
314+
await waitForBatchedUpdates();
315+
316+
const scenario = async () => {
317+
await screen.findByTestId('lhn-options-list');
318+
const firstReportID = '1';
319+
const currentArchivedStatus = reportNameValuePairs[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${firstReportID}`]?.private_isArchived;
320+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${firstReportID}`, {
321+
private_isArchived: currentArchivedStatus === 'true' ? 'false' : 'true',
322+
});
323+
await waitForBatchedUpdatesWithAct();
324+
};
325+
326+
await measureRenders(<LHNTestUtils.MockedSidebarLinks />, {scenario});
327+
});
328+
329+
test('[LHN Component] Scaling test - 500 reports initial render', async () => {
330+
const {reports, reportActions, reportNameValuePairs, policies, personalDetails, reportMetadata} = createReportsWithActions(500);
331+
332+
const scenario = async () => {
333+
await screen.findByTestId('lhn-options-list');
334+
};
335+
336+
await Onyx.multiSet({
337+
[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
338+
[ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
339+
[ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD,
340+
[ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
341+
...reports,
342+
...reportActions,
343+
...reportNameValuePairs,
344+
...policies,
345+
...reportMetadata,
346+
} as Partial<OnyxValues>);
347+
348+
await waitForBatchedUpdatesWithAct();
349+
350+
await measureRenders(<LHNTestUtils.MockedSidebarLinks />, {scenario});
351+
});
352+
});

0 commit comments

Comments
 (0)