-
Notifications
You must be signed in to change notification settings - Fork 75
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
Batch updates second try #315
Batch updates second try #315
Conversation
ccb13e7
to
3f5b5fb
Compare
…zymon/batch_updates_2_try
Is this ready for review? |
We are still adjusting unit tests in Expensfiy/App to this change but we also don't expect a need to modify this pr. |
lib/Onyx.js
Outdated
pendingCollectionUpdates = []; | ||
unstable_batchedUpdates(() => { | ||
for (let i = 0; i < updatesCopy.length; ++i) { | ||
keyChanged(updatesCopy[i][0], updatesCopy[i][1], updatesCopy[i][2]); |
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.
I couldn't find anything that confirms that the pendingUpdate|updatesCopy
is has a 3rd child. Presumably, this is guaranteed as that's how our Onyx updates are written?
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.
If by 3rd child you mean the 3 argument at updatesCopy[i][2]
then it will at least always be undefined
.
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.
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.
While this looks good, it would be great to test the component renders to prove the updates are now batched.
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.
Is there any way to write some unit tests to verify that the updates were batched as expected? Something like... have a mocked function that is triggered on render, and updating multiple properties that the component is listening to triggers the render callback the right number of times.
This is an example test of what I'm thinking of:
react-native-onyx/tests/unit/withOnyxTest.js
Lines 59 to 77 in ad5533e
it('should update withOnyx subscriber multiple times when merge is used', () => { | |
const TestComponentWithOnyx = withOnyx({ | |
text: { | |
key: ONYX_KEYS.COLLECTION.TEST_KEY, | |
}, | |
})(ViewWithCollections); | |
const onRender = jest.fn(); | |
render(<TestComponentWithOnyx onRender={onRender} />); | |
return waitForPromisesToResolve() | |
.then(() => { | |
Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`, {ID: 123}); | |
Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_KEY}2`, {ID: 234}); | |
return Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_KEY}3`, {ID: 345}); | |
}) | |
.then(() => { | |
expect(onRender).toHaveBeenCalledTimes(4); | |
}); | |
}); |
jest.mock('react-native-device-info', () => ({getFreeDiskStorage: () => {}})); | ||
jest.mock('react-native-quick-sqlite', () => ({ | ||
open: () => ({execute: () => {}}), | ||
})); | ||
|
||
jest.useRealTimers(); |
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.
Why is this added here? Do you mind adding a code comment to explain?
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.
Hanno added that because otherwise setTimeout is not executed unless we call jest.advanceTime(). So to avoid calling that everywhere we used real times.
lib/Onyx.js
Outdated
// [key, value, canUpdateSubscriber] | ||
let pendingUpdates = []; | ||
let scheduledPromise; | ||
let pendingCollectionUpdates = []; |
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.
Could you also please add a doc comment for this (like you did for pendingUpdates
)?
} | ||
|
||
scheduledPromise = new Promise((resolve) => { | ||
setTimeout(() => { |
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.
Please add a code comment explaining why it's necessary to have a setTimout(0)
here.
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.
I added a comment explaining when the batch will be flushed. Explaining how timers and batching works in react native is not possible in a single comment so I simplified it a bit.
@@ -20,6 +21,7 @@ const METHOD = { | |||
|
|||
// Key/value store of Onyx key and arrays of values to merge | |||
const mergeQueue = {}; | |||
const mergeQueuePromise = {}; |
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.
I followed most of the changes in this PR except for where it came to mergeQueuePromise
. Could you add some code comments to give a general idea around what it's for and how it's used?
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.
This change is mostly for adjusting tests from what I remember.
Looks like we use it in tests for making sure all the pending merges has been applied.
Co-authored-by: Marc Glasser <marc.aaron.glasser@gmail.com>
Co-authored-by: Marc Glasser <marc.aaron.glasser@gmail.com>
The App tests are failing with this pr. I will let you know when they are fixed. |
@tgolen @marcaaron Are there any places in the app when we trigger Onyx update in Onyx.connect subscriber? |
I just looked at all the usages of |
Oh, right, because they do not call
Sorry, I did not follow the reasoning. Is there some kind of critical problem that we will see if we batch those updates? |
Ah if we don't change state in onyx connect then probably it doesn't make any difference. I did it because I thought you may have some reactive code that is triggered once we merge something. |
![]() |
Ah, you're right. I'm on a branch where we've refactored that specific code to be a little different and it's not clear anymore that the |
I was able to fix most of failing tests in the app repo. There are 6 that left. |
Only batch withOnyx updates
We have two approving reviews. What's next? Test the changes? |
The next step is to adjust tests in the App repo. Expensify/App#27230 |
To get this going ahead I will merge this now given there are two approvals here |
Should we make an announcement not to update the Onyx version until that testing is done? These feel like pretty significant changes. |
Yes, great point. |
The Problem this pull-request solves
One of the main problems with react-native-onyx is that it renders every component in a separate micro-task. Because right now we don't use concurrent rendering every setState not within the same event listener will cause a separate render. Every react render has certain overhead so if it's possible to render 2 state changes together we will only pay for the overhead once. Additionally we introduce additional overhead in render side-effects. For instance in SidebarLinks component we compute which report should be visible. That method is heavy and we run it almost on every render. If we batch 2 setStates together that would save us one such computation. This pull-requests batches all setStates caused by onyx changes that occurs within the same microtask cycle. That saves a ton of time.
Examples of the problem
Details
We use
unstable_batchedUpdates
method from react (That is currently used in even listeners so it's pretty safe).This method triggers single react render for all the setState changes passed to it.
How it works
For every notifySubscribers or collection subscribers we don't queue the update on micro task queue instead we add that change to pending changes list that is flushed at the end of the current micro task cycle. We do it by setting macro task which is
setTimeout(() => {}, 0)
.Related Issues
GH_LINK
Automated Tests
Linked PRs