From 87b0055c80f40589c5bcf3765e59e872bcfae119 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 1 May 2015 14:47:29 +0100 Subject: [PATCH] fix($rootScope): stop IE9 memory leak when destroying scopes Ensure that all child scopes are completely disconnected when a parent is destroyed. Closes #10706 Closes #11786 --- src/ng/rootScope.js | 36 ++++++++++++++++++++++++++---------- test/ng/rootScopeSpec.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 952f4b92d484..ca83cf6fdf85 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -101,6 +101,29 @@ function $RootScopeProvider() { $event.currentScope.$$destroyed = true; } + function cleanUpScope($scope) { + + if (msie === 9) { + // There is a memory leak in IE9 if all child scopes are not disconnected + // completely when a scope is destroyed. So this code will recurse up through + // all this scopes children + // + // See issue https://github.com/angular/angular.js/issues/10706 + $scope.$$childHead && cleanUpScope($scope.$$childHead); + $scope.$$nextSibling && cleanUpScope($scope.$$nextSibling); + } + + // The code below works around IE9 and V8's memory leaks + // + // 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 + + $scope.$parent = $scope.$$nextSibling = $scope.$$prevSibling = $scope.$$childHead = + $scope.$$childTail = $scope.$root = $scope.$$watchers = null; + } + /** * @ngdoc type * @name $rootScope.Scope @@ -897,16 +920,9 @@ function $RootScopeProvider() { 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; + // Disconnect the next sibling to prevent `cleanUpScope` destroying those too + this.$$nextSibling = null; + cleanUpScope(this); }, /** diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 8bdc6b4c50f0..e704608db990 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -1211,6 +1211,34 @@ describe('Scope', function() { expect(child.parentModel).toBe('parent'); expect(child.childModel).toBe('child'); })); + + + if (msie === 9) { + // See issue https://github.com/angular/angular.js/issues/10706 + it('should completely disconnect all child scopes on IE9', inject(function($rootScope) { + var parent = $rootScope.$new(), + child1 = parent.$new(), + child2 = parent.$new(), + grandChild = child1.$new(); + parent.$destroy(); + + $rootScope.$digest(); + + expect(isDisconnected(parent)).toBe(true); + expect(isDisconnected(child1)).toBe(true); + expect(isDisconnected(child2)).toBe(true); + expect(isDisconnected(grandChild)).toBe(true); + + function isDisconnected($scope) { + return $scope.$$nextSibling === null && + $scope.$$prevSibling === null && + $scope.$$childHead === null && + $scope.$$childTail === null && + $scope.$root === null && + $scope.$$watchers === null; + } + })); + } });