Reputation: 1854
We are having problem with memory leak in our AngularJS app while switching between different sections of the app. We have been struggling to find the root cause.
There is a main controller in our app. This main controller contains another sub controller. This sub controller has two sections. At any time, one of the two sections is shown. First section has some html with a button. When clicked on this button, it toggles to second section. We use jquery to toggle between 2 different sections (I know we should not mix jquery & angularjs, but it's long story :-( )
Now second section has ui-view. Another view with controller is loaded in this ui-view. This view contains a button (to switch to first section) and another child ui-view. The child ui-view is loaded with controller. It uses ng-repeat to populate an array in UI.
When we switched to second section from first, we call $state.go to reload parent & child ui-view in second section.
The memory leak happens everytime second section is loaded using $state.go.
Below is sample code similar to the app code. The actual app is much more complex. But the memory leak scenario can be simulated with below code.
Here is Plunker Link
<body ng-app="testApp">
<div ng-controller="MainCtrl">
<div ng-controller="SubCtrl">
<div id="Section1">
<h1> {{title}}</h1>
<button ng-click="SwitchToSection2()">Switch to Section 2</button>
</div>
<div id="Section2" style="display:none;">
<div ui-view="section2view"></div>
</div>
</div>
</div>
var app = angular.module('testApp', ['ui.router'])
app.config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state("section2", {
views: {
'section2view': { templateUrl: 'section2-template.html' }
}
})
.state("section2.child", {
templateUrl: 'section2child-template.html',
params: { p1: "test" }
})
}]);
app.controller('MainCtrl', function($scope){
});
app.controller('SubCtrl', function($scope, $state){
$scope.title = "This is section 1";
$scope.SwitchToSection2 = function(){
$("#Section2").show();
$("#Section1").hide();
$state.go('section2.child', { p1: "test"}, { location: false, reload: true });
}
});
app.controller('Section2Controller', function($scope, $state){
$scope.SwitchToSection1 = function(){
$("#Section1").show();
$("#Section2").hide();
}
});
app.controller('Section2ChildCtrl', function($scope, $state){
$scope.itemArray = [];
for (var i = 0; i < 1000;i++) {
$scope.itemArray.push({
prop1: "Property 1",
prop2: "Property 2",
prop3: "Property 3",
});
}
});
Memory Leak
I am using Chrome Developer Timeline tools to identify the leak. Below screenshot is taken after switching around 10 times between 2 sections. Every time I go to section 2, the memory size increases. The memory is not GC. You can also see the node count increases gradually. I also used 3 snapshot profile technique to find detached nodes and what is retaining it. But it doesn't give any useful information except that angularjs is referencing the dom.
It would be great help if someone can point to right direction. Sorry for the long post. Thank you.
Upvotes: 1
Views: 1510
Reputation: 9292
I can't reproduce the leak with the plunker and the instructions you gave. I've tried several times and I can't see any leaks happening.
I've changed to this: http://plnkr.co/edit/TtrbV9929H2ymaboFTNJ?p=preview which is this code:
var app = angular.module('testApp', ['ui.router']);
app.config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state("section2", {
views: {
'section2view': { templateUrl: 'section2-template.html' }
}
})
.state("section2.child", {
templateUrl: 'section2child-template.html',
params: { p1: "test" }
})
}]);
app.controller('MainCtrl', function($scope){
console.log('Main controller instantiated');
$scope.$on('$destroy', function() {
console.log('Main controller has been destroyed');
});
});
app.controller('SubCtrl', function($scope, $state){
console.log('Sub controller instantiated');
$scope.$on('$destroy', function() {
console.log('Sub controller has been destroyed');
});
$scope.title = "This is section 1";
$scope.SwitchToSection2 = function(){
$("#Section2").show();
$("#Section1").hide();
$state.go('section2.child', { p1: "test"}, { location: false, reload: true });
}
});
app.controller('Section2Controller', function($scope, $state){
console.log('Section2 controller instantiated');
$scope.$on('$destroy', function() {
console.log('Section2 controller has been destroyed');
});
$scope.SwitchToSection1 = function(){
$("#Section1").show();
$("#Section2").hide();
}
});
app.controller('Section2ChildCtrl', function($scope, $state){
$scope.itemArray = [];
console.log($scope.itemArray.length);
for (var i = 0; i < 1000; i++) {
$scope.itemArray.push({
prop1: "Property 1",
prop2: "Property 2",
prop3: "Property 3"
});
}
console.log('Section2 Child Ctrl controller instantiated');
$scope.$on('$destroy', function() {
console.log('Section2 Child Ctrl controller has been destroyed');
});
});
So if you switch you can see that the controllers are being created and destroyed correctly. Late, but correctly.
I've used recorded allocations to see if the memory usage increased overtime. I repeat changes between states like a crazy maniac and the memory usage doesn't increase over time:
Now if we see the timelines:
You can see the same thing. Garbage collection eventually kicks in and there's probably a bit of overhead cause of so many nodes being created and destroyed but the browser recovers itself.
Now, if the case is different in your application I can only think is because the Section2 controller and subcontrollers don't die upon switching to Section 1 and because you may be storing things from those controllers up into the scope chain which maybe the thing leaking. But this is only something I can only imagine as I haven't seen the code.
For what is worth, I've tested this in latest Chrome (46.0.2490.86) on OSX 10.11.1 result may vary on another browser and/or OS but if that's the case the culprit could be the browser itself. Aside from mixing jQuery and Angular, which is only ugly but not bad itself, I can't see anything that could be making the leak happening with the provided code.
Upvotes: 1
Reputation: 5626
You say it's a long story that you're using jQuery but you don't elaborate...it doesn't seem to me like it is necessary to use at all in the scenario you've given. I'd suggest updating your code to use angular features and seeing if you have the same issue. The gist of it is below, or check out this updated Plunker based off your code.
Put two states and a method to control them in your MainCtrl:
app.controller('MainCtrl', function($scope){
$scope.sectionOneShown = true;
$scope.sectionTwoShown = false;
$scope.changeSection = function() {
console.log('changing');
$scope.sectionOneShown = !$scope.sectionOneShown;
$scope.sectionTwoShown = !$scope.sectionTwoShown;
};
});
Update your HTML templates accordingly:
<div ng-controller="MainCtrl">
<div ng-controller="SubCtrl">
<div id="Section1" ng-show="sectionOneShown">
<h1> {{title}}</h1>
<button ng-click="switchView()">Switch to Section 2</button>
</div>
<div id="Section2" ng-show="sectionTwoShown">
<div ui-view="section2view"></div>
</div>
</div>
</div>
Call the same method from the child controller to swap views:
<div>
<div style="background-color:grey;padding:10px;" >
<h1>This is section 2</h1>
<button ng-click="switchView()" style="margin-left:200px;">Switch To Section 1</button>
<div ui-view></div>
</div>
</div>
Upvotes: -2