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

perf(animations): improve algorithm to balance animation namespaces #45057

Closed
wants to merge 1 commit into from

Conversation

JoostK
Copy link
Member

@JoostK JoostK commented Feb 11, 2022

The prior approach would consider all existing namespaces from back to front
to find the one that's the closest ancestor for a given host element. An
expensive contains operation was used which needed to traverse all the
way up the document root for each existing namespace. This commit implements
an optimization where the closest namespace is found by traversing up from
the host element, avoiding repeated DOM traversal.

Closes #45055

@ngbot ngbot bot modified the milestone: Backlog Feb 11, 2022
@JoostK JoostK mentioned this pull request Feb 11, 2022
@JoostK JoostK force-pushed the animations-balance-perf branch from d4fd692 to f1c9c40 Compare February 13, 2022 16:42
Copy link
Member Author

@JoostK JoostK left a comment

Choose a reason for hiding this comment

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

As noted in #45055 (comment), the impact of this change is HUGE for applications that have large component trees with many components with animations.

@@ -151,12 +160,13 @@ if (_isNode || typeof Element !== 'undefined') {
if (!isBrowser()) {
_contains = (elm1, elm2) => elm1.contains(elm2);
} else {
_documentElement = /* @__PURE__ */ (() => document.documentElement)();
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't like this and welcome suggestions on how to do this differently.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious why do we need an iffe here (is it due to the top-level assignment not being optimized by Terser)? If so, may be we can add a quick comment here for future context?

Copy link
Member Author

Choose a reason for hiding this comment

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

There's a ban-toplevel-statement tslint rule (or a similar rule name, I don't recall by heart) that would fire if this is not wrapped inside an IIFE.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, got it. We do have some places where we disable that check via // tslint:disable-next-line: no-toplevel-property-access. However in this particular case I'm not sure if Terser would be able to tree-shake-away that code without that iffe.

Suggested change
_documentElement = /* @__PURE__ */ (() => document.documentElement)();
// tslint:disable-next-line: no-toplevel-property-access
_documentElement = /* @__PURE__ */ document.documentElement;

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah exactly. I also checked whether we have existing suppressions but the ones I can across were for const enums. In this case a bundler would likely assume a potential side-effect for the property read and be able to optimize less aggressively.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for additional info. I think adding a quick comment (to explain why) here should be enough 👍

* be required to implement this method. This method is to become required in a major version of
* Angular.
*/
abstract getParentElement?(element: unknown): unknown;
Copy link
Member Author

Choose a reason for hiding this comment

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

I wonder what the intended use of the AnimationDriver public API really is (TBH I wonder why it's public API at all). If it's to allow custom implementations then this would have to be optional like I've done now, but if it's just for callers into this interface then we could just make this required from the start. I'm interested to hear other's thoughts on this.

I also went with unknown even though it's a bit out of place compared to the other methods, but it seems silly to continue using any when there's a safer alternative available.

Copy link
Contributor

Choose a reason for hiding this comment

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

After some digging in a prior PR, we discovered that AnimationDriver is public API to allow people to write their own custom AnimationDriver implementations. I'm sure that's been done all of zero times, but the option is there. We could consider deprecating that.

I'm in favor of unknown too.

Copy link
Contributor

Choose a reason for hiding this comment

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

@JoostK Just wanted to check if you're planning an immediate follow-up that targets v14 to make this required. I think it's probably good to have this in patch as a perf fix.

Copy link
Member 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 do that, yes. The impact of the breakage is likely negligible to warrant waiting for later majors, so I'll open a follow up to clean this up immediately.

@JoostK JoostK requested a review from jessicajaniuk February 13, 2022 16:54
@JoostK JoostK added action: review The PR is still awaiting reviews from at least one requested reviewer target: patch This PR is targeted for the next patch release labels Feb 13, 2022
@JoostK JoostK marked this pull request as ready for review February 13, 2022 16:54
@JoostK JoostK removed the state: WIP label Feb 13, 2022
Copy link
Contributor

@jessicajaniuk jessicajaniuk left a comment

Choose a reason for hiding this comment

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

LGTM 🍪

reviewed-for: public-api, fw-testing, fw-core, fw-animations

* be required to implement this method. This method is to become required in a major version of
* Angular.
*/
abstract getParentElement?(element: unknown): unknown;
Copy link
Contributor

Choose a reason for hiding this comment

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

After some digging in a prior PR, we discovered that AnimationDriver is public API to allow people to write their own custom AnimationDriver implementations. I'm sure that's been done all of zero times, but the option is there. We could consider deprecating that.

I'm in favor of unknown too.

this._namespaceList.splice(i + 1, 0, ns);
found = true;
break;
if (this.driver.getParentElement !== undefined) {
Copy link
Contributor

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 great optimization. I wish it didn't make this method so long.

@jessicajaniuk jessicajaniuk removed their request for review February 14, 2022 18:29
@pullapprove pullapprove bot requested a review from dylhunn February 14, 2022 18:29
Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

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

reviewed-for: public-api

* be required to implement this method. This method is to become required in a major version of
* Angular.
*/
abstract getParentElement?(element: unknown): unknown;
Copy link
Contributor

Choose a reason for hiding this comment

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

@JoostK Just wanted to check if you're planning an immediate follow-up that targets v14 to make this required. I think it's probably good to have this in patch as a perf fix.

@pullapprove pullapprove bot requested a review from jelbourn February 14, 2022 19:42
Copy link
Contributor

@AndrewKushnir AndrewKushnir left a comment

Choose a reason for hiding this comment

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

@JoostK awesome investigation and the fix 👍

Reviewed-for: public-api

@@ -151,12 +160,13 @@ if (_isNode || typeof Element !== 'undefined') {
if (!isBrowser()) {
_contains = (elm1, elm2) => elm1.contains(elm2);
} else {
_documentElement = /* @__PURE__ */ (() => document.documentElement)();
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious why do we need an iffe here (is it due to the top-level assignment not being optimized by Terser)? If so, may be we can add a quick comment here for future context?

Copy link
Contributor

@dylhunn dylhunn left a comment

Choose a reason for hiding this comment

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

reviewed-for: public-api

@JoostK JoostK force-pushed the animations-balance-perf branch from f1c9c40 to 49ecee7 Compare February 15, 2022 19:41
@JoostK JoostK added action: presubmit The PR is in need of a google3 presubmit and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Feb 15, 2022
@JoostK JoostK force-pushed the animations-balance-perf branch from 49ecee7 to c09ca47 Compare February 15, 2022 20:30
@JoostK JoostK force-pushed the animations-balance-perf branch from c09ca47 to d5ee1ba Compare February 15, 2022 20:49
@JoostK
Copy link
Member Author

JoostK commented Feb 15, 2022

Another rebase tipped this beyond a bundle size limit, so this needs another round of approvals. For context, I'll open a PR targeting master that makes getParentElement required, allowing to drop some code paths again.

Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

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

reviewed-for: size-tracking

@pullapprove pullapprove bot requested a review from dylhunn February 15, 2022 21:38
Copy link
Contributor

@AndrewKushnir AndrewKushnir left a comment

Choose a reason for hiding this comment

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

Reviewed-for: size-tracking

@@ -592,16 +592,40 @@ export class TransitionAnimationEngine {
const limit = this._namespaceList.length - 1;
Copy link
Contributor

@AndrewKushnir AndrewKushnir Feb 15, 2022

Choose a reason for hiding this comment

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

Quick comment on the ~700 bytes increase in goldens/size-tracking/aio-payloads.json: this improvement is definitely worth it 👍

One small improvement/refactoring that we can make in this function (to offset some of this increase) is to extract commonly accessed fields into a local var (since property names are not mangled by our build pipeline):

const nsList = this._namespaceList;
const nsByHostElement = this.namespacesByHostElement;
const getParentElement = this.driver.getParentElement;

It'd be minified to something like this:

var a = t._namespaceList,
      b = t.namespacesByHostElement,
      c = t.driver.getParentElement;

So the long names will not be present in the function body.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've extracted namespaceList and namespacesByHostElement into local variables, that is indeed a clever idea. I've kept the getParentElement reference as is, as changing that into a local variable requires calling it using .call(this.driver, ...) which isn't ideal.

The prior approach would consider all existing namespaces from back to front
to find the one that's the closest ancestor for a given host element. An
expensive `contains` operation was used which needed to traverse all the
way up the document root _for each existing namespace_. This commit implements
an optimization where the closest namespace is found by traversing up from
the host element, avoiding repeated DOM traversal.

Closes angular#45055
@JoostK JoostK force-pushed the animations-balance-perf branch from d5ee1ba to ff85a96 Compare February 16, 2022 18:34
@JoostK JoostK added the action: merge The PR is ready for merge by the caretaker label Feb 16, 2022
@jessicajaniuk jessicajaniuk removed the action: presubmit The PR is in need of a google3 presubmit label Feb 16, 2022
@alxhub alxhub added target: minor This PR is targeted for the next minor release and removed target: patch This PR is targeted for the next patch release labels Feb 16, 2022
@alxhub
Copy link
Member

alxhub commented Feb 16, 2022

FYI, this doesn't merge cleanly to 13.2.x.

@alxhub
Copy link
Member

alxhub commented Feb 16, 2022

This PR was merged into the repository by commit 5c7c56b.

@alxhub alxhub closed this in 5c7c56b Feb 16, 2022
JoostK added a commit to JoostK/angular that referenced this pull request Feb 16, 2022
This change is a follow up to angular#45057 to make `AnimationDriver.getParentElement`
a required method, which allows removing the slow path for the animation
namespace insertion logic.

BREAKING CHANGE:

The `AnimationDriver.getParentElement` method has become required, so any
implementors of this interface are now required to provide an implementation
for this method. This breakage is unlikely to affect application developers,
as `AnimationDriver` is not expected to be implemented in user code.
JoostK added a commit to JoostK/angular that referenced this pull request Feb 16, 2022
This change is a follow up to angular#45057 to make `AnimationDriver.getParentElement`
a required method, which allows removing the slow path for the animation
namespace insertion logic.

BREAKING CHANGE:

The `AnimationDriver.getParentElement` method has become required, so any
implementors of this interface are now required to provide an implementation
for this method. This breakage is unlikely to affect application developers,
as `AnimationDriver` is not expected to be implemented in user code.
JoostK added a commit to JoostK/angular that referenced this pull request Feb 17, 2022
This change is a follow up to angular#45057 to make `AnimationDriver.getParentElement`
a required method, which allows removing the slow path for the animation
namespace insertion logic.

BREAKING CHANGE:

The `AnimationDriver.getParentElement` method has become required, so any
implementors of this interface are now required to provide an implementation
for this method. This breakage is unlikely to affect application developers,
as `AnimationDriver` is not expected to be implemented in user code.
atscott pushed a commit that referenced this pull request Feb 22, 2022
#45114)

This change is a follow up to #45057 to make `AnimationDriver.getParentElement`
a required method, which allows removing the slow path for the animation
namespace insertion logic.

BREAKING CHANGE:

The `AnimationDriver.getParentElement` method has become required, so any
implementors of this interface are now required to provide an implementation
for this method. This breakage is unlikely to affect application developers,
as `AnimationDriver` is not expected to be implemented in user code.

PR Close #45114
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Mar 19, 2022
josmar-crwdstffng pushed a commit to josmar-crwdstffng/angular that referenced this pull request Apr 8, 2022
…ngular#45057)

The prior approach would consider all existing namespaces from back to front
to find the one that's the closest ancestor for a given host element. An
expensive `contains` operation was used which needed to traverse all the
way up the document root _for each existing namespace_. This commit implements
an optimization where the closest namespace is found by traversing up from
the host element, avoiding repeated DOM traversal.

Closes angular#45055

PR Close angular#45057
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: animations target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Performance issue
6 participants