Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

fix($rootScope): fix potential memory leak when removing scope listeners #16161

Merged
merged 2 commits into from
Oct 28, 2017

Conversation

jbedard
Copy link
Contributor

@jbedard jbedard commented Aug 10, 2017

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 change may 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.

@Narretz
Copy link
Contributor

Narretz commented Aug 10, 2017

The errors look weird. An incompatibility between yarn and dgeni-packages? Strange because I don't think we changed anything.

Copy link
Member

@gkalpak gkalpak left a 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,
Copy link
Member

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? 😁

Copy link
Contributor Author

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.

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 there should be a more descriptive comment here, e.g. "depending on browser implementations, delete should deallocate the memory"

Copy link
Contributor Author

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...

Copy link
Contributor

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.

Copy link
Contributor Author

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.

$rootScope.$broadcast('abc');
expect(listener2).toHaveBeenCalled();
expect(listener3).toHaveBeenCalled();
}));
Copy link
Member

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();

Copy link
Contributor Author

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

@Narretz
Copy link
Contributor

Narretz commented Sep 6, 2017

@jbedard can you please rebase this? The tests currently don't run because of a Travis problem at the branch point.

@jbedard
Copy link
Contributor Author

jbedard commented Sep 10, 2017

Rebased. Adjusted the tests a bit like @gkalpak suggested.

@jbedard
Copy link
Contributor Author

jbedard commented Sep 17, 2017

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 delete could cause a switch to the dictionary implementation which forces memory reallocation/copying, but then (for sparse arrays) it will use less memory.

@jbedard jbedard mentioned this pull request Oct 8, 2017
3 tasks
}));


it('should call next listener when removing current', inject(function($rootScope) {
Copy link
Contributor Author

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?

@jbedard
Copy link
Contributor Author

jbedard commented Oct 15, 2017

Actually... I came up with an alternative that might be better, essentially doing the same as $$digestWatchIndex. Basically making the $$listeners iteration index global (per event type per scope) so it can be adjusted, if necessary, when a listener is removed. The only constraint is this means you can not iterator over $$listeners recursively which I think is already a restriction due to the prevoius array cleanup that essentially had the same restriction.

See jbedard@68247ee - WDYT?

@Narretz
Copy link
Contributor

Narretz commented Oct 16, 2017

The only constraint is this means you can not iterator over $$listeners recursively

You mean iterate over $$listeners and modify while you iterate over them? That sounds like an acceptable tradeoff.

@jbedard
Copy link
Contributor Author

jbedard commented Oct 16, 2017

I think it would be recursive $emit or $broadcast calls of the same event name. I'm pretty sure this was already broken in some cases, but this (storing the iteration index on the array) would break it in all cases.

@Narretz
Copy link
Contributor

Narretz commented Oct 17, 2017

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 ...

@gkalpak
Copy link
Member

gkalpak commented Oct 17, 2017

We decided to land this PR as is for 1.6.0 (to avoid any breaking changes and still avoid the memory leak).
For 1.7.0, we will implement something like jbedard/angular.js@68247ee (which avoids making iterations slower as the array grows in length) with an explicit check to prevent nested listener iterations and a breaking change notice.

@Narretz
Copy link
Contributor

Narretz commented Oct 17, 2017

SGTM

@Narretz
Copy link
Contributor

Narretz commented Oct 23, 2017

Don't forget to change the branch target to v1.6.x for this one

@jbedard jbedard changed the base branch from master to v1.6.x October 26, 2017 04:54
@jbedard
Copy link
Contributor Author

jbedard commented Oct 26, 2017

I've rebased this onto v1.6.x. Also updated the first commit to include the changes from #16293.

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

Still LGTM 😃

@jbedard jbedard force-pushed the 16135 branch 2 times, most recently from 7adfbcc to b3023b6 Compare October 27, 2017 16:04
jbedard added a commit to jbedard/angular.js that referenced this pull request Oct 27, 2017
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
jbedard added a commit to jbedard/angular.js that referenced this pull request Oct 28, 2017
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
@jbedard
Copy link
Contributor Author

jbedard commented Oct 28, 2017

I've merged this to v1.6.x and cherry-picked the first commit (extra tests, not specifically for this bug) into master.

jbedard added a commit to jbedard/angular.js that referenced this pull request Dec 5, 2017
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
jbedard added a commit to jbedard/angular.js that referenced this pull request Dec 10, 2017
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
jbedard added a commit to jbedard/angular.js that referenced this pull request Dec 13, 2017
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
Narretz pushed a commit that referenced this pull request Dec 13, 2017
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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants