Skip to content

refactor(core): minor ComponentDef improvements#46093

Closed
crisbeto wants to merge 1 commit intoangular:mainfrom
crisbeto:component-def-improvements
Closed

refactor(core): minor ComponentDef improvements#46093
crisbeto wants to merge 1 commit intoangular:mainfrom
crisbeto:component-def-improvements

Conversation

@crisbeto
Copy link
Copy Markdown
Member

Makes the following improvements in the runtime:

  • Uses the unique ID of the component definition to keep track of its injector in the StandaloneFeature, instead of the definition itself. This reduces the amount of memory we can leak, if something doesn't get cleaned up.
  • Changes the naming and description of the ComponentDef.id to reflect what it is used for.

Makes the following improvements in the runtime:
* Uses the unique ID of the component definition to keep track of its injector in the `StandaloneFeature`, instead of the definition itself. This reduces the amount of memory we can leak, if something doesn't get cleaned up.
* Changes the naming and description of the `ComponentDef.id` to reflect what it is used for.
@crisbeto crisbeto marked this pull request as ready for review May 22, 2022 09:08
@pullapprove pullapprove bot requested a review from alxhub May 22, 2022 09:08
@crisbeto crisbeto added action: review The PR is still awaiting reviews from at least one requested reviewer area: core Issues related to the framework runtime target: rc This PR is targeted for the next release-candidate labels May 22, 2022
@ngbot ngbot bot added this to the Backlog milestone May 22, 2022

if (!this.cachedInjectors.has(componentDef)) {
if (!this.cachedInjectors.has(componentDef.id)) {
const providers = internalImportProvidersFrom(false, componentDef.type);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This reduces the amount of memory we can leak, if something doesn't get cleaned up.

@crisbeto I think this line and the underlying logic in the internalImportProvidersFrom (this one specifically) would prevent the componentDef from being released from the memory, i.e. there will always be a reference to the componentDef.type (which points back to componentDef) when the StandaloneService is around. If my understanding is correct, this change has no effect on the memory management.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Isn't that one going to get destroyed together with the LView or am I misunderstanding?

Either way, my thinking with this change is that it's functionally the same and in the worst case it'll work exactly the same as it does now. In the best case we might get a small memory improvement 🤷

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@crisbeto looking at the code again, I think this change makes sense 👍

The code that I referred to in my previous message (this one) is actually invoked for NgModules only. For the nested standalone components we don't store their defs in an injector, we just iterate over its dependencies, so there should be no extra references. The change that you've made should allow to avoid a reference from the cached injector map. So technically if there are no other references to a particular componentDef (once a component is destroyed) - it should be released from the memory. Currently we would retain the componentDef even if it's unused (when there are no active instances of that component).

Also adding @pkozlowski-opensource as a reviewer, since he is the most familiar with this code.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So, I don't see harm in making those changes but also not sure if it moves a needle. Observations:

The net effect of the above is that we already hold references to all the dynamically bootstrapped types.

What I like about this PR is that it clarifies the component's id purpose.
What I dislike is that it is a bit harder now to understand the standalone future.

As I've said, no strong opinion here - don't mind either way.

Copy link
Copy Markdown
Contributor

@AndrewKushnir AndrewKushnir May 24, 2022

Choose a reason for hiding this comment

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

My understanding is that lazy-loaded components (via router) would benefit from this change. For example, if we move from route A, which uses ComponentA to route B, which uses ComponentB (both components are standalone) - the ComponentA class (and everything that it refers to and depends on) would be retained in memory, since there is a reference to it (because it's used as a key in a map) in the Standalone injector service. With this change, this reference is no longer present, thus allowing the memory to be released (unless there are some other refs to the class retained somewhere). If my understanding is correct, this PR fixes a memory leak that may happen in certain scenarios (lazy loading).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not familiar with the router internals to confirm that this is a problem. More specifically I don't know how component reuse works. Anyhow, I see your point: having less references === more safety.

I don't mind either way. Another option we could consider is the usage of a WeekMap

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Andrew raised this over DM: we can't use a WeakMap, because we have to be able to iterate over the keys in ngOnDestroy.

Copy link
Copy Markdown
Member

@clydin clydin May 25, 2022

Choose a reason for hiding this comment

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

Was going to suggest a WeakMap as well but any iteration rules it out unfortunately. I think it would otherwise be ideal here though. What if we used a combination of WeakMap for the lookup and a Set (or array) for the injector cleanup. Has a side benefit of making it more clear that there is both caching and cleanup tracking happening in the class.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh right, WeakMap is not an option then.

As I've said, no objections on merging this PR, it does provide an additional safety net.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could use a WeakMap and a separate Set to keep track of the injector instances themselves (for iteration). Either way is fine with me.

export interface ComponentDef<T> extends DirectiveDef<T> {
/**
* Runtime unique component ID.
* Unique ID for the component. Used in view encapsulation and
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"for the component" => "for the component type / definition" - or anything else that makes it clear that we are talking about component classes and not instances.

Copy link
Copy Markdown
Member

@pkozlowski-opensource pkozlowski-opensource left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Copy Markdown
Member

@alxhub alxhub left a comment

Choose a reason for hiding this comment

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

Note that this PR should not merge within the current feature freeze period and should wait for 14.0.1.


if (!this.cachedInjectors.has(componentDef)) {
if (!this.cachedInjectors.has(componentDef.id)) {
const providers = internalImportProvidersFrom(false, componentDef.type);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could use a WeakMap and a separate Set to keep track of the injector instances themselves (for iteration). Either way is fine with me.

@crisbeto crisbeto added action: merge The PR is ready for merge by the caretaker state: blocked target: patch This PR is targeted for the next patch release and removed action: review The PR is still awaiting reviews from at least one requested reviewer target: rc This PR is targeted for the next release-candidate labels Jun 2, 2022
@crisbeto
Copy link
Copy Markdown
Member Author

crisbeto commented Jun 2, 2022

Blocked until after the commit freeze.

@alxhub
Copy link
Copy Markdown
Member

alxhub commented Jun 2, 2022

This PR was merged into the repository by commit 0206c10.

alxhub pushed a commit that referenced this pull request Jun 2, 2022
Makes the following improvements in the runtime:
* Uses the unique ID of the component definition to keep track of its injector in the `StandaloneFeature`, instead of the definition itself. This reduces the amount of memory we can leak, if something doesn't get cleaned up.
* Changes the naming and description of the `ComponentDef.id` to reflect what it is used for.

PR Close #46093
@alxhub alxhub closed this in 0206c10 Jun 2, 2022
@angular-automatic-lock-bot
Copy link
Copy Markdown

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 Jul 3, 2022
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: core Issues related to the framework runtime target: patch This PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants