Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

perf(*): don't trigger digests after enter/leave of structural directives #15345

Merged
merged 2 commits into from
Nov 23, 2016

Conversation

Narretz
Copy link
Contributor

@Narretz Narretz commented Nov 1, 2016

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
perf

What is the current behavior? (You can also link to an open issue here)
Structural animations such as ngIf trigger an additional digest after $animate.leave completes

Does this PR introduce a breaking change?
No

Please check if the PR fulfills these requirements

@dcherman
Copy link
Contributor

dcherman commented Nov 1, 2016

👏

Nitpicky, but unnessecary --> unnecessary in the commit message. ❤️

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

An alternative, more radical approach (involving a BC) would be using $$q instead of $q for the AnimateRunner and let people manually call $apply() if their callback needs it. Most of the time, the animation callbacks trigger DOM-related operations/cleanup (and not touch on business logic or data), so a digest should not be necessary.

Unfortunately, such a BC has to wait for 1.7 now 😞

@@ -115,7 +115,7 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) {
}
if (block) {
previousElements = getBlockNodes(block.clone);
$animate.leave(previousElements).then(function() {
$animate.leave(previousElements).done(function() {
Copy link
Member

Choose a reason for hiding this comment

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

This is not functionally the same as .then(). We need to check for success/error.
(Not sure if it makes a difference in practice.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doh ... you are right, I'll change that.

for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) {

// Start from the end, in case the array is modified during the loop
for (i = previousLeaveAnimations.length - 1; i >= 0; i--) {
Copy link
Member

Choose a reason for hiding this comment

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

This change should not be necessary if we call the .done() callback on success only.

@Narretz
Copy link
Contributor Author

Narretz commented Nov 1, 2016

@gkalpak Using $$q and letting people do $apply manually was implemented before and was changed specifically to use digest-integrated promises. I don't know if this was actually an issue for people, but I assume that it at least makes the behavior consistent with $http, and $q issued promises.

@Narretz Narretz changed the title perf(*): don't trigger additional digests on enter/leave of structura… …l directives perf(*): don't trigger digests after enter/leave of structural directives Nov 3, 2016
@Narretz
Copy link
Contributor Author

Narretz commented Nov 4, 2016

@gkalpak This is ready for another review :)

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

LGTM with the exception of one comment/question.

if (runner.end) {
runner.end();
if (runner.cancel) {
runner.cancel();
Copy link
Member

Choose a reason for hiding this comment

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

I think this is a good change (i.e. the right thing to do), but isn't it a breaking change.
I couldn't find the behavior documented anywhere btw.

Copy link
Contributor Author

@Narretz Narretz Nov 5, 2016

Choose a reason for hiding this comment

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

If it's not documented that it's not a change, so also no Breaking Change :>
Seriously though, I thought this change would only align ng.$animate with ngAnimate.$animate, but it turns out cancelling an actual animation resolves the promise.
The reason why I change this code is because otherwise .done isn't called with false, so some switch tests fail. The problem is that these ngSwitch tests are "synthetic" in the sense that they never complete the "animations" they start because the rAFs aren't flushed. I think if we did that, then we didn't have to do this change. However, we would have to insert raF flushes in about 8 ngSwitch tests where they actually don't need to be there from considering the test's scope (these are not animation tests).

I'd be okay with introducing promise rejection on cancellation as a breaking change for 1.7 on both ng + ngAnimate, but that would mean this perf improvemtn has to wait ... at least for ngSwitch.

Copy link
Member

Choose a reason for hiding this comment

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

I thought this change would only align ng.$animate with ngAnimate.$animate

Isn't it the exact same $animate that is being used. Only underlying pieces (e.g. $$animateQueue) are different with ngAnimate. So, whether we change it or not, $animate will be consistent with and without ngAnimate.

The reason why I change this code is because otherwise .done isn't called with false, so some switch tests fail.

It sounds more like a test issue, not an implementation issue.

I'd be okay with introducing promise rejection on cancellation as a breaking change for 1.7 on both ng + ngAnimate, but that would mean this perf improvemtn has to wait

Sorry, I didn't get why. Apart from some (bad) tests failing, what is the problem with using .done() (but keeping the runner.end() call above)?

Copy link
Contributor Author

@Narretz Narretz Nov 5, 2016

Choose a reason for hiding this comment

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

If $animate.cancel doesn't reject the promise, then .done callback handlers aren't called with false, which means in the ngSwitch done handler, I cannot distinguish between failed and successful animations, which leads to "cannot read function .end of undefined" error I had at the beginning. I then changed the code in ngSwitch to read the stored runners from the end to work around this problem, but then you said, I should check for successful animations inside done, which is how I found this problem with cancel in the first place

It's probably easier to understand if you run the tests with the various changes to see the different behavior.

Copy link
Member

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 it worked before but not now. Previously, .then(cb) was used. This is implemented in $animate like this (code changed for brevity):

then: resolveHandler => {
  return $q((resolve, reject) => this.done(status => status === false ? reject() : resolve())).
            then(resolveHandler)
}

So, afaict we should be able to safely replace .then(cb) with .done(status => status !== false && cb()) keeping the exact same behavior minus the extra digest (and asynchronicity) of $q.

Is the fact that the callback is now synchronously called causing the problem?

Copy link
Member

Choose a reason for hiding this comment

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

I don't see why my fix would fail. It doesn't rely on the index but on the item itself (looking it up via previousLeaveAnimations.indexOf(...)). So, it is not affected by structural changes to the array.

Removing in reverse order should also work. (Why did I say I didn't like that?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because the error isn't coming from the splicer but from the cancel loop: http://plnkr.co/edit/MPUiYm4mTr9nNwg8SRS3?p=preview (Apologies if I'm being dense, it's very late again :D)

Copy link
Member

@gkalpak gkalpak Nov 9, 2016

Choose a reason for hiding this comment

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

Right. I think we have two problems (and my suggestion only takes care of one of them). So, in addition we should also change the cancellation code as follows (demo):

// Before:
for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) {
  $animate.cancel(previousLeaveAnimations[i]);
}
previousLeaveAnimations.length = 0;

// After:
while (previousLeaveAnimations.length) {
  $animate.cancel(previousLeaveAnimations.shift());
}

In any case, I think it is fine to traverse the array backwards as an alternative (if it doesn't create other problems).

Copy link
Contributor Author

@Narretz Narretz Nov 9, 2016

Choose a reason for hiding this comment

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

But do we need the splice change? I don't see any errors without it.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need the slice change if we traverse the array backwards. Either backwards-array-traversal or slice-change+while-loop. Up to you 😃

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

A couple of minor comments. LGTM otherwise.

@@ -333,6 +333,36 @@ describe('ngSwitch', function() {
}));


it('should handle changes to the switch value in a digest loop with multiple value matches',
inject(function($$rAF, $compile, $rootScope, $timeout) {
Copy link
Member

Choose a reason for hiding this comment

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

Are $$rAF and $timeout needed?

$rootScope.select = 2;
$rootScope.$apply();
$$rAF.flush();
Copy link
Member

Choose a reason for hiding this comment

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

Now that 696b7a4 has been added, are these extra $$rAF calls necessary?



it('should handle changes to the switch value in a digest loop with multiple value matches',
inject(function($$rAF, $compile, $rootScope, $timeout) {
Copy link
Member

Choose a reason for hiding this comment

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

Are $$rAF and $timeout necessary?

@Narretz
Copy link
Contributor Author

Narretz commented Nov 14, 2016

Good catch, no, these flushes are not necessary. The only one necessary is in the test that ensures that no digest is triggered after the leave animation

@petebacondarwin
Copy link
Contributor

OK, are we happy to merge this?

@gkalpak
Copy link
Member

gkalpak commented Nov 21, 2016

I am 😃

@petebacondarwin
Copy link
Contributor

Please do: 1.5.x and master?

@Narretz Narretz assigned Narretz and unassigned gkalpak Nov 22, 2016
…ives

ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned
by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView).
Previously, they were using promise callbacks (`then`), which caused an unnessecary digest.

Background:

In angular@bf0f550, animation
promises where introduced instead of animation callbacks. These promises were however not tied to
the digest cycle, so you had to manually call `$apply` inside them.

This was changed in angular@c8700f0,
so animation promise callbacks would always trigger a `$digest`. This meant that several built-in
directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude,
ngView). The `done` callback, which receives a single argument indicating success / failure was
introduced to allow digest-less responses, but wasn't applied to these directives.

Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and
no actual animations are running, because the animation runner code is in the core ng module.

Fixes angular#15322
Closes angular#15345
@Narretz Narretz merged commit f4fb6e0 into angular:master Nov 23, 2016
Narretz added a commit that referenced this pull request Nov 23, 2016
…ives

ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned
by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView).
Previously, they were using promise callbacks (`then`), which caused an unnessecary digest.

Background:

In bf0f550, animation
promises where introduced instead of animation callbacks. These promises were however not tied to
the digest cycle, so you had to manually call `$apply` inside them.

This was changed in c8700f0,
so animation promise callbacks would always trigger a `$digest`. This meant that several built-in
directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude,
ngView). The `done` callback, which receives a single argument indicating success / failure was
introduced to allow digest-less responses, but wasn't applied to these directives.

Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and
no actual animations are running, because the animation runner code is in the core ng module.

Fixes #15322
Closes #15345
petebacondarwin pushed a commit that referenced this pull request Nov 23, 2016
…ives

ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned
by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView).
Previously, they were using promise callbacks (`then`), which caused an unnessecary digest.

Background:

In bf0f550, animation
promises where introduced instead of animation callbacks. These promises were however not tied to
the digest cycle, so you had to manually call `$apply` inside them.

This was changed in c8700f0,
so animation promise callbacks would always trigger a `$digest`. This meant that several built-in
directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude,
ngView). The `done` callback, which receives a single argument indicating success / failure was
introduced to allow digest-less responses, but wasn't applied to these directives.

Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and
no actual animations are running, because the animation runner code is in the core ng module.

Fixes #15322
Closes #15345
petebacondarwin pushed a commit that referenced this pull request Nov 24, 2016
…ives

ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned
by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView).
Previously, they were using promise callbacks (`then`), which caused an unnessecary digest.

Background:

In bf0f550, animation
promises where introduced instead of animation callbacks. These promises were however not tied to
the digest cycle, so you had to manually call `$apply` inside them.

This was changed in c8700f0,
so animation promise callbacks would always trigger a `$digest`. This meant that several built-in
directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude,
ngView). The `done` callback, which receives a single argument indicating success / failure was
introduced to allow digest-less responses, but wasn't applied to these directives.

Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and
no actual animations are running, because the animation runner code is in the core ng module.

Fixes #15322
Closes #15345
ellimist pushed a commit to ellimist/angular.js that referenced this pull request Mar 15, 2017
…ives

ngIf, ngInclude, ngSwitch, and ngView now use the `done` callback functions on animation runners returned
by leave/enter animations to do internal cleanup (and $anchorScroll for ngInclude and ngView).
Previously, they were using promise callbacks (`then`), which caused an unnessecary digest.

Background:

In angular@bf0f550, animation
promises where introduced instead of animation callbacks. These promises were however not tied to
the digest cycle, so you had to manually call `$apply` inside them.

This was changed in angular@c8700f0,
so animation promise callbacks would always trigger a `$digest`. This meant that several built-in
directives would now trigger additional digests on leave (ngIf, ngSwitch) or enter/leave (ngInclude,
ngView). The `done` callback, which receives a single argument indicating success / failure was
introduced to allow digest-less responses, but wasn't applied to these directives.

Note that this applies to all calls to $animate.enter/leave, even if ngAnimate isn't included, and
no actual animations are running, because the animation runner code is in the core ng module.

Fixes angular#15322
Closes angular#15345
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants