144

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;
})();
ty.
  • 10,924
  • 9
  • 52
  • 71
  • Per controller is easy. Every `$scope` has a $$watchers array with the number of watchers on that controller (well, if you have some ng-repeat or something that creates another scope, that doesn't work that good). But I think that there is no way to see all the watches in the entire app. – Jesus Rodriguez Aug 29 '13 at 01:27

12 Answers12

220

(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.

Community
  • 1
  • 1
Jesus is Lord
  • 14,971
  • 11
  • 66
  • 97
  • 2
    I have been trying to do something similar, and found this snippet very helpful. One thing I that I think is worth clarifying is that 'root' should be set to whatever element has the 'ng-app' attribute, as that is where the $rootScope is kept. In my app its on the 'html' tag. Running your script as is missed the $watchers in $rootScope in my app. – aamiri Dec 12 '13 at 15:00
  • I found one problem with the above code: if a child element has it's own scope, but now watchers, wouldn't $scope.$$watchers return it's parent scope's watchers? I think you should add before the push something like this (I'm using lodash): if (!_.contains(watchers,watcher)){ watchers.push(watcher); } – Ben2307 Dec 16 '13 at 09:16
  • 2
    This gets all the watchers that are attached to DOM elements. If you remove a DOM element, is the cleanup of the associated `$scope` and `$$watcher` automatic, or is there a performance hit they incur? – SimplGy Mar 21 '14 at 19:44
  • Does it take into account the new one-time binding feature? Does your count skip this kind of binding? – systempuntoout Oct 20 '14 at 22:24
  • @systempuntoout I've heard of [this](https://github.com/Pasvaz/bindonce) - Angular has something similar, now? – Jesus is Lord Oct 20 '14 at 23:00
  • This function is not correct, because a scope can be attached to many elements. This means the function may report more watchers than there really are. – erilem Nov 28 '14 at 17:52
  • 10
    Just a heads up: If you use `$compileProvider.debugInfoEnabled(false);` in your project, this snippet will count zero watchers. – Michael Klöpzig Feb 16 '15 at 12:41
  • This function does not count the watchers on scopes not attached to the DOM. Compare to the function from plantian. On a single page application this can count many more watchers... – iRaS Jun 05 '15 at 06:49
  • What is the difference between this and traversing the $rootScope? – Arlen Beiler Jun 25 '15 at 14:36
  • Nice script, but weird thing.. Why did I get 0 watchers at http://mgcrea.github.io/angular-strap/? I tried with `angular watchers` plugin on chrome also return 0. – user1995781 Jul 04 '15 at 07:53
  • I'm trying to filter 1 time watchers too. I found many watchers have "noop" (no op function performing no work) as their "exp", "fn", and "get" functions. I found exp had "oneTimeWatch" for its function in some cases and am using that to filter 1 time bindings. Anyone know what these different functions are used for in the watchers? exp (expression?) fn, and get? Just for testing, I added this to the end of the script: var filtered = watchersWithoutDuplicates.filter(function(obj) {return !((String(obj.exp)).indexOf("oneTimeWatch") > 0)}); console.log(filtered); – DragonMoon Sep 03 '15 at 00:57
  • @DragonMoon You should probably make a new question, You'll get a better response there. – Jesus is Lord Sep 03 '15 at 02:14
  • One point of confusion for me: Why are you using `angular.element(document.getElementsByTagName('body'))` and `$(document.getElementsByTagName('body'))`? Aren't those equivalent to `angular.element('body')` and `$('body')`, respectively? – Justin Morgan - On strike Jun 23 '16 at 19:20
  • @JustinMorgan: The `$` was the original answer, when I knew a lot less about JavaScript, AngularJS and jQuery. I don't think `$` is available unless you have jQuery as a dependency. [Out-of-the-box AngularJS uses jqLite](https://docs.angularjs.org/api/ng/function/angular.element), which doesn't have all the features as jQuery. When I updated the answer per suggestions of people who commented, I took out the jQuery/`$` dependency by changing to `angular.element`. Does that answer your question? – Jesus is Lord Jun 23 '16 at 19:44
  • @JustinMorgan: If you were asking about the `getElementsByTagName` usage, I didn't know that it could be omitted. You probably know more than me about referencing DOM elements. Feel free to edit the answer if you think it will be clearer; I welcome all improvements! – Jesus is Lord Jun 23 '16 at 19:45
  • is it just me, or is it bad coding to call `element.data()` 6 times inside `f`? – gilad905 Jan 19 '18 at 10:30
16

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.

DTFagus
  • 161
  • 1
  • 2
13

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);
    };
Ian Wilson
  • 6,223
  • 1
  • 16
  • 24
  • This is similar to my original solution (see edit history). I moved to a different approach because I think walking the scope hierarchy would miss isolate scopes. – ty. Aug 30 '13 at 21:18
  • 3
    If I create an isolated scope with `$rootScope.$new(true)` or `$scope.$new(true)`, where `$scope` is for the controller, then walking the hierarchy still finds that scope. I think it means that the prototypes are not connected instead of that the scope is not in the hierarchy. – Ian Wilson Aug 31 '13 at 00:15
  • Yes, *all* scopes descend from the $rootScope, only inheritence is "isolated" in isolated scopes. Isolated scopes are often used in directives - here you don't want app-variables from parents to interfere. – markmarijnissen Feb 06 '14 at 16:03
  • If you are trying to detect scopes that you create but don't clean up, this method is superior. In my case, crawling the DOM always shows the same number of scopes but those which are not DOM-attached are multiplying in shadowland. – SimplGy Mar 21 '14 at 23:07
10

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

Nick Steele
  • 7,419
  • 4
  • 36
  • 33
9

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;
})();
Community
  • 1
  • 1
Miraage
  • 3,334
  • 3
  • 26
  • 43
9

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.

boyomarinov
  • 615
  • 4
  • 12
8

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

Community
  • 1
  • 1
Paul Sweatte
  • 24,148
  • 7
  • 127
  • 265
  • 1
    Thanks! I changed `angular.element(document)` to `angular.element('[ng-app]')` and put it in a bookmarklet with an alert: `alert('found ' + countWatchers() + ' watchers');` – undefined Jul 23 '15 at 17:27
4

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'));
Gray Fox
  • 673
  • 1
  • 8
  • 21
1

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.

erilem
  • 2,634
  • 21
  • 21
  • I think `element.data()` can sometimes be undefined or something (at least on a 1.0.5 application that I got an error on when I ran this snipped and tried to call `countWatchers`). Just FYI. – Jesus is Lord Nov 30 '14 at 02:54
1

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.

zd333
  • 201
  • 2
  • 3
1

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();
Community
  • 1
  • 1
Arlen Beiler
  • 15,336
  • 34
  • 92
  • 135
0

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.

makrigia
  • 400
  • 2
  • 8