Skip to content

Commit 89eb0f4

Browse files
committed
Merge branch 'perf/lhn-sort-cache' into perf/lhn-options-cache
2 parents 832b3b6 + 2fb9e46 commit 89eb0f4

7 files changed

Lines changed: 262 additions & 240 deletions

File tree

src/libs/ReportActionsUtils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,6 @@ function setSortedReportActionsCacheMaxSize(newSize: number): void {
672672
sortedReportActionsCacheMaxSize = newSize;
673673
}
674674

675-
// Immediately evict if current caches are above the new limit.
676675
evictOldestCacheEntries(sortedReportActionsCacheAscending);
677676
evictOldestCacheEntries(sortedReportActionsCacheDescending);
678677
}

src/libs/SidebarUtils.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,7 @@ function getReportsToDisplayInLHN(
274274
) {
275275
const isInFocusMode = priorityMode === CONST.PRIORITY_MODE.GSD;
276276
const allReportsDictValues = reports ?? {};
277-
// Adjust the size of the sorted report actions cache to roughly match
278-
// the current number of reports, while still respecting the global cap
279-
// inside ReportActionsUtils to guard against excessive memory usage.
277+
280278
setSortedReportActionsCacheMaxSize(Object.keys(allReportsDictValues).length);
281279
const reportsToDisplay: ReportsToDisplayInLHN = {};
282280

tests/perf-test/ReportActionsUtils.perf-test.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import {getLastClosedReportAction} from '@selectors/ReportAction';
22
import Onyx from 'react-native-onyx';
33
import {measureFunction} from 'reassure';
4-
import {getLastVisibleAction, getLastVisibleMessage, getMostRecentIOURequestActionID, getSortedReportActionsForDisplay} from '@libs/ReportActionsUtils';
4+
import {
5+
clearSortedReportActionsCache,
6+
getLastVisibleAction,
7+
getLastVisibleMessage,
8+
getMostRecentIOURequestActionID,
9+
getSortedReportActions,
10+
getSortedReportActionsForDisplay,
11+
} from '@libs/ReportActionsUtils';
512
import CONST from '@src/CONST';
613
import ONYXKEYS from '@src/ONYXKEYS';
714
import type {ReportActions} from '@src/types/onyx/ReportAction';
@@ -35,6 +42,7 @@ const reportActions = createCollection<ReportAction>(
3542
);
3643

3744
const reportId = '1';
45+
const ACTIONS_COUNT_CACHE = 100;
3846

3947
describe('ReportActionsUtils', () => {
4048
beforeAll(() => {
@@ -140,4 +148,96 @@ describe('ReportActionsUtils', () => {
140148
await waitForBatchedUpdates();
141149
await measureFunction(() => getLastClosedReportAction(reportActions));
142150
});
151+
152+
test('[ReportActionsUtils] getSortedReportActions cache hit performance', async () => {
153+
const reportActionsForCache = createCollection<ReportAction>(
154+
(item) => `${item.reportActionID}`,
155+
(index) => createRandomReportAction(index),
156+
ACTIONS_COUNT_CACHE,
157+
);
158+
const actionsArray = Object.values(reportActionsForCache);
159+
160+
await measureFunction(() => getSortedReportActions(actionsArray, true), {
161+
runs: 20,
162+
warmupRuns: 5,
163+
});
164+
});
165+
166+
test('[ReportActionsUtils] getSortedReportActions cache hit with different array references', async () => {
167+
const reportActionsForCache = createCollection<ReportAction>(
168+
(item) => `${item.reportActionID}`,
169+
(index) => createRandomReportAction(index),
170+
ACTIONS_COUNT_CACHE,
171+
);
172+
const actionsArray = Object.values(reportActionsForCache);
173+
const newArrayRef = [...actionsArray];
174+
175+
await measureFunction(() => getSortedReportActions(newArrayRef, true), {
176+
runs: 20,
177+
warmupRuns: 5,
178+
});
179+
});
180+
181+
/**
182+
* Measures getSortedReportActions with cache MISS every run (cold path).
183+
* Clears cache before each call – so we measure: clear + sort + cache set.
184+
* Compare with "cache hit performance" to see total impact; use "cache miss (natural)"
185+
* for pure sort+set without clear.
186+
*/
187+
test('[ReportActionsUtils] getSortedReportActions cache miss (cold) – clear + sort + set', async () => {
188+
const reportActionsForCache = createCollection<ReportAction>(
189+
(item) => `${item.reportActionID}`,
190+
(index) => createRandomReportAction(index),
191+
ACTIONS_COUNT_CACHE,
192+
);
193+
const actionsArray = Object.values(reportActionsForCache);
194+
195+
await measureFunction(() => {
196+
clearSortedReportActionsCache();
197+
return getSortedReportActions(actionsArray, true);
198+
}, {
199+
runs: 20,
200+
warmupRuns: 2,
201+
});
202+
});
203+
204+
/**
205+
* Measures only clearSortedReportActionsCache() to see its cost vs sort+set.
206+
* Enables "pure miss" = (cache miss with clear) − (clear only).
207+
*/
208+
test('[ReportActionsUtils] clearSortedReportActionsCache() only', async () => {
209+
await measureFunction(() => {
210+
clearSortedReportActionsCache();
211+
}, {
212+
runs: 20,
213+
warmupRuns: 2,
214+
});
215+
});
216+
217+
/**
218+
* Cache MISS without calling clear – each run uses a different dataset (different cache key).
219+
* Measures only: sort + cache set (no clear). Same size (100 actions) as cache hit tests.
220+
*/
221+
test('[ReportActionsUtils] getSortedReportActions cache miss (natural) – sort + set only', async () => {
222+
const NUM_SETS = 25;
223+
const actionSets: ReportAction[][] = [];
224+
for (let s = 0; s < NUM_SETS; s += 1) {
225+
const offset = s * (ACTIONS_COUNT_CACHE + 1);
226+
const coll = createCollection<ReportAction>(
227+
(item) => `${item.reportActionID}`,
228+
(index) => createRandomReportAction(offset + index),
229+
ACTIONS_COUNT_CACHE,
230+
);
231+
actionSets.push(Object.values(coll));
232+
}
233+
let runIndex = 0;
234+
await measureFunction(() => {
235+
const arr = actionSets[runIndex % NUM_SETS];
236+
runIndex += 1;
237+
return getSortedReportActions(arr, true);
238+
}, {
239+
runs: 20,
240+
warmupRuns: 2,
241+
});
242+
});
143243
});

tests/perf-test/ReportActionsUtilsCache.perf-test.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

tests/perf-test/SidebarLinks.perf-test.tsx

Lines changed: 8 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import type * as Navigation from '@react-navigation/native';
22
import {fireEvent, screen, waitFor} from '@testing-library/react-native';
3-
import type {OnyxCollection} from 'react-native-onyx';
43
import Onyx from 'react-native-onyx';
54
import {measureRenders} from 'reassure';
65
import CONST from '@src/CONST';
76
import ONYXKEYS from '@src/ONYXKEYS';
87
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';
8+
import type {ReportActions} from '@src/types/onyx';
139
import createRandomReportAction from '../utils/collections/reportActions';
14-
import {createRandomReport} from '../utils/collections/reports';
10+
import {createSidebarReportsWithActions as createReportsWithActions} from '../utils/collections/sidebarReports';
1511
import * as LHNTestUtils from '../utils/LHNTestUtils';
1612
import * as TestHelper from '../utils/TestHelper';
1713
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
@@ -75,70 +71,6 @@ const getMockedReportsMap = (length = 100) => {
7571
const mockedResponseMap = getMockedReportsMap(500);
7672

7773
const REPORTS_COUNT = 150;
78-
const ACTIONS_PER_REPORT = 50;
79-
80-
const createMockReportActions = (reportID: string, count: number): ReportActions => {
81-
const actions: ReportActions = {};
82-
for (let i = 0; i < count; i++) {
83-
actions[`${reportID}_${i}`] = createRandomReportAction(i);
84-
}
85-
return actions;
86-
};
87-
88-
const createReportsWithActions = (count: number) => {
89-
const reports: OnyxCollection<Report> = {};
90-
const reportActions: OnyxCollection<ReportActions> = {};
91-
const reportNameValuePairs: OnyxCollection<ReportNameValuePairs> = {};
92-
const policies: OnyxCollection<Policy> = {};
93-
const personalDetails: OnyxCollection<PersonalDetails> = {};
94-
const reportMetadata: OnyxCollection<ReportMetadata> = {};
95-
96-
const basePolicy = createRandomPolicy(1);
97-
policies[`${ONYXKEYS.COLLECTION.POLICY}${basePolicy.id}`] = basePolicy;
98-
99-
for (let i = 1; i <= count; i++) {
100-
const reportID = String(i);
101-
const report = createRandomReport(i, undefined);
102-
103-
const isArchived = i % 10 === 0;
104-
const reportTypeMod = i % 4;
105-
let reportType: string;
106-
if (reportTypeMod === 0) {
107-
reportType = CONST.REPORT.TYPE.IOU;
108-
} else if (reportTypeMod === 1) {
109-
reportType = CONST.REPORT.TYPE.EXPENSE;
110-
} else {
111-
reportType = CONST.REPORT.TYPE.CHAT;
112-
}
113-
114-
reports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] = {
115-
...report,
116-
type: reportType,
117-
};
118-
reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = createMockReportActions(reportID, ACTIONS_PER_REPORT);
119-
reportNameValuePairs[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`] = {
120-
private_isArchived: isArchived ? 'true' : 'false',
121-
};
122-
123-
const lastActorAccountID = report.lastActorAccountID ?? i;
124-
personalDetails[String(lastActorAccountID)] = createPersonalDetails(lastActorAccountID);
125-
126-
if (i % 5 === 0) {
127-
reportMetadata[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`] = {
128-
lastVisitTime: new Date().toISOString(),
129-
};
130-
}
131-
}
132-
133-
return {
134-
reports,
135-
reportActions,
136-
reportNameValuePairs,
137-
policies,
138-
personalDetails,
139-
reportMetadata,
140-
};
141-
};
14274

14375
describe('SidebarLinks', () => {
14476
beforeAll(() => {
@@ -248,7 +180,8 @@ describe('SidebarLinks', () => {
248180
const scenario = async () => {
249181
await screen.findByTestId('lhn-options-list');
250182
const firstReportID = '1';
251-
const firstReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${firstReportID}`];
183+
const actionsCollection = reportActions ?? {};
184+
const firstReportActions = actionsCollection[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${firstReportID}`];
252185
if (firstReportActions) {
253186
const newAction = createRandomReportAction(999);
254187
const updatedActions = {
@@ -283,9 +216,10 @@ describe('SidebarLinks', () => {
283216
const scenario = async () => {
284217
await screen.findByTestId('lhn-options-list');
285218
const updates: Record<string, ReportActions> = {};
219+
const actionsCollection = reportActions ?? {};
286220
for (let i = 1; i <= 10; i++) {
287221
const reportID = String(i);
288-
const existingActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
222+
const existingActions = actionsCollection[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
289223
if (existingActions) {
290224
const newAction = createRandomReportAction(1000 + i);
291225
updates[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = {
@@ -350,7 +284,8 @@ describe('SidebarLinks', () => {
350284
const scenario = async () => {
351285
await screen.findByTestId('lhn-options-list');
352286
const firstReportID = '1';
353-
const currentArchivedStatus = reportNameValuePairs[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${firstReportID}`]?.private_isArchived;
287+
const nameValuePairsCollection = reportNameValuePairs ?? {};
288+
const currentArchivedStatus = nameValuePairsCollection[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${firstReportID}`]?.private_isArchived;
354289
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${firstReportID}`, {
355290
private_isArchived: currentArchivedStatus === 'true' ? 'false' : 'true',
356291
});

0 commit comments

Comments
 (0)