-
Notifications
You must be signed in to change notification settings - Fork 27.5k
fix($rootScope): fix potential memory leak when removing scope listeners #16161
Conversation
The errors look weird. An incompatibility between yarn and dgeni-packages? Strange because I don't think we changed anything. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A minor suggestion, but LGTM 👍
@@ -1180,7 +1180,10 @@ function $RootScopeProvider() { | |||
return function() { | |||
var indexOfListener = namedListeners.indexOf(listener); | |||
if (indexOfListener !== -1) { | |||
namedListeners[indexOfListener] = null; | |||
// Use delete in the hope of the browser deallocating the memory for the array entry, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...in the hope of the browser deallocating...
Why wouldn't it? 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it is implementation dependent, not part of any spec etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there should be a more descriptive comment here, e.g. "depending on browser implementations, delete should deallocate the memory"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if it "should" though. Is there anything that actually says it should? I think browsers (some? all?) do this just to be more efficient. I kind of like the "in the hope of" so I'm having trouble finding a better description...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we ever find a conclusive answer to the actual implementation in specific browsers? Or is this based on benchmarks? Either info could be listed here and then maybe a note that we assume other browsers do it as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on benchmarks Chrome and FF seem to both use a more memory efficient array implementation for sparse arrays (which I noted here). But I'm not sure about all browsers, or the details of when Chrome and FF do it, and I'm assuming there is no actual standard for this.
test/ng/rootScopeSpec.js
Outdated
$rootScope.$broadcast('abc'); | ||
expect(listener2).toHaveBeenCalled(); | ||
expect(listener3).toHaveBeenCalled(); | ||
})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could run a second round (it this and the subsequent tests) to make sure the listener will be called correctly and nothing will break after the initial run. E.g.:
listener2.calls.reset();
listener3.calls.reset();
$rootScope.$broadcast('abc');
expect(listener2).toHaveBeenCalled();
expect(listener3).toHaveBeenCalled();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added this to all 3 tests
@jbedard can you please rebase this? The tests currently don't run because of a Travis problem at the branch point. |
8490e74
to
396566f
Compare
Rebased. Adjusted the tests a bit like @gkalpak suggested. |
Another good read about array storage/optimizations: https://www.html5rocks.com/en/tutorials/speed/v8/#toc-topic-numbers Again v8 specific, but this confirms what we've been talking about (and what @jvilk measured) with array vs dictionary versions of an array. Using |
test/ng/rootScopeSpec.js
Outdated
})); | ||
|
||
|
||
it('should call next listener when removing current', inject(function($rootScope) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized there are two similar tests to this one where the current listener gets removed for $emit and $broadcast (e6966e0) under describe('$emit'
. Maybe those two should move here to replace this one? Or just keep this extra one?
Actually... I came up with an alternative that might be better, essentially doing the same as $$digestWatchIndex. Basically making the See jbedard@68247ee - WDYT? |
You mean iterate over $$listeners and modify while you iterate over them? That sounds like an acceptable tradeoff. |
I think it would be recursive |
Since I don't really use events in AngularJs, I can't say if there are valid use cases for recursive emission and broadcasting. It's probably a BC for someone ... |
We decided to land this PR as is for 1.6.0 (to avoid any breaking changes and still avoid the memory leak). |
SGTM |
Don't forget to change the branch target to v1.6.x for this one |
I've rebased this onto v1.6.x. Also updated the first commit to include the changes from #16293. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still LGTM 😃
7adfbcc
to
b3023b6
Compare
When removing listeners they are removed from the array but the array size is not changed until the event is fired again. If the event is never fired but listeners are added/removed then the array will continue growing. By changing the listener removal to `delete` the array entry instead of setting it to `null` browsers can potentially deallocate the memory for the entry. Fixes angular#16135 Closes angular#16161
When removing listeners they are removed from the array but the array size is not changed until the event is fired again. If the event is never fired but listeners are added/removed then the array will continue growing. By changing the listener removal to `delete` the array entry instead of setting it to `null` browsers can potentially deallocate the memory for the entry. Fixes angular#16135 Closes angular#16161
When removing listeners they are removed from the array but the array size is not changed until the event is fired again. If the event is never fired but listeners are added/removed then the array will continue growing. By changing the listener removal to `delete` the array entry instead of setting it to `null` browsers can potentially deallocate the memory for the entry. Fixes angular#16135 Closes angular#16161
I've merged this to v1.6.x and cherry-picked the first commit (extra tests, not specifically for this bug) into master. |
When removing listeners they are removed from the array but the array size is not changed until the event is fired again. If the event is never fired but listeners are added/removed then the array will continue growing. By changing the listener removal to `delete` the array entry instead of setting it to `null` browsers can potentially deallocate the memory for the entry. Fixes angular#16135 Closes angular#16161
When removing listeners they are removed from the array but the array size is not changed until the event is fired again. If the event is never fired but listeners are added/removed then the array will continue growing. By changing the listener removal to `delete` the array entry instead of setting it to `null` browsers can potentially deallocate the memory for the entry. Fixes angular#16135 Closes angular#16161
When removing listeners they are removed from the array but the array size is not changed until the event is fired again. If the event is never fired but listeners are added/removed then the array will continue growing. By changing the listener removal to `delete` the array entry instead of setting it to `null` browsers can potentially deallocate the memory for the entry. Fixes angular#16135 Closes angular#16161
When removing listeners they are removed from the array but the array size is not changed until the event is fired again. If the event is never fired but listeners are added/removed then the array will continue growing. By changing the listener removal to `delete` the array entry instead of setting it to `null` browsers can potentially deallocate the memory for the entry. Fixes #16135 Closes #16161
This attempts to fix #16135, but is assuming that browsers will use some type of memory efficient array implementation for sparse arrays (which they appear to do, see 1, 2, 3). In the worst case this
should have no changemay cause some extra swapping of array implementations and/or not fix the bug, in the best case it will be more memory efficient in this edge case.Also added a couple tests that show why we don't simply use
splice
.Thanks @jvilk for investigating this
delete
solution.