How to count total number of watches on a page?
Asked Answered
C

12

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;
})();
Crippen answered 28/8, 2013 at 23:24 Comment(1)
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.Ferrand
C
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.

Conchaconchie answered 30/8, 2013 at 6:55 Comment(16)
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.Kneehole
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); }Ignoramus
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?Macaluso
Does it take into account the new one-time binding feature? Does your count skip this kind of binding?Frenulum
@Frenulum I've heard of this - Angular has something similar, now?Conchaconchie
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.Ruhl
Just a heads up: If you use $compileProvider.debugInfoEnabled(false); in your project, this snippet will count zero watchers.Nicolanicolai
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...Divide
What is the difference between this and traversing the $rootScope?Suberin
Nice script, but weird thing.. Why did I get 0 watchers at mgcrea.github.io/angular-strap? I tried with angular watchers plugin on chrome also return 0.Novena
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);Devonne
@Devonne You should probably make a new question, You'll get a better response there.Conchaconchie
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?Denary
@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, 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?Conchaconchie
@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!Conchaconchie
is it just me, or is it bad coding to call element.data() 6 times inside f?Chrissa
P
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.

Pelagic answered 13/8, 2014 at 7:16 Comment(0)
C
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);
    };
Cottage answered 30/8, 2013 at 18:47 Comment(4)
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.Crippen
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.Cottage
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.Headphone
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.Macaluso
G
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

Glassco answered 3/7, 2015 at 9:0 Comment(1)
this is indeed much apt extObstetrician
E
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;
})();
Efficacious answered 5/5, 2014 at 10:45 Comment(1)
Because you're not using jQuery selectors, you can use angular.element() instead of $()Qualitative
I
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.

Insufficient answered 19/11, 2014 at 10:9 Comment(0)
D
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

Dover answered 22/6, 2015 at 18:50 Comment(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');Solifluction
L
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'));
Lefton answered 27/5, 2015 at 8:47 Comment(0)
R
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.

Ruhl answered 28/11, 2014 at 17:57 Comment(1)
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.Conchaconchie
R
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.

Reubenreuchlin answered 15/1, 2015 at 22:48 Comment(0)
S
1

Plantian's answer is faster: https://mcmap.net/q/158764/-how-to-count-total-number-of-watches-on-a-page

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();
Suberin answered 25/6, 2015 at 13:57 Comment(0)
C
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.

Cybil answered 28/2, 2019 at 10:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.