Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory Leak: Remaining elements in cache and data_user in AngularJs

I create elements (some are SVG Tags, some are simple HTML) with ng-repeat. On changes of the data model - an object that is reset on arrival of new data - there are always elements left behind as detached DOM elements. They are held like this:

Screenshot from Chrome Developer Tools

The Elements are part of data_user which seems to be part of jquery. This problem occurs at several places on change of data. It seems that watchers are the problem, since they are keeping reference to their expression.

The elements are created e.g. like this:

.directive('svgGraphic', ['$compile', function ($compile) {
    return {
        restrict: 'E',
        replace: false,
        link: function (scope, element, attrs) {
            var svgData = scope.model.getAttribute("svgGraphic");
            var svgDomElement =  $(svgData.svg);
            scope.layers = svgData.layers;

            svgDomElement.append('<svg-layer ng-repeat="layer in layers"></svg-layer>');
            element.append($compile(svgDomElement)(scope));

            scope.$on("$destroy", function() {
                scope.$$watchers = null;
                scope.$$listeners = null;
            })
        }
    };
}])

A workaround is to manually delete watchers and listeners as you can see above - what is no good solution I think!

When new data from the server arrives, it is set like this:

$scope.model = model;
$scope.$digest();

Is it a problem to just replace the model data?

Is there any idea how it can happen that angular does not remove listeners on old elements? Angular should delete all the watchers when ng-repeat receives new data and rebuilds all elements.

like image 250
beseder Avatar asked Jul 29 '14 13:07

beseder


1 Answers

I found the same issue. I created a Watcher Class, so then with the profiler I am able to count the Watcher instances. I saw that the instances continue increasing while I navigate the app, some of the instances are retained by the data_user cache :(.

Also I fixed the delete of childScopes watcher amd added some metadata to scopes, like a list of childScope.

Here is the angular code that I changed (only the functions that I changed). I hope this help you to found the error, I am still fighting with it :)

function $RootScopeProvider() {


 this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
  function($injector, $exceptionHandler, $parse, $browser) {

 var watcherCount = 0;

function Watcher(listener, initWatchVal, get, watchExp, objectEquality, scope) {
  this.fn = isFunction(listener) ? listener : noop;
  this.last = initWatchVal;
  this.get = get;
  this.exp = watchExp;
  this.eq = !!objectEquality;
  this.scope = scope;
  this.id = watcherCount++;
}

Watcher.prototype = {
  constructor: Watcher
}

function Scope() {
  this.$id = nextUid();
  this.$$phase = this.$parent = this.$$watchers =
                 this.$$nextSibling = this.$$prevSibling =
                 this.$$childHead = this.$$childTail = null;
  this.$root = this;
  this.$$destroyed = false;
  this.$$listeners = {};
  this.$$listenerCount = {};
  this.$$isolateBindings = null;
  this.childsScopes = [];
}


Scope.prototype = {
  constructor: Scope,

  $new: function(isolate, parent) {
    var child;

    parent = parent || this;

    if (isolate) {
      child = new Scope();
      child.$root = this.$root;
    } else {
      // Only create a child scope class if somebody asks for one,
      // but cache it to allow the VM to optimize lookups.
      if (!this.$$ChildScope) {
        this.$$ChildScope = function ChildScope() {
          this.$$watchers = this.$$nextSibling =
              this.$$childHead = this.$$childTail = null;
          this.$$listeners = {};
          this.$$listenerCount = {};
          this.$id = nextUid();
          this.$$ChildScope = null;
        };
        this.$$ChildScope.prototype = this;
      }
      child = new this.$$ChildScope();
    }
    //window.scopes = window.scopes || {};
    //window.scopes[child.$id] = child;
    this.childsScopes.push(child);
    child.$parent = parent;
    child.$$prevSibling = parent.$$childTail;
    if (parent.$$childHead) {
      parent.$$childTail.$$nextSibling = child;
      parent.$$childTail = child;
    } else {
      parent.$$childHead = parent.$$childTail = child;
    }

    // When the new scope is not isolated or we inherit from `this`, and
    // the parent scope is destroyed, the property `$$destroyed` is inherited
    // prototypically. In all other cases, this property needs to be set
    // when the parent scope is destroyed.
    // The listener needs to be added after the parent is set
    if (isolate || parent != this) child.$on('$destroy', destroyChild);

    return child;

    function destroyChild() {
      child.$$destroyed = true;
      child.$$watchers = null;
      child.$$listeners = {};
      //child.$parent = null;
      child.$$nextSibling = null;
      child.$$childHead = null;
      child.$$childTail = null;
      child.$$prevSibling = null;
      child.$$listenerCount = {};
      if (child.$parent) {
        var index = child.$parent.childsScopes.indexOf(child);
        child.$parent.childsScopes.splice(index, 1);
      }

      console.log("Destroying childScope " + child.$id);

    }
  }

  $destroy: function() {
    // we can't destroy the root scope or a scope that has been already destroyed
    if (this.$$destroyed) return;
    var parent = this.$parent;
    console.log('Destroying Scope '+ this.$id);
    //delete window.scopes[this.$id];
    this.$broadcast('$destroy');
    this.$$destroyed = true;
    if (this === $rootScope) return;

    for (var eventName in this.$$listenerCount) {
      decrementListenerCount(this, this.$$listenerCount[eventName], eventName);
    }

    // sever all the references to parent scopes (after this cleanup, the current scope should
    // not be retained by any of our references and should be eligible for garbage collection)
    if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
    if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
    if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
    if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;

    // Disable listeners, watchers and apply/digest methods
    this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop;
    this.$on = this.$watch = this.$watchGroup = function() { return noop; };
    this.$$listeners = {};

    // All of the code below is bogus code that works around V8's memory leak via optimized code
    // and inline caches.
    //
    // see:
    // - https://code.google.com/p/v8/issues/detail?id=2073#c26
    // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909
    // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451

    this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
        this.$$childTail = this.$root = this.$$watchers = null;
  }


}];
}
like image 60
Facka Avatar answered Nov 15 '22 00:11

Facka