fix(forms): remove animationstart listener on component destroy to prevent memory leak#68170
Open
arturovt wants to merge 1 commit intoangular:mainfrom
Open
fix(forms): remove animationstart listener on component destroy to prevent memory leak#68170arturovt wants to merge 1 commit intoangular:mainfrom
arturovt wants to merge 1 commit intoangular:mainfrom
Conversation
Contributor
Author
|
@thePunderWoman not sure if CI failing is related. |
…event memory leak
The `watchValidity` method in `AnimationInputValidityMonitor` was registering
an anonymous arrow function via `addEventListener` with no corresponding
`removeEventListener` call.
In V8, each closure is represented as a `JSFunction` holding a strong pointer
to a heap-allocated `Context` object containing captured variables
(`VariableLocation::CONTEXT` slots, decided at parse time by
`Scope::MustAllocateInContext`). In Blink, DOM event listeners are stored in
the element's `EventTargetData::event_listener_map` as `JSEventListener`
wrappers backed by a `v8::Persistent<JSFunction>` handle — a strong cross-heap
reference that keeps the function alive as long as the element is alive.
Because the callback passed to `watchValidity` closes over the calling
component/directive (which itself holds a reference back to the element), this
produced a cross-heap reference cycle:
```
HTMLInputElement (Blink/Oilpan)
└── EventTargetData → JSEventListener → v8::Persistent<JSFunction>
└── Context → callback closure
└── component → HTMLInputElement ← cycle
```
Neither V8's nor Blink's GC could independently break this cycle because it
crosses the V8/Oilpan heap boundary. The element was therefore never collected
after being removed from the DOM.
The fix stores the listener in a named local variable and registers its removal
via `DestroyRef.onDestroy`, tying cleanup to the lifetime of the component that
owns the element. This ensures `removeEventListener` is called with the exact
same `JSFunction` reference, causing Blink to drop the `v8::Persistent` handle
and allowing both the function and the element to become GC-eligible.
fe82db1 to
bdd008b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
watchValiditymethod inAnimationInputValidityMonitorwas registering an anonymous arrow function viaaddEventListenerwith no correspondingremoveEventListenercall.In V8, each closure is represented as a
JSFunctionholding a strong pointer to a heap-allocatedContextobject containing captured variables (VariableLocation::CONTEXTslots, decided at parse time byScope::MustAllocateInContext). In Blink, DOM event listeners are stored in the element'sEventTargetData::event_listener_mapasJSEventListenerwrappers backed by av8::Persistent<JSFunction>handle — a strong cross-heap reference that keeps the function alive as long as the element is alive.Because the callback passed to
watchValiditycloses over the calling component/directive (which itself holds a reference back to the element), this produced a cross-heap reference cycle:Neither V8's nor Blink's GC could independently break this cycle because it crosses the V8/Oilpan heap boundary. The element was therefore never collected after being removed from the DOM.
The fix stores the listener in a named local variable and registers its removal via
DestroyRef.onDestroy, tying cleanup to the lifetime of the component that owns the element. This ensuresremoveEventListeneris called with the exact sameJSFunctionreference, causing Blink to drop thev8::Persistenthandle and allowing both the function and the element to become GC-eligible.