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

Remove Animation priority #9968

Closed
wants to merge 4 commits into from

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Jun 14, 2017

Follow up to this comment: #9952 (comment)

If we're already performing work, we shouldn't schedule an additional callback, e.g. if setState is called inside componentWillReceiveProps.

Update

The scope of this PR increased from its original description. Now it removes Animation priority as a feature. There's no advantage to scheduling updates with animation priority versus scheduling sync work inside requestAnimationCallback. So we can remove all the animation-specific code. Now there's only one type of callback.

@acdlite
Copy link
Collaborator Author

acdlite commented Jun 14, 2017

The first commit isn't super-related. It adds a new ReactNoop API for testing, which I used to test this bug fix. The commit message has details.

@gaearon
Copy link
Collaborator

gaearon commented Jun 14, 2017

Breaks the build for noop-renderer:

'jest-matchers' is imported by src/renderers/noop/ReactNoopEntry.js, but could not be resolved – treating it as an external dependency

Maybe need to add it here.

Allows us to make assertions on the values that are yielded when
performing work. In our existing tests, we do this manually by pushing
into an array.

ReactNoop.flushThrough extends this concept. It accepts an array of
expected values and flushes until those values are yielded.
@acdlite acdlite changed the title Do not schedule a callback if already inside a callback Remove Animation priority Jun 15, 2017
yarn.lock Outdated
dependencies:
chalk "^1.1.3"
pretty-format "^20.0.3"

jest-matchers@^19.0.0:
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 to download two of these questionable deps? Great.

Copy link
Collaborator Author

@acdlite acdlite Jun 15, 2017

Choose a reason for hiding this comment

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

I can change the version constraint ¯\_(ツ)_/¯ It's just the noop renderer

scheduleAnimationCallback: ReactDOMFrameScheduling.rAF,

scheduleDeferredCallback: ReactDOMFrameScheduling.rIC,
scheduleCallback: ReactDOMFrameScheduling.rIC,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is too vague. scheduleDeferredCallback?

We could call it requestIdleCallback but I suspect we don't actually want to be forced to pass the concept of a deadline to this but rather ask it "shouldIContinue()`? To allow for environments that are not timer based but input based.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

scheduleAsyncCallback? Seems redundant, but so does "deferred" :D Will we ever schedule more than one type of callback?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Remember that this is a public API for renderers to use. It should first and foremost describe what kind of function they're supposed to provide here.

Async isn't quite enough since that could be a micro-task tick (like the Promise queue).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let's just call it scheduleDeferredCallback for now but idk how descriptive that actually is.

switch (nextPriorityLevel) {
case SynchronousPriority:
case TaskPriority:
// We have remaining syncrhonous or task work. Keep performing it,
Copy link
Collaborator

Choose a reason for hiding this comment

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

typo


let keepPerformingWork = true;
let hasRemainingAsyncWork = false;
while (keepPerformingWork && fatalError === null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the intention here with all the possible states (9) these can be in? I have trouble following the extra flags. The flags are indicative of a code smell. Can you refactor this in terms of control flow?

Copy link
Collaborator Author

@acdlite acdlite Jun 15, 2017

Choose a reason for hiding this comment

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

Basically this is a result of me trying to both 1) avoid while (true), and 2) not repeat the same checks that are done in workLoop. Should I just use while (true) and break / continue?

I could get rid of hasRemainingAsyncWork by scheduling the callback inside this loop instead of once we exit it. That's how I did it originally but I found this easier to follow. I'll change it back.

case TaskPriority:
// TODO: If we're not already performing work, schedule a
// deferred callback.
return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did this return get lost? The TODO above seems wrong to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the topic I brought up at lunch. I believe you're the one who suggested this to me, and why I added this comment :)

Seems correct to me that if we setState at Task priority, but we're not already performing work, we should schedule a callback and perform it later. The other option is to treat it as synchronous work, I suppose. We need to decide whether "task" priority should be treated as semantically different from sync priority for non-nested updates.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This would schedule an unnecessary deferred callback if you call setState in componentDidMount inside a sync render.

Would be nice to have a test for that like Dan's ReactIncrementalPerf-test.js.snap that spawned these other changes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a snapshot test

@@ -309,37 +267,6 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop.getChildren()).toEqual([span(5)]);
});

it('does not perform animation work after time runs out', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't this still a valuable test but for HighPriority instead of AnimationPriority?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe? Why didn't we already have a HighPriority test then? My impression was that this test existed because of the different semantics for Animation priority versus async/deferred priorities.

class Foo extends React.Component {
state = {};
componentDidUpdate() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why delete this from the assertion? It has nothing to do with the ReactNoop.yield stuff so you've lost a part of the test, no?

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 asserted on the children instead.

expect(ReactNoop.getChildren()).toEqual([span('')]);

There's no advantage to scheduling updates with animation priority
versus scheduling sync work inside requestAnimationCallback. So we can
remove all the animation-specific code. Now there's only one type of
callback.
@acdlite
Copy link
Collaborator Author

acdlite commented Jun 16, 2017

@sebmarkbage Updated

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.

Add unit tests for the things we found might break. :)

}
}

function performWork(
priorityLevel: PriorityLevel,
Copy link
Collaborator

Choose a reason for hiding this comment

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

We realized that this is necessary but missing a unit test.

case TaskPriority:
// TODO: If we're not already performing work, schedule a
// deferred callback.
return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This would schedule an unnecessary deferred callback if you call setState in componentDidMount inside a sync render.

Would be nice to have a test for that like Dan's ReactIncrementalPerf-test.js.snap that spawned these other changes.

@acdlite
Copy link
Collaborator Author

acdlite commented Jun 16, 2017

@sebmarkbage I think I addressed everything

const previousIsBatchingUpdates = isBatchingUpdates;
// This is only true if we're nested inside either batchedUpdates or a
// lifecycle method.
isUnbatchingUpdates = isBatchingUpdates || isPerformingWork;
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 set if isPerformingWork and then the consequence is ignored if isPerformingWork?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're right, this check is redundant. I added this check first, then realized it made more sense to check isPerformingWork in scheduleUpdate, but forgot to remove the check here. I'll remove 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.

Fixed

Behavior now matches Stack. It's unfortunate that this prevents us
from unifying SynchronousPriority and TaskPriority.
// batchedUpdates or a lifecycle. We should only flush
// synchronous work, not task work.
performWork(SynchronousPriority, null);
} else {
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 branch needed? Why aren't we just scheduling with TaskPriority if it is supposed to be Task priority?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Discussed offline, but documenting here for anyone watching.

Both branches are performing synchronous work. What's different is the first parameter to performWork, which specifies the minimum level of work that should be performed. In the normal case, we should perform both task and synchronous work. In the isUnbatchingUpdates case, we should only perform synchronous work.

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.

Ok. I hope this is correct. Let's keep iterating.

@acdlite acdlite closed this Jun 20, 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.

None yet

4 participants