-
Notifications
You must be signed in to change notification settings - Fork 49.9k
[schedule] Support multiple callbacks in scheduler #12746
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
[schedule] Support multiple callbacks in scheduler #12746
Conversation
**what is the change?:** see title **why make this change?:** Once we support multiple callbacks you will need to use the id to specify which callback you mean. **test plan:** Added a test, ran all tests, lint, etc.
**what is the change?:** We keep a queue of callbacks instead of just one at a time, and call them in order first by their timeoutTime and then by the order which they were scheduled in. **why make this change?:** We plan on using this module to coordinate JS outside of React, so we will need to schedule more than one callback at a time. **test plan:** Added a boatload of shiny new tests. :) Plus ran all the old ones. NOTE: The tests do not yet cover the vital logic of callbacks timing out, and later commits will add the missing test coverage.
**what is the change?:** Tracks the current soonest timeOut time for all scheduled callbacks. **why make this change?:** We were checking every scheduled callback to see if it timed out on every tick. It's more efficient to skip that O(n) check if we know that none have timed out. **test plan:** Ran existing tests. Will write new tests to cover timeout behavior in more detail soon.
**what is the change?:** See title **why make this change?:** We don't have error handling in place yet, so should maintain the old behavior until that is in place. But want to get this far to continue making incremental changes. **test plan:** Updated and ran tests.
**what is the change?:** see title **why make this change?:** We haven't added error handling yet, so should not expose this feature. **test plan:** Ran all tests, temporarily split out the tests for multiple callbacks into separate file. Will recombine once we remove the flag.
|
ReactDOM: size: 🔺+0.4%, gzip: 🔺+0.4% Details of bundled changes.Comparing: 25dda90...5b1c090 react-dom
react-art
react-scheduler
Generated by 🚫 dangerJS |
| // unregistered by removing the id from this object. | ||
| // Then we skip calling any callback which is not registered. | ||
| // This means cancelling is an O(1) time complexity instead of O(n). | ||
| const registeredCallbackIds: {[number]: boolean} = {}; |
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.
Use a Map?
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.
Sebastian said in our last discussion to not use a Map, mainly to avoid dependencies on polyfills.
-> We don't use 'Map' because this library is intended for use outside of React, don't want to add dependency on a polyfill if we can avoid it.
in the previous PR in the stack.
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.
That was in reference to using a linked list entry instead, not if we're going to use a Map algorithm.
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.
We still could use a linked list. Is the potential size of the map a concern?
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.
The linked list would return the link to user space. Not a number.
Removing the link is:
link.next.prev = link.prev;
link.prev.next = link.next;Removing from a Map is O(n) worst case. The O(1) big O notation is misleading for maps. Accessing a few pointers is always much faster than scanning a tree no matter how clever your search is.
The linked list can add more GC work in theory but they'll mostly be young generation.
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.
Yeah I think we should use a linked list instead of ids for cancellation, but it's fine to fix that in a follow-up.
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 like the efficiency. Would be curious to know more about the efficiency of deletion in a Map or object. It sounds like the slowest way is to search in an array and delete with splice, then the next fastest is deleting from a Map, and then even faster is using a linked list.
It's possible that other users of this API are relying on the fact that the 'id' returned is typed as a number, but seems unlikely. I had initially avoided changing the type because it introduces inconsistency in our API for how the 'cancel' method works in our renderer in general.
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.
Note that in V8, objects with numeric keys are slow. So if we don't use a Map here (or something else) then it might be a good idea to prefix keys, e.g. id_${id}.
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.
I need to bookmark this.
Even today's scheduler uses minHeap instead of linked list, I think this discussion in the past is super valuable: it well represents how React core team thinks of "optimization" and "performance", not just using a {} or [] to cache value.
I will probably use this as an argument when I heard "it makes no sense to interview frontend with data structure and algorithm"..well in this context it does make sense :)
| const timeoutTime = currentCallbackConfig.timeoutTime; | ||
| if (timeoutTime !== -1 && timeoutTime <= currentTime) { | ||
| // it has timed out! | ||
| foundTimedOutCallback = true; |
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.
What is this for? Don't we have to check the entire list regardless?
I don't understand why this is a nested loop. What is the purpose of the outer loop?
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.
The nesting is because a callback in the list might time out while we are in the process of calling other callbacks.
Example:
- We have callbacks [A, B, C] and B has timed out.
- We call B. It takes a while to run, and while it is running A times out.
- We finish our loop by checking C, but we don't know that A has also now timed out!
So that's why we keep looping until we verify that nothing has timed out.
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.
Discussing in person, we agreed that it could just get pushed into the next tick if something times out while one cb is running.
packages/shared/ReactFeatureFlags.js
Outdated
| export const warnAboutDeprecatedLifecycles = false; | ||
|
|
||
| // Controls enabling of experimental multi-callback support in 'schedule' | ||
| export const scheduleModuleSupportsMultipleCallbacks = false; |
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 a feature flag? I don't think there's any valid use of the schedule module unless it supports multiple callbacks. Except for its use by React, but in that case it's only used for async mode, which is already behind its own feature flag. I don't think we need this one.
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.
Was being cautious, but if we can skip the feature flag that is simpler.
See comments on facebook#12743
**what is the change?:** We used to re-run any callbacks which time out whilst other callbacks are running, but now we will only check once for timed out callbacks then then run them. **why make this change?:** To simplify the code and the behavior of this module. **test plan:** Ran all existing tests.
**what is the change?:** see title **why make this change?:** Because only React is using this, and it sounds like async. rendering won't hit any different behavior due to these changes. **test plan:** Existing tests pass, and this allowed us to recombine all tests to run in both 'test' and 'test-build' modes.
|
@acdlite this is ready for re-review, I'm going to start working on top of it and will do non-critical improvements in the follow-up PR. |
acdlite
left a comment
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 think this is good enough to land so we can start trying it out in www, and address the remaining stuff in follow up PRs.
NOTE: We still need a way to test the timing out of callbacks, probably with some type of injection we we could create a 'noopSchedule' similar to 'noopRenderer'.
I'm going to go back and implement that as a 2/n PR, but wanted review of this because it's the direction I would ideally take after that.
This PR would be followed by a PR to add error handling.