Skip to content
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

Merged
merged 14 commits into from
Nov 2, 2016
Merged

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Oct 27, 2016

Work in progress Ready for review

  • Implement synchronous work
  • Add host config to opt into synchronous scheduling
  • Batch nested updates
  • Turn on sync scheduling in ReactDOMFiber
  • Figure out which tests are failing as a result, and why
  • Expose unstable_batchedUpdates API

@acdlite acdlite changed the title [Fiber ] Use synchronous scheduling by default [Fiber] Use synchronous scheduling by default Oct 27, 2016
@acdlite
Copy link
Collaborator Author

acdlite commented Oct 28, 2016

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);
},
Copy link
Collaborator

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh duh

@acdlite
Copy link
Collaborator Author

acdlite commented Oct 28, 2016

Rebased on top of Dan's error boundary work

@acdlite acdlite force-pushed the fibersynchronouswork branch 3 times, most recently from b065e71 to 55235cb Compare October 28, 2016 22:16
@gaearon
Copy link
Collaborator

gaearon commented Oct 28, 2016

It would be great if you could add a test in ErrorBoundaries-test for setState inside batchedUpdates, like this:

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.

@gaearon
Copy link
Collaborator

gaearon commented Oct 28, 2016

Work in progress

It will be current once you commit!
(I just can't resist terrible Fiber puns.)

@acdlite acdlite force-pushed the fibersynchronouswork branch 3 times, most recently from 6b01166 to ffc832e Compare October 29, 2016 04:24
@@ -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);
Copy link
Collaborator Author

@acdlite acdlite Oct 29, 2016

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.

// 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;
Copy link
Collaborator

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?

Copy link
Collaborator

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.

Copy link
Collaborator Author

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);
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

@acdlite acdlite Oct 29, 2016

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

Copy link
Collaborator

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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

@acdlite acdlite force-pushed the fibersynchronouswork branch 2 times, most recently from 2994868 to b44da09 Compare October 31, 2016 05:35
@acdlite
Copy link
Collaborator Author

acdlite commented Oct 31, 2016

Ready for review!

@acdlite
Copy link
Collaborator Author

acdlite commented Oct 31, 2016

@gaearon I'll look at the error boundary stuff once we get #8153 merged.

@@ -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) {
Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

@sebmarkbage sebmarkbage Oct 31, 2016

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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) {
Copy link
Collaborator

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.

@acdlite
Copy link
Collaborator Author

acdlite commented Oct 31, 2016

@sebmarkbage Changed performAndHandleErrors to branch on the priority level rather than use a first-class function

@sebmarkbage
Copy link
Collaborator

Accepting pending the comments I mentioned above.

@gaearon
Copy link
Collaborator

gaearon commented Nov 2, 2016

Thanks a lot folks!

@gaearon gaearon mentioned this pull request Nov 2, 2016
4 tasks
@acdlite acdlite force-pushed the fibersynchronouswork branch 2 times, most recently from 91401b2 to d3ee9c6 Compare November 2, 2016 17:46
@acdlite
Copy link
Collaborator Author

acdlite commented Nov 2, 2016

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
Copy link
Collaborator

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 😄

@gaearon
Copy link
Collaborator

gaearon commented Nov 2, 2016

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Nov 2, 2016

So, there is a use case for the existing behavior of the callback. It allows you to do work after your parent's componentDidMount. Which is what we do for things like controlled components. It'll be interesting to see if this new behavior will break something critical.

@gaearon
Copy link
Collaborator

gaearon commented Nov 2, 2016

I fixed up lint. Test probably failed due to my mistake in master which I have fixed there.

Copy link
Collaborator Author

@acdlite acdlite left a 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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants