-
Notifications
You must be signed in to change notification settings - Fork 47k
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
[Fiber] Use synchronous scheduling by default #8127
Conversation
Just to be clear, I'm still working on this. Don't quite know what I'm doing yet :D |
@@ -210,6 +211,10 @@ var ReactNoop = { | |||
NoopRenderer.performWithPriority(AnimationPriority, fn); | |||
}, | |||
|
|||
setDefaultPriority(priorityLevel: PriorityLevel) { | |||
NoopRenderer.setDefaultPriority(priorityLevel); | |||
}, |
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.
Can we just make this low pri by default in ReactNoop and synchronous in ReactDOMFiber? So we don't have to expose this as an API?
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.
Oh duh
73ddf40
to
e4a160b
Compare
Rebased on top of Dan's error boundary work |
b065e71
to
55235cb
Compare
It would be great if you could add a test in batchedUpdates(() => {
component.setState({ ... });
throw something;
}); It needs to still perform the work despite throwing. Not related to error boundaries per se but we have all tests about errors there now so makes sense to add it there. |
It will be current once you commit! |
6b01166
to
ffc832e
Compare
@@ -47,19 +46,19 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori | |||
const updateQueue = fiber.updateQueue ? | |||
addToQueue(fiber.updateQueue, partialState) : | |||
createUpdateQueue(partialState); | |||
scheduleUpdateQueue(fiber, updateQueue, LowPriority); | |||
scheduleUpdateQueue(fiber, updateQueue); |
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 I found the problem... If scheduleUpdateQueue
is sync (and not batched) then the update is flushed before enqueueCallback
is ever called. Then enqueueCallback
will add a callback to the queue but it won't ever get called until the next time the fiber is committed.
8a5c59d
to
dfe8378
Compare
// The default priority to use for updates. | ||
let defaultPriority : PriorityLevel = LowPriority; | ||
// The priority level to use when scheduling an update. | ||
let priorityContext : (PriorityLevel | null) = null; |
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.
AFAIK having a variable switch between null and a number prevents some optimizations in V8. Can we use something like -1
for this?
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.
You could stil keep the distinction in type system if you don't make it a part of the PriorityContext
union and explicitly say PriorityContext | NoPriorityContext
. Or something like it.
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.
Good call. I think I'll just get rid of defaultPriorityContext
entirely. I added it when I thought we'd need a way to change the default on demand, but now that it's determined by the host config it's not necessary.
|
||
function performAndHandleErrors<A>(fn: (a: A) => void, a: A) { | ||
try { | ||
fn(a); |
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.
Do we try to avoid first class functions unless we're dealing with browser or user code? I'm not sure, just curious what @sebmarkbage wants.
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.
Hmm idk. Doesn't seem much different than passing performAnimationWork
to requestAnimationFrame
.
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.
And it shouldn't lead to deep stacks because nested sync updates are 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.
Ideally we should avoid first-class functions because I'd like everything to be able to take advantage of inline expansion - and be easily portable to C. It also helps newcomers to follow code that you don't have to jump back and forth. Not sure if we'll even use first-class functions for scheduling in React Native because we're not tied to rAF/rIC. Might just move that part to the DOM renderer.
It'd probably be better to go in the opposite direction and unify deferred/animation work and simply branch in performWork before the loop.
]; | ||
} else { | ||
// There's a bug in the stack reconciler where setState callbacks inside | ||
// componentWillMount aren't flushed properly |
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.
Oh nice, we are fixing this?
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 we should fix this in the stack reconciler. I'll open an issue for it once I understand it better. (Not very familiar with how stack works.) @sebmarkbage has more details.
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 extent of my knowledge is: under certain circumstances in componentWillMount
, enqueued callbacks aren't flushed, so they stick around until some other update causes them to flush.
2994868
to
b44da09
Compare
Ready for review! |
@@ -728,6 +728,11 @@ var ReactClassMixin = { | |||
* type signature and the only use case for this, is to avoid that. | |||
*/ | |||
replaceState: function(newState, callback) { | |||
if (this.updater.isFiberUpdater) { |
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.
Can you explain why Fiber needs to be special here and take a callback argument instead of a separate enqueueCallback
call?
If we're going down that route, we should just do the same for the stack reconciler instead.
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 we're changing Component internals may be better to do this before 15.4.0 which semi-separates classic stuff from reconciler.
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.
Took me a while to figure out. If the updater schedules them separately, and the update is synchronous, then the work is already done by the time the callback is scheduled, so the callback doesn't get called until the next flush. This is what I was trying to describe on Friday that is similar to the componentWillMount
bug we found.
In incremental mode, the flow goes like this:
- (in updater) Schedule update
- (in updater) Schedule callback
- Perform work
In sync mode, it goes like this
- (in updater) Schedule update
- Because the update is synchronous, the work is performed immediately
- (in updater) Schedule callback
I'm actually not sure how Stack deals with this, come to think of it, but this seemed like the most straightforward solution.
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 the way stack works is that enqueueCallback
enqueues a second synchronous flush, which doesn't really do anything except calling the callback.
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 does this strategy that Stack uses (two separate synchronous reconciliations) work for Fiber? My worry would be that this is covering up another bug.
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 bug because enqueueUpdate
only adding the callback to the queue; it didn't schedule an update on the fiber. This was unobservable previously because the callback was added to the same update queue node as the state update.
I can get it to work by adding that to enqueueUpdate
, but it felt wrong that you need an extra flush for a callback that is conceptually linked to the update.
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.
Oops, meant enqueueCallback
. On my phone so can't edit.
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 split them out in Stack because they look like they are conceptually linked but they're actually not since the callback isn't fired after a particular state update but after all the state updates. It's an API designed without consideration for batching.
At some point I considered making the enqueueCallback API public instead of the argument to setState.
We only have to do an extra flush now because they're synchronous but as soon as we get rid of that it won't cause an extra flush. They'all always be batched.
We shouldn't even need to traverse the parents in schedule update since we've already scheduled this node's priority.
['componentWillUnmount', 'blue'], | ||
].join('\n')); | ||
let expected; | ||
if (ReactDOMFeatureFlags.useFiber) { |
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.
Can we localize this branch to the two places that differ? ...(ReactDOMFeatureFlags.useFiber ? ['foo'] : [])
and ...(!ReactDOMFeatureFlags.useFiber ? ['foo'] : [])
It is hard to follow what the difference is here.
@sebmarkbage Changed |
Accepting pending the comments I mentioned above. |
Thanks a lot folks! |
91401b2
to
d3ee9c6
Compare
Merging once the build passes |
@@ -692,6 +691,7 @@ src/renderers/dom/stack/client/__tests__/ReactMount-test.js | |||
* should warn if mounting into dirty rendered markup | |||
* should not warn if mounting into non-empty node | |||
* should warn when mounting into document.body | |||
* passes the correct callback context |
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.
Just learned about this a week ago. This is the weirdest part of React API 😄
So, there is a use case for the existing behavior of the callback. It allows you to do work after your parent's |
I fixed up lint. Test probably failed due to my mistake in master which I have fixed there. |
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.
Thanks :) at lunch and that was driving me crazy
I don't think this actually changes any behavior because of the way findNextUnitOfWork works, but I think this is easier to follow.
A bit of restructuring so that setState uses whatever the current priority context is, like top-level render already does. Renamed defaultPriority to priorityContext, and added a new variable called defaultPriorityContext. Having two separate variables allows the default to be changed without interfering with the current context.
I'll turn this on in ReactDOMFiber once I figure out batchedUpdates.
Almost working...
Without this fix, in non-batched mode, the update is scheduled first and synchronously flushed before the callback is added to the queue. The callback isn't called until the next flush.
Implements batchedUpdates and exposes as unstable_batchedUpdates. All nested synchronous updates are automatically batched.
Turns out this isn't necessary. Simpler to keep it as one field.
Instead we'll branch on the priority level, like in scheduleWork.
Covers some bugs I encountered while working on this feature. Introduces a syncUpdates API to ReactNoop. Is this something we'd want to expose to the reconciler?
This solves the problem I had with enqueueSetState and enqueueCallback being separate.
9b9378f
to
40fec61
Compare
40fec61
to
95084be
Compare
Work in progressReady for reviewunstable_batchedUpdates
API