-
Notifications
You must be signed in to change notification settings - Fork 27.5k
TRANSCLUDE MEMORY LEAK: Memory leak with transclusion, ng:repeat and jQuery #9095
Comments
@mzgol - any thoughts on this? |
Hi, additional info, this doesn't happen only with jquery's latest (2.x). We have this issue currently in our app which uses jQuery 1.10.2 |
Just to be clear this is only related to 1.2.x and not 1.3.x? |
Interestingly, I have just tried the same example with the latest 1.3.x of angular and the problem is actually worse, as it happens WITH or WITHOUT jquery being loaded. But still only in the case of ng:repeat being used. |
Hmm that is not good. |
Here is the jsbin with latest master: http://jsbin.com/lisodiwuluzo/1/edit?html,js,output |
So the scopes seem to go up by 7 when the ng-if is true and down by only 1 when it is false. |
Problem with ng-if - ng-if combo too: http://jsbin.com/zimuvo/1/edit |
I have a feeling that some of the recent performance changes have a role to play here, such as b5f7970#diff-a732922b631efed1b9f33a24082ae0dbL1036 |
Changing https://github.com/angular/angular.js/blob/master/src/ng/compile.js#L1179 from if (scopeCreated && !elementTransclusion) { back to if (scopeCreated) { Gets us to only one leaked scope on each toggle (rather than 6) |
Current state of mastermissing destroy for all the ngRepeat children and the ngTransclude/ngRepeat scope destroy
With element transclusion perf fix removedstill missing the ngTransclude/ngRepeat scope destroy
|
Here is a simple plnkr that generates this info: http://plnkr.co/edit/KDHv95XjI6ymNVetYiJq?p=preview |
So if we get ngTransclude to destroy its scope when its element is destroyed and similarly get ngRepeat to destroy its child scopes when it is destroyed then all is good. See http://plnkr.co/edit/KDHv95XjI6ymNVetYiJq?p=preview |
Here is a failing test: iit('should not leak memory when a directive removes the element containing the transclude directive', function() {
module(function() {
directive('toggle', function() {
return {
transclude: true,
template:
'<section class="toggle">' +
'<div ng:if="t">' +
'<div ng:transclude></div>' +
'</div>' +
'</section>'
};
});
});
inject(function($compile, $rootScope) {
function getAllScopes() {
return [$rootScope].concat(
getChildScopes($rootScope)
);
function getChildScopes(scope) {
var children = [];
if (!scope.$$childHead) { return children; }
var childScope = scope.$$childHead;
do {
children.push(childScope);
children = children.concat(getChildScopes(childScope));
} while ((childScope = childScope.$$nextSibling));
return children;
}
}
$rootScope.messages = ['message 0'];
element = $compile(
'<div toggle>' +
' <div ng:repeat="message in messages">{{ message }}</div>' +
'</div>'
)($rootScope);
$rootScope.$apply('t = true');
expect(element.text()).toContain('message 0');
expect(getAllScopes().length).toEqual(4);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('message 0');
expect(getAllScopes().length).toEqual(1);
$rootScope.$apply('t = true');
expect(element.text()).toContain('message 0');
expect(getAllScopes().length).toEqual(4);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('message 0');
expect(getAllScopes().length).toEqual(1);
});
});
}); |
and possibly this is enough to fix it: var ngTranscludeDirective = ngDirective({
restrict: 'EAC',
link: function($scope, $element, $attrs, controller, $transclude) {
if (!$transclude) {
throw minErr('ngTransclude')('orphan',
'Illegal use of ngTransclude directive in the template! ' +
'No parent directive that requires a transclusion found. ' +
'Element: {0}',
startingTag($element));
}
$transclude(function(clone, scope) {
$element.empty();
$element.append(clone);
$element.on('$destroy', function() {
scope.$destroy();
});
});
}
}); |
It looks like we don't need to fix ngRepeat |
this leak should have been prevented by this code in compile.js:
Which generally works, except when repeaters are being transcluded as the root element, because then the Since this jqLite behavior changed in 1.3, this is the reason why in the original report the issue was not present with jqLite in 1.2.x. The fix proposed by Pete is correct for ngTransclude, and it makes the I can't think of a proper fix for the manual transclusion case. Maybe we should just say that if someone is doing $transclude they are responsible for destroying the transcluded scope. Document this requirement in docs and breaking commit message and call it done. |
I think I have a more general fix in #9129. This uses the fact that we do indeed know what the future parent element will be and so we can attach the scope destroy handler to this rather than a potential comment. @IgorMinar - is this likely to break other things? I don't see why the transclusion scope should not be destroyed when the container is destroyed. |
@tbosch and I paired on this today. We realised that neither of the following fixes are sufficient:
We decided that the only real solution is to say that if people use In this case the memory leak was actually that ngTransclude was not taking responsibility for destroying its transcluded scope when its own scope was destroyed. Adding this in fixes the issue. See http://plnkr.co/edit/1FIvPdbtF7pR4NcLvesP?p=preview. In addition this ought to allow to remove the |
@tobias and @IgorMinar - I just awoke with a new idea: Our problem is that the transclude scope is detached from the scope tree at the point it is used.
What we really need is for transclude scopes really to be children of their containing element's scope while maintaining their prototypical inheritance from the place where they were taken. This can easily be done by adding a new parent parameter to
And now when the ngIf scope is destroyed the ngRepeat scope is destroyed automatically with no need for the $destroy listener, in either the Directives like ngIf and ngRepeat who stamp out copies of their transcluded content are still responsible for removing their cloned DOM elements and destroying their cloned scopes but in cases where you just use ngTransclude you would get no memory leaks when the element containing the ngTransclude directive is removed by some directive outside. Clearly this would be a breaking change for people who are relying upon |
Of course this change would break the test at https://github.com/angular/angular.js/blob/master/test/ng/compileSpec.js#L4565 But it is arguable that if we are adding and removing elements in the DOM then we should be creating a scope around it and then destroying that scope. |
Yes, I agree. Make it so :-) |
@petebacondarwin are you planning on adding a new parameter to the transclude function for the parent scope? |
Yes. I'll have a pr ready in a couple of hours.
|
… prevent memory leaks Transcluded scopes are now connected to the scope in which they are created via their `$parent` property. This means that they will be automatically destroyed when their "containing" scope is destroyed, without having to resort to listening for a `$destroy` event on various DOM elements or other scopes. Previously, transclude scope not only inherited prototypically from the scope from which they were transcluded but they were also still owned by that "outer" scope. This meant that there were scenarios where the "real" container scope/element was destroyed but the transclude scope was not, leading to memory leaks. The original strategy for dealing with this was to attach a `$destroy` event handler to the DOM elements in the transcluded content, so that if the elements were removed from the DOM then their associated transcluded scope would be destroyed. This didn't work for transclude contents that didn't contain any elements - most importantly in the case of the transclude content containing an element transclude directive at its root, since the compiler swaps out this element for a comment before a destroy handler could be attached. BREAKING CHANGE: `$transclude` functions no longer attach `$destroy` event handlers to the transcluded content, and so the associated transclude scope will not automatically be destroyed if you remove a transcluded element from the DOM using direct DOM manipulation such as the jquery `remove()` method. If you want to explicitly remove DOM elements inside your directive that have been compiled, and so potentially contain child (and transcluded) scopes, then it is your responsibility to get hold of the scope and destroy it at the same time. The suggested approach is to create a new child scope of your own around any DOM elements that you wish to manipulate in this way and destroy those scopes if you remove their contents - any child scopes will then be destroyed and cleaned up automatically. Note that all the built-in directives that manipulate the DOM (ngIf, ngRepeat, ngSwitch, etc) already follow this best practice, so if you only use these for manipulating the DOM then you do not have to worry about this change. Closes angular#9095
Hi there,
I believe I have found a memory leak in a very specific use case it seems as it only happens when transcluding ng:repeat generated elements and ONLY if jQuery is loaded.
Here is a use case: http://jsbin.com/nirubi/11/edit?html,js,output
You can try everything first with only jQlite. Follow the number of active scopes and active watches.
Then uncomment the jQuery script tag and try again, you'll see that the number of active scopes and watches creeps up in the case of ng:repeat and ONLY when jQuery is being loaded.
Let me know if I'm doing something wrong or if it's a genuine bug.
Thanks!
The text was updated successfully, but these errors were encountered: