Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to catch memory leaks in an Angular application?

I have a webapp written in AngularJS which basically polls an API to two endpoints. So, every minute it polls to see if there is anything new.

I discovered that it has a small memory leak and I've done my best to find it but I'm not able to do it. In the process I've managed to reduce the memory usage of my app, which is great.

Without doing anything else, every poll you can see a spike in the memory usage (that's normal) and then it should drop, but it's always increasing. I've changed the cleaning of the arrays from [] to array.length = 0 and I think I'm sure that references don't persist so it shouldn't be retaining any of this.

I've also tried this: https://github.com/angular/angular.js/issues/1522

But without any luck...

So, this is a comparison between two heaps:

Memory heap

Most of the leak seems to come from (array) which, if I open, are the arrays returned by the parsing of the API call but I'm sure they're not being stored:

This is basically the structure:

poll: function(service) {   var self = this;   log('Polling for %s', service);    this[service].get().then(function(response) {     if (!response) {       return;     }      var interval = response.headers ? (parseInt(response.headers('X-Poll-Interval'), 10) || 60) : 60;      services[service].timeout = setTimeout(function(){       $rootScope.$apply(function(){         self.poll(service);       });     }, interval * 1000);      services[service].lastRead = new Date();     $rootScope.$broadcast('api.'+service, response.data);   }); } 

Basically, let's say I have a sellings service so, that would be the value of the service variable.

Then, in the main view:

$scope.$on('api.sellings', function(event, data) {   $scope.sellings.length = 0;   $scope.sellings = data; }); 

And the view does have an ngRepeat which renders this as needed. I spent a lot of time trying to figure this out by myself and I couldn't. I know this is a hard issue but, do anyone have any idea on how to track this down?

Edit 1 - Adding Promise showcase:

This is makeRequest which is the function used by the two services:

return $http(options).then(function(response) {     if (response.data.message) {       log('api.error', response.data);     }      if (response.data.message == 'Server Error') {           return $q.reject();     }      if (response.data.message == 'Bad credentials' || response.data.message == 'Maximum number of login attempts exceeded') {       $rootScope.$broadcast('api.unauthorized');       return $q.reject();     }      return response;     }, function(response) {     if (response.status == 401 || response.status == 403) {       $rootScope.$broadcast('api.unauthorized');     } }); 

If I comment out the $scope.$on('api.sellings') part, the leakage still exists but drops to 1%.

PS: I'm using latest Angular version to date

Edit 2 - Opening (array) tree in an image

enter image description here

It's everything like that so it's quite useless imho :(

Also, here are 4 heap reports so you can play yourself:

https://www.dropbox.com/s/ys3fxyewgdanw5c/Heap.zip

Edit 3 - In response to @zeroflagL

Editing the directive, didn't have any impact on the leak although the closure part seems to be better since it's not showing jQuery cache things?

No more leakage?

The directive now looks like this

var destroy = function(){   if (cls){     stopObserving();     cls.destroy();     cls = null;   } };  el.on('$destroy', destroy); scope.$on('$destroy', destroy); 

To me, it seems that what's happening is on the (array) part. There is also 3 new heaps in between pollings.

like image 258
Antonio Laguna Avatar asked Dec 18 '13 09:12

Antonio Laguna


People also ask

How do you check if there is a memory leak in angular?

The first thing to do is to open the Chrome Dev Tools, open the panel on the right and click on More Tools > Performance Monitor. The memory of our application is displayed in the blue graph.

What causes memory leaks in angular?

Here are a few things that might cause memory leaks in an Angular app: Missing unsubscription, which will retain the components in the memory. Missing unregistration for DOM event Listeners: such as a listener to a scroll event, a listener to forms onChange event, etc. unclosed WebSocket connections when they are ...


2 Answers

And the answer is cache.

Heap Snapshot analysis

I don't know what it is, but this thing grows. It seems to be related to jQuery. Maybe it's the jQuery element cache. Do you by any chance apply a jQuery plugin on one or more elements after every service call?

Update

The problem is that HTML elements are added, processed with jQuery (e.g. via the popbox plugin), but either never removed at all or not removed with jQuery. To process in this case means stuff like adding event handlers. The entries in the cache object (whatever it is for) do only get removed if jQuery knows that the elements have been removed. That is the elements have to be removed with jQuery.

Update 2

It's not quite clear why these entries in the cache haven't been removed, as angular is supposed to use jQuery, when it's included. But they have been added through the plugin mentioned in the comments and contained event handlers and data. AFAIK Antonio has changed the plugin code to unbind the event handlers and remove the data in the plugin's destroy() method. That eventually removed the memory leak.

like image 114
a better oliver Avatar answered Oct 16 '22 12:10

a better oliver


The standard browser way to fix memory leaks is to refresh the page. And JavaScript garbage collection is kind of lazy, likely banking on this. And since Angular is typically a SPA, the browser never gets a chance to refresh.

But we have 1 thing to our advantage: Javascript is primarily a top-down hierarchial language. Instead of searching for memory leaks from the bottom up, we may be able to clear them from the top down.

Therefore I came up with this solution, which works, but may or may not be 100% effective depending on your app.

The Home Page

The typical Angular app home page consists of some Controller and ng-view. Like this:

<div ng-controller="MainController as vm"> <div id="main-content-app" ng-view></div> </div>

The Controller

Then to "refresh" the app in the controller, which would be MainController from the code above, we redundantly call jQuery's .empty() and Angular's .empty() just to make sure that any cross-library references are cleared.

function refreshApp() { var host = document.getElementById('main-content-app'); if(host) {     var mainDiv = $("#main-content-app");     mainDiv.empty();     angular.element(host).empty(); } } 

and to call the above before routing begins, simulating a page refresh:

$rootScope.$on('$routeChangeStart', function (event, next, current) {     refreshApp(); } ); 

Result

This is kind of a hacky method for "refreshing the browser type behavior", clearing the DOM and hopefully any leaks. Hope it helps.

like image 39
jmbmage Avatar answered Oct 16 '22 12:10

jmbmage