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] Move memoization to begin phase #8655

Merged
merged 3 commits into from
Jan 13, 2017

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Dec 30, 2016

Because memoizedProps and memoizedState are read from the instance, the
instance's input pointers (props, state, context) should be updated even
when shouldComponentUpdate causes a bail-out.

Update

The scope of this PR changed a bit. The main change is to move memoization to the begin phase, right after reconciliation.

Currently we update the memoized inputs (props, state) during the complete phase, as we go back up the tree. That means we can't reuse work until of its children have completed.

By moving memoization to the begin phase, we can do a shallow bailout, reusing a unit of work even if there's still work to do in its children.

Memoization now happens whenever a fiber's child property is updated; typically, right after reconciling. It's also updated when shouldComponentUpdate returns false, because that indicates that the given state and props are equal to the memoized state and props.

@acdlite acdlite requested a review from bvaughn December 30, 2016 02:11
@acdlite acdlite changed the title [Fiber] Memoize preempted work [Fiber] Memoize work even if sCU causes a bail-out Dec 30, 2016
@acdlite
Copy link
Collaborator Author

acdlite commented Dec 30, 2016

If you can think of a more realistic test scenario let me know. The one I wrote is pretty contrived, but since this is an edge case that might be unavoidable.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

This would need to tag work as "in the middle of work" because otherwise if it gets aborted half way it looks as if the whole subtree can be reused.

I'm surprised we don't have a test that covers that. We should.

@acdlite
Copy link
Collaborator Author

acdlite commented Dec 30, 2016

I'll write one and see if it can be solved by setting the priority level at the same time as memoization, as you suggested in the FB group.

@acdlite
Copy link
Collaborator Author

acdlite commented Dec 30, 2016

@sebmarkbage Struggling to come up with a failing test case. As far as I can tell, even when work is aborted half way, the subtree doesn't get reused because bailoutOnFinishedWork returns the child.

@sebmarkbage
Copy link
Collaborator

Try it with one or two more levels. The work of the direct child isn't reused because one more child needs to be worked on to test if it has priority level. Also, ensure that there are siblings since currently work can only be reused after a tree is fully completed so to abort you need to be able to abort in a later sibling.

@@ -103,6 +103,8 @@ module.exports = function(

const instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
instance.props = workInProgress.memoizedProps;
instance.state = workInProgress.memoizedState;
const shouldUpdate = instance.shouldComponentUpdate(newProps, newState, newContext);
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we have the same potential problem below for the ReactPureComponent state check?

!shallowEqual(instance.state, newState)

Shouldn't this function always use memoizedProps and memoizedState? (Can we also just remove the redundant oldProps parameter?)

const instance = workInProgress.stateNode;
workInProgress.memoizedState = instance.state;
workInProgress.memoizedProps = instance.props;
workInProgress.pendingProps = null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We had trouble keeping things in sync when we split these. It might be helpful to move this inside of updateClassComponent and similarly for the others so that it is easier to tell when this needs to happen relative to other things. I'm not 100% sure this should definitely be at the very end in all cases.

@@ -103,6 +103,8 @@ module.exports = function(

const instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
instance.props = workInProgress.memoizedProps;
instance.state = workInProgress.memoizedState;
const shouldUpdate = instance.shouldComponentUpdate(newProps, newState, newContext);
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we also do this reset before componentWillReceiveProps? and componentWillUpdate?

I think this argues for putting the reset at the start of updateClassInstance- so the other 2 lifecycle methods aren't called with stale props. Also, this way, even if the user doesn't define shouldComponentUpdate the instance attributes will still be synced before render is called.

@acdlite
Copy link
Collaborator Author

acdlite commented Jan 5, 2017

Found a few more problems. The test I wrote is wrong. I think relying so much on instance.props and instance.state is masking issues that we didn't notice before. Will look at it some more tomorrow.

@acdlite acdlite force-pushed the fibermemoizepremptedwork branch 5 times, most recently from ef96a54 to 7cace1b Compare January 8, 2017 01:26
@acdlite acdlite changed the title [Fiber] Memoize work even if sCU causes a bail-out [Fiber] Move memoization to begin phase Jan 8, 2017
@acdlite
Copy link
Collaborator Author

acdlite commented Jan 8, 2017

Ok, I clean up my commits and this is now ready for review.

I couldn't find a clean way to separate updating the instance pointers from moving memoization to the begin phase, so I've left them as part of the same commit. The diff isn't that big so hopefully it's not hard to review.

@sebmarkbage, I wrote a test to confirm that we don't drop work in the subtree. I tried to break this for a while and I'm pretty confident that it works.

// So its direct children cannot be reused when we resume at
// low priority. I think this would be fixed by changing
// pendingWorkPriority and progressedPriority to be the priority of
// the children only, not including the fiber itself.
Copy link
Collaborator Author

@acdlite acdlite Jan 8, 2017

Choose a reason for hiding this comment

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

I started working on a fix for this, which I'll submit as a separate PR, since it's outside the scope of this one.

@acdlite acdlite force-pushed the fibermemoizepremptedwork branch 2 times, most recently from df65ba6 to 026f7d3 Compare January 9, 2017 00:05
Currently we update the memoized inputs (props, state) during the
complete phase, as we go back up the tree. That means we can't reuse
work until of its children have completed.

By moving memoization to the begin phase, we can do a shallow bailout,
reusing a unit of work even if there's still work to do in its children.

Memoization now happens whenever a fiber's `child` property is updated;
typically, right after reconciling. It's also updated when
`shouldComponentUpdate` returns false, because that indicates that the
given state and props are equal to the memoized state and props.
Includes a test that confirms that work that is bailed out before
completing can be reused without dropping the entire subtree.
if (workInProgress.tag === ClassComponent) {
const instance = workInProgress.stateNode;
workInProgress.memoizedProps = instance.props;
workInProgress.memoizedState = instance.state;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this needed? Wouldn't this already have been memoized if a child threw?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I put the memoization after reconciling the children because it seemed to make the most sense conceptually. Perhaps I could move it earlier before any lifecycles. The tricky part about that is that some things need to read from the previous memoized values. So you'd have to pass those around and it gets a bit hard to follow. But there's likely a better way to structure all 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.

I'd like to get to the point where we never read from the instance at all. This is one of two (I believe) places where we still do.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I still don't understand why this needs to be done in beginFailedWork specifically.

If anything, this should set the memoized values to null since this component is now in an invalid state. The children output doesn't correspond to the input.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree but that breaks an assumption we make in a few places that we always have either pending or memoized props. I figured that was outside the scope of this PR but I'll fix it here if it helps this land. Shouldn't be too difficult.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That wasn't the case before because we would always memoize in completeWork, even if it failed in beginWork.

Copy link
Collaborator Author

@acdlite acdlite Jan 12, 2017

Choose a reason for hiding this comment

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

Haha if I remove those invariants everything seems to still work. I guess I thought it'd be a bit more complicated than that. I'll go ahead and push that :)

Edit: Never mind, this actually does break some things

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 although what should get passed to shouldComponentUpdate the next time if we set memoizedProps to null? An empty object?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I guess in that case we shouldn't call shouldComponentUpdate at all since it's as if it's mounting for the first time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that something is broken here with this error handling logic but I understand now that you're only preserving existing behavior by moving it in here. I think that's fine for the first iteration.

@sebmarkbage
Copy link
Collaborator

The heavy use of abstractions like markUpdate, markUpdateIfAlreadyInProgress, resetInputPointers, memoizeProps, memoizeState is an indication that we probably need to reorganize some of this so that those steps can be consolidated but we can do that later if this works out.

} = ReactFiberClassComponent(
scheduleUpdate,
getPriorityContext,
memoizeProps,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this an abstraction worthy the pain of passing around? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Haha I almost did it so I could log when I was happening :D And at one point it was a bit more involved.

I agree this could be restructured in a better way but when I'm unsure I prefer to just find something that works and passes the tests and then clean it up later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

*mostly, not almost

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For example if I rewrote all the stuff in ReactFiberClassComponent, and then you told me the overall approach was wrong, I'd have wasted a bunch of time. And it would be harder to review. Easier to rewrite after overall approach is figured out.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

I was surprised that this just works.

I was afraid that we would be trying to reuse incomplete work however if we're trying to reuse a previously progressed tree, then cloneChildFibers will go into the non-clone path which will preserve the pendingProps and priority flag.

I don't feel like I totally understand this but I don't see any obvious major flaws.

@acdlite
Copy link
Collaborator Author

acdlite commented Jan 12, 2017

@sebmarkbage My understanding is: right now bailoutOnAlreadyFinishedWork returns the child, so it's not possible to lose the side-effects in the subtree. If we want to return null instead and do a full bailout, we need to ensure that the subtree has completed, so we don't lose side-effects. For that will need to flip a bit in the effect tag (or equivalent solution) as you pointed out.

Related: #8716

@acdlite
Copy link
Collaborator Author

acdlite commented Jan 12, 2017

Once the CI passes I'll merge this then open a follow-up PR to restructure and clean-up the weird abstractions.

@acdlite acdlite merged commit 199db63 into facebook:master Jan 13, 2017
@acdlite acdlite deleted the fibermemoizepremptedwork branch January 13, 2017 00:23
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.

5 participants