8000 fix(common): cleanup `updateLatestValue` if view is destroyed before promise resolves by arturovt · Pull Request #58041 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

fix(common): cleanup updateLatestValue if view is destroyed before promise resolves #58041

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

Closed
wants to merge 1 commit into from

Conversation

arturovt
Copy link
Contributor
@arturovt arturovt commented Oct 1, 2024

According to the promise specification, promises are not cancellable by default.
Once a promise is created, it will either resolve or reject, and it doesn't
provide a built-in mechanism to cancel it.
There may be situations where a promise is provided, and it either resolves after
the pipe has been destroyed or never resolves at all. If the promise never
resolves — potentially due to factors beyond our control, such as third-party
libraries — this can lead to a memory leak.
When we use async.then(updateLatestValue), the engine captures a reference to the
updateLatestValue function. This allows the promise to invoke that function when it
resolves. In this case, the promise directly captures a reference to the
updateLatestValue function. If the promise resolves later, it retains a reference
to the original updateLatestValue, meaning that even if the context where
updateLatestValue was defined has been destroyed, the function reference remains in memory.
This can lead to memory leaks if updateLatestValue is no longer needed or if it holds
onto resources that should be released.
When we do async.then(v => ...) the promise captures a reference to the lambda
function (the arrow function).
When we assign updateLatestValue = null within the context of an unsubscribe function,
we're changing the reference of updateLatestValue in the current scope to null.
The lambda will no longer have access to it after the assignment, effectively
preventing any further calls to the original function and allowing it to be garbage collected.

If Chrome is built with additional flags and run with --allow-natives-syntax --track-retaining-path,
we could use %DebugTrackRetainingPath to see the distance from the root for updateLatestValue
if it's passed directly to async.then, e.g.:

%DebugTrackRetainingPath(updateLatestValue);

// Distance from root 4: 0x123456789abc <JSPromise (sfi = 0x1fbb02e2d7f1)>

@angular-robot angular-robot bot added the area: common Issues related to APIs in the @angular/common package label Oct 1, 2024
@ngbot ngbot bot added this to the Backlog milestone Oct 1, 2024
@arturovt arturovt marked this pull request as ready for review October 2, 2024 14:20
@pullapprove pullapprove bot requested a review from alxhub October 2, 2024 14:20
@arturovt arturovt force-pushed the fix/common_async_pipe_leak branch 2 times, most recently from d25bcf2 to b69033f Compare October 31, 2024 00:27
@arturovt arturovt changed the title fix(common): wrap promise in from() to prevent potential memory leaks fix(common): cleanup updateLatestValue if view is destroyed before promise resolves Oct 31, 2024
@AndrewKushnir AndrewKushnir modified the milestones: Backlog, v19 final Nov 6, 2024
@arturovt arturovt force-pushed the fix/common_async_pipe_leak branch from b69033f to c5e69fc Compare January 23, 2025 17:45
@thePunderWoman
Copy link
Contributor

@arturovt Can you rebase to resolve the conflicts here?

@arturovt arturovt force-pushed the fix/common_async_pipe_leak branch from c5e69fc to c43b43e Compare March 5, 2025 17:44
@thePunderWoman thePunderWoman requested review from pkozlowski-opensource and removed request for alxhub March 5, 2025 17:50
@arturovt arturovt force-pushed the fix/common_async_pipe_leak branch from c43b43e to 7ba238e Compare March 5, 2025 18:30
@arturovt arturovt force-pushed the fix/common_async_pipe_leak branch 2 times, most recently from c42c2ff to 16d6986 Compare April 2, 2025 20:00
…promise resolves

According to the promise specification, promises are not cancellable by default.
Once a promise is created, it will either resolve or reject, and it doesn't
provide a built-in mechanism to cancel it.
There may be situations where a promise is provided, and it either resolves after
the pipe has been destroyed or never resolves at all. If the promise never
resolves — potentially due to factors beyond our control, such as third-party
libraries — this can lead to a memory leak.
When we use `async.then(updateLatestValue)`, the engine captures a reference to the
`updateLatestValue` function. This allows the promise to invoke that function when it
resolves. In this case, the promise directly captures a reference to the
`updateLatestValue` function. If the promise resolves later, it retains a reference
to the original `updateLatestValue`, meaning that even if the context where
`updateLatestValue` was defined has been destroyed, the function reference remains in memory.
This can lead to memory leaks if `updateLatestValue` is no longer needed or if it holds
onto resources that should be released.
When we do `async.then(v => ...)` the promise captures a reference to the lambda
function (the arrow function).
When we assign `updateLatestValue = null` within the context of an `unsubscribe` function,
we're changing the reference of `updateLatestValue` in the current scope to `null`.
The lambda will no longer have access to it after the assignment, effectively
preventing any further calls to the original function and allowing it to be garbage collected.

If Chrome is built with additional flags and run with `--allow-natives-syntax --track-retaining-path`,
we could use `%DebugTrackRetainingPath` to see the distance from the root for `updateLatestValue`
if it's passed directly to async.then, e.g.:

```js
%DebugTrackRetainingPath(updateLatestValue);

// Distance from root 4: 0x123456789abc <JSPromise (sfi = 0x1fbb02e2d7f1)>
```
@arturovt arturovt force-pushed the fix/common_async_pipe_leak branch from 16d6986 to 362ee4f Compare April 25, 2025 18:14
@thePunderWoman thePunderWoman added action: merge The PR is ready for merge by the caretaker target: patch This PR is targeted for the next patch release labels Apr 30, 2025
@arturovt
Copy link
Contributor Author

@thePunderWoman I would probably create another [patch] PR into 19.2.x

@mmalerba mmalerba added target: major This PR is targeted for the next major release and removed target: patch This PR is targeted for the next patch release labels Apr 30, 2025
arturovt added a commit to arturovt/angular that referenced this pull request Apr 30, 2025
@AndrewKushnir
Copy link
Contributor

I think this PR would benefit from running a TGP to make sure there are no edge cases that we need to handle, adding the TGP label.

@thePunderWoman
Copy link
Contributor

TGP

@AndrewKushnir
Copy link
Contributor
AndrewKushnir commented May 1, 2025

Caretaker note: TGP is "green" after restarting some tests.

@AndrewKushnir AndrewKushnir added target: rc This PR is targeted for the next release-candidate and removed action: global presubmit The PR is in need of a google3 global presubmit target: major This PR is targeted for the next major release labels May 1, 2025
@AndrewKushnir
Copy link
Contributor

This PR was merged into the repository by commit 1bbf750.

The changes were merged into the following branches: main, 20.0.x

AndrewKushnir pushed a commit that referenced this pull request May 1, 2025
…promise resolves (#58041)

According to the promise specification, promises are not cancellable by default.
Once a promise is created, it will either resolve or reject, and it doesn't
provide a built-in mechanism to cancel it.
There may be situations where a promise is provided, and it either resolves after
the pipe has been destroyed or never resolves at all. If the promise never
resolves — potentially due to factors beyond our control, such as third-party
libraries — this can lead to a memory leak.
When we use `async.then(updateLatestValue)`, the engine captures a reference to the
`updateLatestValue` function. This allows the promise to invoke that function when it
resolves. In this case, the promise directly captures a reference to the
`updateLatestValue` function. If the promise resolves later, it retains a reference
to the original `updateLatestValue`, meaning that even if the context where
`updateLatestValue` was defined has been destroyed, the function reference remains in memory.
This can lead to memory leaks if `updateLatestValue` is no longer needed or if it holds
onto resources that should be released.
When we do `async.then(v => ...)` the promise captures a reference to the lambda
function (the arrow function).
When we assign `updateLatestValue = null` within the context of an `unsubscribe` function,
we're changing the reference of `updateLatestValue` in the current scope to `null`.
The lambda will no longer have access to it after the assignment, effectively
preventing any further calls to the original function and allowing it to be garbage collected.

If Chrome is built with additional flags and run with `--allow-natives-syntax --track-retaining-path`,
we could use `%DebugTrackRetainingPath` to see the distance from the root for `updateLatestValue`
if it's passed directly to async.then, e.g.:

```js
%DebugTrackRetainingPath(updateLatestValue);

// Distance from root 4: 0x123456789abc <JSPromise (sfi = 0x1fbb02e2d7f1)>
```

PR Close #58041
AndrewKushnir pushed a commit that referenced this pull request May 1, 2025
@arturovt arturovt deleted the fix/common_async_pipe_leak branch May 1, 2025 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
action: merge The PR is ready for merge by the caretaker area: common Issues related to APIs in the @angular/common package target: rc This PR is targeted for the next release-candidate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants
0