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] Don't schedule class fibers without relevant lifecycles for commit #9105

Merged
merged 7 commits into from
Mar 4, 2017

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Mar 3, 2017

While working on #9071, I noticed that we're scheduling Update effect for all classes, regardless of whether they have something to do in the commit phase:

screen shot 2017-03-03 at 7 46 52 pm

(The JSONArrow class doesn't have lifecycle methods.)

To fix this, I am checking whether there's work to do before marking a fiber with the Update tag. This mirrors the logic in the commit phase.

To verify, I ran the same code with the new bundle, and now only class fibers with corresponding lifecycles are scheduled in the effect list:

screen shot 2017-03-03 at 7 45 09 pm

This is a perf optimization and is unobservable from tests.

@sebmarkbage
Copy link
Collaborator

Seems like this removed a lot of overhead. That's a bit worrying since that indicates that we have a lot of overhead in the commit phase or in the perf measurement. We should probably make a comparison of running with and without perf instrumentation.

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 3, 2017

Yea I'll definitely be doing that, it's on my list.
I'm not to worried because those are microseconds.

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 3, 2017

With measurements on, the difference between before and after is 2ms vs 1ms. Is that enough to worry about?

@trueadm
Copy link
Contributor

trueadm commented Mar 3, 2017

@gaearon Totally. Especially given you're probably perf testing on a top-end MacBook Pro. When I did perf testing with Inferno, I'd always run perf benchmarks again on an old crappy laptop I had, it always gave me a better indication on actual performance the end-users might get. Maybe we should get into the habit of doing something similar?

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 3, 2017

Yeah, I just mean that I'd expect this to be ~1ms change on my example, so I don't think it's perf measurements exaggerating the issue. But I'll check if they are.

@xtuc
Copy link

xtuc commented Mar 3, 2017

Wouldn't it be interessting to test on a mobile device? What about RN?

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 3, 2017

This is only relevant for Fiber codebase, it won't affect React apps using existing React version. It's also not that faster, just shaving off some minor unnecessary work in the algorithm we haven't optimized yet.

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Mar 3, 2017

The commit phase will often be running at the very end of a frame deadline. 1ms is 6% of the entire frame. Easily enough to push it over the edge to miss the frame. So yea, I actually think that 1ms matters a lot in this particular case.

However, if that commit phase is invoking a bunch of componentDidMount/componentDidUpdate those are probably going to be pretty slow running and likely cause a frame drop anyway. Therefore the best solution is to just not use those like this PR helps with.

If these updates are on native nodes for example, then it is much more likely to be an issue since we will have a lots of those updates in normal frame updates.

@trueadm
Copy link
Contributor

trueadm commented Mar 3, 2017

@gaearon @sebmarkbage Just an idea:

What if componentDidMount/componentDidUpdate were async by default? If we do that, we're less likely to push frames over the edge?

We could add componentDidMountSync/componentDidUpdateSync to support legacy apps for those that need them.

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Mar 3, 2017

@trueadm Yea, I think we should make a revamp of the life-cycles. It's filed under #7678. I called the new one componentWasMounted for the async version since it is new behavior but I'm starting to think that maybe need a whole new naming scheme where the "default" shorter name is the async ones.

It's harder to break in place because there's no incremental path to fixing things.

if (current) {
// During an update, we might either need to detach a ref
// or to call the componentDidUpdate() hook in the commit phase.
const currentRef = current.ref;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We have a separate effect for refs (Ref)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Or wait, should ref change trigger a componentDidUpdate call? I assumed no but maybe it should.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It should not.

@trueadm
Copy link
Contributor

trueadm commented Mar 3, 2017

@sebmarkbage Thanks for the pointer on #7678. I'll comment there :)

Copy link
Collaborator

@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.

Refs shouldn't cause an Update effect.

if (current) {
// During an update, we might either need to detach a ref
// or to call the componentDidUpdate() hook in the commit phase.
const currentRef = current.ref;
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

Yea, @acdlite is right but since @gaearon added this code, I wonder what lead to that. Is there a path where the Ref tag is insufficient?

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 3, 2017

since @gaearon added this code, I wonder what lead to that. Is there a path where the Ref tag is insufficient?

I was just looking at what commitWork does for class components. We go into commitWork for anything with Update or PlacementAndUpdate effects.

@acdlite
Copy link
Collaborator

acdlite commented Mar 3, 2017

I believe the reason this didn't break any tests is because by the time this code is fired, shouldComponentUpdate did not return false, which means we will definitely re-render. So adding an Update effect is wrong but it doesn't cause a behavior change.

@acdlite
Copy link
Collaborator

acdlite commented Mar 3, 2017

@gaearon Oh yup, that's a bug. It should check for a Ref effect, too. Which means that switch statement will get a bit more complicated :D

@sebmarkbage
Copy link
Collaborator

Maybe we should just move that out to commitRef and check for Ref separately?

// Use Task priority for lifecycle updates
if (nextEffect.effectTag & (Update | Callback)) {
if (effectTag & (Update | Callback)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Outside the scope of this PR, but we should probably have a separate check for callbacks, for similar reasons.

if (!current && typeof instance.componentDidMount !== 'function') {
return;
}
if (current && typeof instance.componentDidUpdate !== 'function') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Style q: why are these separate checks?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How would you combine them? It feels a bit messy to me to have || and && with ! in a single condition.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah I can see how that looks a bit messy.

if ((!current && typeof instance.componentDidMount !== 'function') ||
    (current && typeof instance.componentDidUpdate !== 'function')) {
  return;
}

Alternatively, you could do

if (current) {
  if (typeof instance.componentDidUpdate !== 'function') {
    return;
  }
} else {
  if (typeof instance.componentDidMount !== 'function') {
    return;
  }
}

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 tried the second one earlier but I thought it's a bit less confusing when it exits if we collapse them.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perfwise is it the same? Does the engine have to check current twice?

Also we should use current === null

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed

config.resetTextContent(nextEffect.stateNode);
}

if (effectTag & Ref) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if we should move this check into the switch statement. Already bad that ContentReset isn't in there. It requires adding a bunch of combinations (e.g. PlacementAndRef, PlacementAndUpdateAndRef, UpdateAndRef, etc) but it's better for perf.

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 3, 2017

Anything more that should be done here? Should I do the explosion of flags in this PR?

@acdlite
Copy link
Collaborator

acdlite commented Mar 4, 2017

Personally I'm ok with it being in a separate PR

function markUpdateIfNecessary(workInProgress) {
const current = workInProgress.alternate;
const instance = workInProgress.stateNode;
if (current !== null) {
Copy link
Collaborator

@sebmarkbage sebmarkbage Mar 4, 2017

Choose a reason for hiding this comment

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

Everywhere you call this, you already have the instance in scope (probably a register) and know whether it is an update or a mount. I'd recommend just inlining this stuff. It will be less abstractions to read when you follow the code and easier to optimize because all these field reads and checks disappear.

@gaearon gaearon merged commit d87c4d5 into facebook:master Mar 4, 2017
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