-
Notifications
You must be signed in to change notification settings - Fork 27.5k
Small Memory Leak in $rootScope.$on #16135
Comments
The array does get resized the next time the event occurs. Is that not happening in your case? |
No, it is not. The event $translateChangeSuccess occurs when the user changes their language, which does not happen in most sessions. (Sorry for brevity -- I am on a phone right now. I found this happening in the open source Loomio application.) |
I see. With angular-translate the events are added to the The reason (I assume) that the listener-removal only sets them to null is because that array might be getting looped over at the same time, so modifying it will cause the loop to potentially skip entries. Will have to look into potential solutions and see how easy it is... |
Note that scope watchers solve this by storing the loop index on However I think this only works because you can not have nested calls to I wonder if deleting the array entry is any better then setting it to null? |
I was wondering the same. @jvilk, have you seen a meassurable impact on memory allocation due to the nulls? An (possibly easy) way out could be defragmanting the array in the |
Yeah I think doing it post-digest would work and be fairly easy. But then removing a listener would cause a post-digest event, and causing a post-digest event might mean causing a digest. Is that ok? Would it be worth it? |
I don't think |
That's true, it just might not get run until the next digest which should be fine... |
@gkalpak After about 5 navigations to different views (Dashboard -> Group Page -> Thread Page -> Group Page -> Thread Page), the array grows to 215 entries. All but 18 are Assuming a constant rate of growth, that's 43 fresh array entries per navigation. Assuming 32-bit entries in the array for object references / It's not an urgent memory leak, but it seemed warranted to bring up to all of you and confirm that it's not due to an incorrect usage of an API. |
I would expect VMs to optimize storing But I do agree that we should fix this if the fix is simple enough. One idea is using If anyone wants to take a stub at it, please do 😃 |
I think you mean 10MB? JITs don't typically optimize the storage of individual array entries AFAIK. It's either an optimization on the entire thing (e.g. 32 bits per element because it only stores integers), or nothing. Of course, I'm happy to be proven wrong! 10MB would be consistent with 8 bytes (64 bits) per NULL, plus extra room for the size class. (Since arrays in JS are resizeable, the JS engine likely increases the size of the array in powers of 2 to anticipate further elements.) (1000000 nulls * 8 bytes per null) / 1024 bytes per kilobyte / 1024 kilobytes per megabyte = ~7.62 MB |
@jvilk if you have a test up and running... what if you delete half of those array entries? I'm curious if deleting array entries is any different then assigning null... |
@jbedard Okay, I did a bit of research.
Here's the modified CodePen. Note that it takes a long time for the deletions to complete. Pull up devtools to see the console messages illustrating progress. (The in-browser "console" that CodePen provides won't update, since we're doing all of this synchronously.) tl;dr: Deleting array properties seems like it would work quite well. (As would using an object instead -- while not tested in my CodePen, it's likely the size of an object used in the same manner would be equivalent to the size of an array.) |
So just switching from |
@jvilk, oops. I didn't realize the numbers I was looking at where localized; misinterpreted the thousand separator for a comma (I said it was a quick test 😁)
@jbedard, that will totally come back and bite as at some point. I would stay away from such hacks for dealing with a minor memory leak (especially when there are simple alternatives - as discussed above). |
Yeah? I thought that was a reasonable solution, and a super simple! |
When removing listeners the listener is removed from the array but the array size is not changed until the event is fired again. If that event is never fired but listeners are added/removed then this array will continue growing. This changes the listener removal to `delete` the array entry instead of setting it to `null` in the hope of the browser deallocating the memory for the array entry. Fixes angular#16135
This seems to explain it fairly well: https://stackoverflow.com/questions/614126/why-is-array-push-sometimes-faster-than-arrayn-value/614255#614255 |
As discussed "offline", |
When removing listeners the listener is removed from the array but the array size is not changed until the event is fired again. If that event is never fired but listeners are added/removed then this array will continue growing. This changes the listener removal to `delete` the array entry instead of setting it to `null` in the hope of the browser deallocating the memory for the array entry. Fixes angular#16135
When removing listeners the listener is removed from the array but the array size is not changed until the event is fired again. If that event is never fired but listeners are added/removed then this array will continue growing. This changes the listener removal to `delete` the array entry instead of setting it to `null` in the hope of the browser deallocating the memory for the array entry. Fixes angular#16135
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
I've tried to profile the delete solution (what #16161 currently does). Basically profiling
Both also get slower as the array length increases, however... the If an event gets This makes me question this fix. It basically changes event add/remove (with no firing of the event) from a small/slow memory leak to a small but fast speed leak. WDYT? |
Naturally, |
FYI @jvilk I think everyone agreed with you and the PR should be merged soon |
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
Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see angular@e6966e0). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes angular#16135
Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see angular@e6966e0). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes angular#16135
Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see angular@e6966e0). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes angular#16135 BREAKING CHANGE: `$emit`/`$broadcast` listeners of a specific event name on a scope can no longer be recursivly invoked.
Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see angular@e6966e0). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes angular#16135 BREAKING CHANGE: `$emit`/`$broadcast` listeners of a specific event name on a scope can no longer be recursivly invoked.
Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see e6966e0). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes angular#16135 BREAKING CHANGE: `$emit`/`$broadcast` listeners of a specific event name on a scope can no longer be recursivly invoked.
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
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
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
Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see e6966e0). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes angular#16135 BREAKING CHANGE: `$emit`/`$broadcast` listeners of a specific event name on a scope can no longer be recursivly invoked.
Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see e6966e0). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes angular#16135 Closes angular#16293 BREAKING CHANGE: Recursively invoking `$emit` or `$broadcast` with the same event name is no longer supported. This will now throw a `inevt` minErr.
Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see e6966e0). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes angular#16135 Closes angular#16293 BREAKING CHANGE: Recursively invoking `$emit` or `$broadcast` with the same event name is no longer supported. This will now throw a `inevt` minErr.
FYI this is now fixed in 1.7 and 1.6 and should be in the next release of both. In 1.6 we replaced the In 1.7 we replaced the Thanks @jvilk for all the help |
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
I'm submitting a ...
Current behavior:
Registering a new handler with
$rootScope.$on
push
es the handler to the end of a list. Removing the handler onlynull
s out the array index that contains the handler. The code never resizes the array to remove thenull
entries, leading to an array that grows without bounds.Given:
'e'
such that there is always 1 registered listener on the event (preventing the handler list from being removed).$on('e', handler)
to run before all handlers are de-registered.$rootScope.$on('e'
, handler)` and responsibly de-registers the handler over the course of a session...then the handler list for
'e'
will grow by 1 element every time$rootScope.$on('e', handler)
is called. The end result is an array that looks like this:[null, null, null, ... handler, handler]
.I'm observing this happen in an app with the $translateChangeSuccess event. The array grows to about 40
null
entries over 8 navigations to different views/pages.The leak is small, but avoidable.
Expected / new behavior:
The handler list does not grow unboundedly over the course of a session. Handler positions are either re-used (as in a free list-based memory allocator), or handlers are registered in an object where handler positions are
delete
-ed once they are de-registered.Minimal reproduction of the problem with instructions:
See above description.
Angular version: Observed in 1.4.1, but code still present in 1.6.
Browser:
All
Anything else:
See above for suggestions on how to fix.
Note that I am not heavily experienced with AngularJS, but I am heavily experienced in JavaScript. Hence, I am not able to provide you with a succinct reproduction of the issue, but the description and a quick glance of the code should confirm the problem.
The text was updated successfully, but these errors were encountered: