Repository navigation
Reuse unchanged derived values and skip no-op derived writes #76
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
Merged
sosek108
merged 5 commits into
feature/onyx-store-pr-5-recut
from
fix/onyx-117-derived-ref-reuse
Oct 9, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
342e097
Reuse unchanged derived values and skip no-op derived writes
sosek108 987b0ca
Merge branch 'main' into fix/onyx-117-derived-ref-reuse
sosek108 d4b3d76
Merge branch 'feature/onyx-store-pr-5-recut' into fix/onyx-117-derive…
sosek108 bb154de
move tests
sosek108 c70a92e
add shallow equal check to reportTransactionsAndViolations
sosek108 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
tests/perf-test/AttendeesCellPersonalDetailsWrites.perf-test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import {screen} from '@testing-library/react-native'; | ||
|
|
||
| import ComposeProviders from '@components/ComposeProviders'; | ||
| import {LocaleContextProvider} from '@components/LocaleContextProvider'; | ||
| import OnyxListItemProvider from '@components/OnyxListItemProvider'; | ||
| import AttendeesCell from '@components/Search/SearchList/ListItem/AttendeesCell'; | ||
|
|
||
| import initOnyxDerivedValues from '@userActions/OnyxDerived'; | ||
|
|
||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import type {PersonalDetailsList} from '@src/types/onyx'; | ||
| import type {Attendee} from '@src/types/onyx/IOU'; | ||
|
|
||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
| import Onyx from 'react-native-onyx'; | ||
| import {measureRenders} from 'reassure'; | ||
|
|
||
| import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; | ||
|
|
||
| const ROW_COUNT = 50; | ||
| const ATTENDEES_PER_ROW = 3; | ||
| const WRITES_PER_SCENARIO = 10; | ||
| const UNRELATED_ACCOUNT_ID = 1000; | ||
|
|
||
| const personalDetails: PersonalDetailsList = { | ||
| [UNRELATED_ACCOUNT_ID]: {accountID: UNRELATED_ACCOUNT_ID, login: 'unrelated@test.com', displayName: 'Unrelated'}, | ||
| }; | ||
| const rows: Attendee[][] = []; | ||
| for (let row = 0; row < ROW_COUNT; row++) { | ||
| const attendees: Attendee[] = []; | ||
| for (let index = 0; index < ATTENDEES_PER_ROW; index++) { | ||
| const accountID = row * ATTENDEES_PER_ROW + index + 1; | ||
| const email = `attendee${accountID}@test.com`; | ||
| personalDetails[accountID] = {accountID, login: email, displayName: `Attendee ${accountID}`}; | ||
| attendees.push({email, displayName: `Attendee ${accountID}`, avatarUrl: ''}); | ||
| } | ||
| rows.push(attendees); | ||
| } | ||
|
|
||
| function SearchRowsWithAttendees() { | ||
| return ( | ||
| <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}> | ||
| <View> | ||
| {rows.map((attendees, index) => ( | ||
| <AttendeesCell | ||
| // eslint-disable-next-line react/no-array-index-key | ||
| key={index} | ||
| attendees={attendees} | ||
| isHovered={false} | ||
| isPressed={false} | ||
| /> | ||
| ))} | ||
| </View> | ||
| </ComposeProviders> | ||
| ); | ||
| } | ||
|
|
||
| describe('AttendeesCell on personal details writes', () => { | ||
| beforeAll(() => { | ||
| Onyx.init({keys: ONYXKEYS}); | ||
| initOnyxDerivedValues(); | ||
| }); | ||
|
|
||
| beforeEach(async () => { | ||
| await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails); | ||
| await waitForBatchedUpdates(); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await Onyx.clear(); | ||
| await waitForBatchedUpdates(); | ||
| }); | ||
|
|
||
| test('[AttendeesCell] should re-render 50 Search rows when a user who attends nothing is renamed', async () => { | ||
| // Reassure runs the scenario several times against one seeded Onyx, so every run writes a name that doesn't exist yet | ||
| let run = 0; | ||
| const scenario = async () => { | ||
| // Given 50 Search rows with attendees have rendered | ||
| await screen.findAllByTestId('AttendeesCell-Row'); | ||
|
|
||
| // When a user who attends none of them is renamed, which makes the engine recompute the login map to the same content | ||
| for (let index = 0; index < WRITES_PER_SCENARIO; index++) { | ||
| await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[UNRELATED_ACCOUNT_ID]: {displayName: `Unrelated ${run}-${index}`}}); | ||
| await waitForBatchedUpdates(); | ||
| } | ||
| run++; | ||
| }; | ||
|
|
||
| // Then reassure measures how many row re-renders the unrelated writes cause | ||
| await measureRenders(<SearchRowsWithAttendees />, {scenario}); | ||
| }); | ||
| }); |
110 changes: 110 additions & 0 deletions
110
tests/perf-test/SidebarLinksPersonalDetailsWrites.perf-test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import {screen} from '@testing-library/react-native'; | ||
|
|
||
| import {setHasRadio} from '@libs/NetworkState'; | ||
|
|
||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import type {PersonalDetailsList} from '@src/types/onyx'; | ||
|
|
||
| import Onyx from 'react-native-onyx'; | ||
| import {measureRenders} from 'reassure'; | ||
|
|
||
| import * as LHNTestUtils from '../utils/LHNTestUtils'; | ||
| import * as TestHelper from '../utils/TestHelper'; | ||
| import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; | ||
| import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; | ||
|
|
||
| jest.mock('@libs/Permissions'); | ||
| jest.mock('../../src/libs/Navigation/Navigation', () => ({ | ||
| navigate: jest.fn(), | ||
| isActiveRoute: jest.fn(), | ||
| getTopmostReportId: jest.fn(), | ||
| getActiveRoute: jest.fn(), | ||
| getTopmostReportActionId: jest.fn(), | ||
| isNavigationReady: jest.fn(() => Promise.resolve()), | ||
| isDisplayedInModal: jest.fn(() => false), | ||
| getActiveRouteWithoutParams: jest.fn(() => ''), | ||
| })); | ||
| jest.mock('../../src/libs/Navigation/navigationRef', () => ({ | ||
| getState: () => ({ | ||
| routes: [{name: 'Report'}], | ||
| }), | ||
| getRootState: () => ({ | ||
| routes: [], | ||
| }), | ||
| addListener: () => () => {}, | ||
| isReady: () => true, | ||
| })); | ||
|
|
||
| jest.mock('@react-navigation/native'); | ||
|
|
||
| const REPORT_COUNT = 500; | ||
| const WRITES_PER_SCENARIO = 10; | ||
| const GUIDE_ACCOUNT_ID = 50; | ||
| const UNRELATED_ACCOUNT_ID = 60; | ||
|
|
||
| const reports = Object.fromEntries( | ||
| Array.from({length: REPORT_COUNT}, (value, index) => { | ||
| const report = {...LHNTestUtils.getFakeReport([1, 2], 1, true), lastMessageText: 'hey'}; | ||
| return [`${ONYXKEYS.COLLECTION.REPORT}${index + 1}`, report]; | ||
| }), | ||
| ); | ||
|
|
||
| const personalDetails: PersonalDetailsList = { | ||
| ...LHNTestUtils.fakePersonalDetails, | ||
| [GUIDE_ACCOUNT_ID]: {accountID: GUIDE_ACCOUNT_ID, login: `guide@${CONST.EMAIL.GUIDES_DOMAIN}`, displayName: 'Guide'}, | ||
| [UNRELATED_ACCOUNT_ID]: {accountID: UNRELATED_ACCOUNT_ID, login: 'unrelated@test.com', displayName: 'Unrelated'}, | ||
| }; | ||
|
|
||
| describe('SidebarLinks on personal details writes', () => { | ||
| beforeAll(() => { | ||
| Onyx.init({ | ||
| keys: ONYXKEYS, | ||
| evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], | ||
| }); | ||
| // Required lazily so LHNTestUtils registers its @react-navigation/native mock before the engine's imports load it | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access | ||
| require('@userActions/OnyxDerived').default(); | ||
| }); | ||
|
|
||
| beforeEach(async () => { | ||
| global.fetch = TestHelper.getGlobalFetchMock(); | ||
| wrapOnyxWithWaitForBatchedUpdates(Onyx); | ||
| setHasRadio(true); | ||
| await TestHelper.signInWithTestUser(1, 'email1@test.com', undefined, undefined, 'One'); | ||
| await waitForBatchedUpdates(); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await Onyx.clear(); | ||
| await waitForBatchedUpdates(); | ||
| }); | ||
|
|
||
| test('[SidebarLinks] should re-render when a user outside the LHN is renamed', async () => { | ||
| // Given 500 reports in the LHN, a guide, and a user who is in none of the reports | ||
| await Onyx.multiSet({ | ||
| [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails, | ||
| [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS], | ||
| [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, | ||
| [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, | ||
| ...reports, | ||
| }); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| // Reassure runs the scenario several times against one seeded Onyx, so every run writes a name that doesn't exist yet | ||
| let run = 0; | ||
| const scenario = async () => { | ||
| await screen.findByTestId('lhn-options-list'); | ||
|
|
||
| // When that user is renamed repeatedly, which makes the derived engine recompute the guide accountIDs to the same content | ||
| for (let index = 0; index < WRITES_PER_SCENARIO; index++) { | ||
| await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[UNRELATED_ACCOUNT_ID]: {displayName: `Unrelated ${run}-${index}`}}); | ||
| await waitForBatchedUpdates(); | ||
| } | ||
| run++; | ||
| }; | ||
|
|
||
| // Then reassure measures what the unrelated writes cost the LHN | ||
| await measureRenders(<LHNTestUtils.MockedSidebarLinks />, {scenario}); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case we hit this we will still reach the
finallyblock, right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, finally is ran after
returnis called.Confirmed with this simple script