How to count total number of watches on a page?

Angularjs

Angularjs Problem Overview


Is there a way, in JavaScript, to count the number of angular watches on the entire page?

We use Batarang, but it doesn't always suit our needs. Our application is big and we're interested in using automated tests to check if the watch count goes up too much.

It would also be useful to count watches on a per-controller basis.

Edit: here is my attempt. It counts watches in everything with class ng-scope.

(function () {
    var elts = document.getElementsByClassName('ng-scope');
    var watches = [];
    var visited_ids = {};
    for (var i=0; i < elts.length; i++) {
       var scope = angular.element(elts[i]).scope();
       if (scope.$id in visited_ids) 
         continue;
       visited_ids[scope.$id] = true;
       watches.push.apply(watches, scope.$$watchers);
    }
    return watches.length;
})();

Angularjs Solutions


Solution 1 - Angularjs

(You may need to change body to html or wherever you put your ng-app)
(function () { 
    var root = angular.element(document.getElementsByTagName('body'));

    var watchers = [];

    var f = function (element) {
        angular.forEach(['$scope', '$isolateScope'], function (scopeProperty) { 
            if (element.data() && element.data().hasOwnProperty(scopeProperty)) {
                angular.forEach(element.data()[scopeProperty].$$watchers, function (watcher) {
                    watchers.push(watcher);
                });
            }
        });

        angular.forEach(element.children(), function (childElement) {
            f(angular.element(childElement));
        });
    };

    f(root);

    // Remove duplicate watchers
    var watchersWithoutDuplicates = [];
    angular.forEach(watchers, function(item) {
        if(watchersWithoutDuplicates.indexOf(item) < 0) {
             watchersWithoutDuplicates.push(item);
        }
    });

    console.log(watchersWithoutDuplicates.length);
})();
  • Thanks to erilem for pointing out this answer was missing the $isolateScope searching and the watchers potentially being duplicated in his/her answer/comment.

  • Thanks to Ben2307 for pointing out that the 'body' may need to be changed.


Original

I did the same thing except I checked the data attribute of the HTML element rather than its class. I ran yours here:

http://fluid.ie/

And got 83. I ran mine and got 121.

(function () { 
    var root = $(document.getElementsByTagName('body'));
    var watchers = [];

    var f = function (element) {
        if (element.data().hasOwnProperty('$scope')) {
            angular.forEach(element.data().$scope.$$watchers, function (watcher) {
                watchers.push(watcher);
            });
        }

        angular.forEach(element.children(), function (childElement) {
            f($(childElement));
        });
    };

    f(root);

    console.log(watchers.length);
})();

I also put this in mine:

for (var i = 0; i < watchers.length; i++) {
    for (var j = 0; j < watchers.length; j++) {
        if (i !== j && watchers[i] === watchers[j]) {
            console.log('here');
        }
    }
}

And nothing printed out, so I'm guessing that mine is better (in that it found more watches) - but I lack intimate angular knowledge to know for sure that mine isn't a proper subset of the solution set.

Solution 2 - Angularjs

I think the mentioned approaches are inaccurate since they count watchers in the same scope double. Here is my version of a bookmarklet:

https://gist.github.com/DTFagus/3966db108a578f2eb00d

It also shows some more details for analyzing watchers.

Solution 3 - Angularjs

Here is a hacky solution that I put together based on inspecting the scope structures. It "seems" to work. I'm not sure how accurate this is and it definitely depends on some internal API. I'm using angularjs 1.0.5.

    $rootScope.countWatchers = function () {
        var q = [$rootScope], watchers = 0, scope;
        while (q.length > 0) {
            scope = q.pop();
            if (scope.$$watchers) {
                watchers += scope.$$watchers.length;
            }
            if (scope.$$childHead) {
                q.push(scope.$$childHead);
            }
            if (scope.$$nextSibling) {
                q.push(scope.$$nextSibling);
            }
        }
        window.console.log(watchers);
    };

Solution 4 - Angularjs

There is a new chrome plugin that automatically shows the current total watchers and the last change (+/-) at any time in your app... it's pure awesome.

https://chrome.google.com/webstore/detail/angular-watchers/nlmjblobloedpmkmmckeehnbfalnjnjk

Solution 5 - Angularjs

Minor improvement for Words Like Jared's answer.

(function () {
    var root = $(document.getElementsByTagName('body'));
    var watchers = 0;

    var f = function (element) {
        if (element.data().hasOwnProperty('$scope')) {
            watchers += (element.data().$scope.$$watchers || []).length;
        }

        angular.forEach(element.children(), function (childElement) {
            f($(childElement));
        });
    };

    f(root);

    return watchers;
})();

Solution 6 - Angularjs

As I was recently struggling with high number of watchers in my application, too, I found out a great library, called ng-stats - https://github.com/kentcdodds/ng-stats . It has minimal setup and gives you the number of watchers on the current page + digest cycle length. It could also project a small real-time graph.

Solution 7 - Angularjs

In AngularJS 1.3.2, a countWatchers method was added to the ngMock module:

/**

  • @ngdoc method
  • @name $rootScope.Scope#$countWatchers
  • @module ngMock
  • @description
  • Counts all the watchers of direct and indirect child scopes of the current scope.
  • The watchers of the current scope are included in the count and so are all the watchers of
  • isolate child scopes.
  • @returns {number} Total number of watchers. */

function countWatchers() { var root = angular.element(document).injector().get('$rootScope'); var count = root.$$watchers ? root.$$watchers.length : 0; // include the current scope var pendingChildHeads = [root.$$childHead]; var currentScope;

while (pendingChildHeads.length) { currentScope = pendingChildHeads.shift();

while (currentScope) 
  {
  count += currentScope.$$watchers ? currentScope.$$watchers.length : 0;
  pendingChildHeads.push(currentScope.$$childHead);
  currentScope = currentScope.$$nextSibling;
  }
}

return count; }

References

Solution 8 - Angularjs

I took the code below directly from the $digest function itself. Of course, you probably need to update the application element selector (document.body) at the bottom.

(function ($rootScope) {
    var watchers, length, target, next, count = 0;

    var current = target = $rootScope;

    do {
        if ((watchers = current.$$watchers)) {
            count += watchers.length;
        }

        if (!(next = (current.$$childHead ||
                (current !== target && current.$$nextSibling)))) {
            while (current !== target && !(next = current.$$nextSibling)) {
                current = current.$parent;
            }
        }
    } while ((current = next));

    return count;
})(angular.element(document.body).injector().get('$rootScope'));

Solution 9 - Angularjs

This is the functions I use:

/**
 * @fileoverview This script provides a window.countWatchers function that
 * the number of Angular watchers in the page.
 *
 * You can do `countWatchers()` in a console to know the current number of
 * watchers.
 *
 * To display the number of watchers every 5 seconds in the console:
 *
 * setInterval(function(){console.log(countWatchers())}, 5000);
 */
(function () {

  var root = angular.element(document.getElementsByTagName('body'));

  var countWatchers_ = function(element, scopes, count) {
    var scope;
    scope = element.data().$scope;
    if (scope && !(scope.$id in scopes)) {
      scopes[scope.$id] = true;
      if (scope.$$watchers) {
        count += scope.$$watchers.length;
      }
    }
    scope = element.data().$isolateScope;
    if (scope && !(scope.$id in scopes)) {
      scopes[scope.$id] = true;
      if (scope.$$watchers) {
        count += scope.$$watchers.length;
      }
    }
    angular.forEach(element.children(), function (child) {
      count = countWatchers_(angular.element(child), scopes, count);
    });
    return count;
  };

  window.countWatchers = function() {
    return countWatchers_(root, {}, 0);
  };

})();

This function uses a hash not to count the same scope multiple times.

Solution 10 - Angularjs

There is a recursive function published by Lars Eidnes' blog at http://larseidnes.com/2014/11/05/angularjs-the-bad-parts/ to collect the total number watchers. I compare the result using the function posted here and the one his posted in his blog, which has generated slightly higher number. I cannot tell which one is more accurate. Just added here as a across reference.

function getScopes(root) {
    var scopes = [];
    function traverse(scope) {
        scopes.push(scope);
        if (scope.$$nextSibling)
            traverse(scope.$$nextSibling);
        if (scope.$$childHead)
            traverse(scope.$$childHead);
    }
    traverse(root);
    return scopes;
}
var rootScope = angular.element(document.querySelectorAll("[ng-app]")).scope();
var scopes = getScopes(rootScope);
var watcherLists = scopes.map(function(s) { return s.$$watchers; });
_.uniq(_.flatten(watcherLists)).length;

NOTE: you might need change "ng-app" to "data-ng-app" for your Angular app.

Solution 11 - Angularjs

Plantian's answer is faster: https://stackoverflow.com/a/18539624/258482

Here is a function which I hand-wrote. I didn't think about using recursive functions, but this is what I did instead. It might be leaner, I don't know.

var logScope; //put this somewhere in a global piece of code

Then put this inside your highest controller ( if you use a global controller ).

$scope.$on('logScope', function () { 
    var target = $scope.$parent, current = target, next;
    var count = 0;
    var count1 = 0;
    var checks = {};
    while(count1 < 10000){ //to prevent infinite loops, just in case
        count1++;
        if(current.$$watchers)
            count += current.$$watchers.length;

        //This if...else is also to prevent infinite loops. 
        //The while loop could be set to true.
        if(!checks[current.$id]) checks[current.$id] = true;
        else { console.error('bad', current.$id, current); break; }
        if(current.$$childHead) 
            current = current.$$childHead;
        else if(current.$$nextSibling)
            current = current.$$nextSibling;
        else if(current.$parent) {
            while(!current.$$nextSibling && current.$parent) current = current.$parent;
            if(current.$$nextSibling) current = current.$$nextSibling;
            else break;
        } else break;
    }
    //sort of by accident, count1 contains the number of scopes.
    console.log('watchers', count, count1);
    console.log('globalCtrl', $scope); 
   });

logScope = function () {
    $scope.$broadcast('logScope');
};

And finally a bookmarket:

javascript:logScope();

Solution 12 - Angularjs

A bit late to this question, but I use this

angular.element(document.querySelector('[data-ng-app]')).scope().$$watchersCount

just make sure you use the correct querySelector.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionty.View Question on Stackoverflow
Solution 1 - AngularjsJesus is LordView Answer on Stackoverflow
Solution 2 - AngularjsDTFagusView Answer on Stackoverflow
Solution 3 - AngularjsIan WilsonView Answer on Stackoverflow
Solution 4 - AngularjsNick SteeleView Answer on Stackoverflow
Solution 5 - AngularjsMiraageView Answer on Stackoverflow
Solution 6 - AngularjsboyomarinovView Answer on Stackoverflow
Solution 7 - AngularjsPaul SweatteView Answer on Stackoverflow
Solution 8 - AngularjsGray FoxView Answer on Stackoverflow
Solution 9 - AngularjserilemView Answer on Stackoverflow
Solution 10 - Angularjszd333View Answer on Stackoverflow
Solution 11 - AngularjsArlen BeilerView Answer on Stackoverflow
Solution 12 - AngularjsmakrigiaView Answer on Stackoverflow