pbojinov
9/3/2014 - 5:16 PM

AngularJS Chrome DevTools Snippets

AngularJS Chrome DevTools Snippets

// Adapted for Chrome DevTools Snippet. Find the full project here: https://github.com/kentcdodds/ng-stats

/*
Usage:

  showAngularStats(options);

See full project for options;
*/

(function (root, factory) {
  root.showAngularStats = factory();
}(window, function() {
  'use strict';
  var autoloadKey = 'showAngularStats_autoload';
  var current = null;
  // define the timer function to use based upon whether or not 'performance is available'
  var timerNow = window.performance
    ? function() { return performance.now(); }
    : function() { return Date.now(); };

  var lastWatchCountRun = timerNow();
  var watchCountTimeout = null;
  var lastWatchCount = getWatcherCount() || 0;
  var lastDigestLength = 0;

  var bodyEl = angular.element(document.body);
  var digestIsHijacked = false;

  var listeners = {
    digest: [],
    watchCount: [],
    digestLength: []
  };

  // Hijack $digest to time it and update data on every digest.
  function hijackDigest() {
    if (digestIsHijacked) {
      return;
    }
    digestIsHijacked = true;
    var $rootScope = bodyEl.injector().get('$rootScope');
    var scopePrototype = Object.getPrototypeOf($rootScope);
    var oldDigest = scopePrototype.$digest;
    scopePrototype.$digest = function $digest() {
      var start = timerNow();
      oldDigest.apply(this, arguments);
      var diff = (timerNow() - start);
      updateData(getWatcherCount(), diff);
    };
  }

  // check for autoload
  var autoloadOptions = sessionStorage[autoloadKey];
  if (autoloadOptions) {
    autoload(JSON.parse(autoloadOptions));
  }

  function autoload(options) {
    if (window.angular && angular.element(document.body).injector()) {
      showAngularStats(options);
    } else {
      // wait for angular to load...
      window.setTimeout(function() {
        autoload(options);
      }, 200);
    }
  }

  function showAngularStats(opts) {
    // delete the previous one
    if (current) {
      current.$el && current.$el.remove();
      current.active = false;
      current = null;
    }

    // do nothing if the argument is false
    if (opts === false) {
      sessionStorage.removeItem(autoloadKey);
      return;
    } else {
      opts = angular.extend({
        position: 'top-left',
        digestTimeThreshold: 16,
        autoload: false
      }, opts || {});
    }

    hijackDigest();

    // setup the state
    var state = current = { active:true };

    // auto-load on startup
    if (opts.autoload) {
      sessionStorage.setItem(autoloadKey,JSON.stringify(opts));
    } else {
      sessionStorage.removeItem(autoloadKey);
    }

    // general variables
    var noDigestSteps = 0;

    // add the DOM element
    state.$el = angular.element('<div><canvas></canvas><div></div></div>').css({
      position: 'fixed',
      background: 'black',
      borderBottom: '1px solid #666',
      borderRight: '1px solid #666',
      color: 'red',
      fontFamily: 'Courier',
      width: 130,
      zIndex: 9999,
      top: opts.position.indexOf('top') == -1 ? null : 0,
      bottom: opts.position.indexOf('bottom') == -1 ? null : 0,
      right: opts.position.indexOf('right') == -1 ? null : 0,
      left: opts.position.indexOf('left') == -1 ? null : 0,
      textAlign: 'right'
    });
    bodyEl.append(state.$el);
    var $text = state.$el.find('div');

    // initialize the canvas
    var graphSz = { width: 130, height: 40 };
    var cvs = state.$el.find('canvas').attr(graphSz)[0];


    // replace the digest
    listeners.digestLength.push(function(digestLength) {
      addDataToCanvas(null, digestLength);
    });

    listeners.watchCount.push(function(watchCount) {
      addDataToCanvas(watchCount);
    });

    function addDataToCanvas(watchCount, digestLength) {
      var averageDigest = digestLength || lastDigestLength;
      var color = (averageDigest > opts.digestTimeThreshold) ? 'red' : 'green';
      lastWatchCount = nullOrUndef(watchCount) ? lastWatchCount : watchCount;
      lastDigestLength = nullOrUndef(digestLength) ? lastDigestLength : digestLength;
      $text.text(lastWatchCount + ' | ' + lastDigestLength.toFixed(2)).css({color:color});

      if (!digestLength) {
        return;
      }

      // color the sliver if this is the first step
      var ctx = cvs.getContext('2d');
      if (noDigestSteps > 0) {
        noDigestSteps = 0;
        ctx.fillStyle = '#333';
        ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height);
      }

      // mark the point on the graph
      ctx.fillStyle = color;
      ctx.fillRect(graphSz.width-1,Math.max(0,graphSz.height - averageDigest),2,2);
    }

    //! Shift the canvas to the left.
    function shiftLeft() {
      if (state.active) {
        window.setTimeout(shiftLeft,250);
        var ctx = cvs.getContext('2d');
        var imageData = ctx.getImageData(1,0,graphSz.width-1,graphSz.height);
        ctx.putImageData(imageData,0,0);
        ctx.fillStyle = ((noDigestSteps++)>2) ? 'black' : '#333';
        ctx.fillRect(graphSz.width-1,0,1,graphSz.height);
      }
    }

    // start everything
    shiftLeft();
    var $rootScope = bodyEl.injector().get('$rootScope');
    if(!$rootScope.$$phase) {
      $rootScope.$digest();
    }
  }

  return showAngularStats;


  // UTILITY FUNCTIONS

  // Uses timeouts to ensure that this is only run every 300ms (it's a perf bottleneck)
  function getWatcherCount() {
    window.clearTimeout(watchCountTimeout);
    var now = timerNow();
    if (now - lastWatchCountRun > 300) {
      lastWatchCountRun = now;
      lastWatchCount = getWatcherCountForElement(angular.element(document.documentElement));
    } else {
      watchCountTimeout = window.setTimeout(function() {
        updateData(getWatcherCount());
      }, 350);
    }
    return lastWatchCount;
  }

  function getWatcherCountForElement(element) {
    var watcherCount = 0;
    if (!element || !element.length) {
      return watcherCount;
    }
    var isolateWatchers = getWatchersFromScope(element.data().$isolateScope);
    var scopeWatchers = getWatchersFromScope(element.data().$scope);
    var watchers = scopeWatchers.concat(isolateWatchers);
    watcherCount += watchers.length;
    angular.forEach(element.children(), function (childElement) {
      watcherCount += getWatcherCountForElement(angular.element(childElement));
    });
    return watcherCount;
  }

  function getWatchersFromScope(scope) {
    return scope && scope.$$watchers ? scope.$$watchers : [];
  }

  // iterate through listeners to call them with the watchCount and digestLength
  function updateData(watchCount, digestLength) {
    // update the listeners
    if (!nullOrUndef(watchCount)) {
      angular.forEach(listeners.watchCount, function(listener) {
        listener(watchCount);
      });
    }
    if (!nullOrUndef(digestLength)) {
      angular.forEach(listeners.digestLength, function(listener) {
        listener(digestLength);
      });
    }
  }

  function nullOrUndef(item) {
    return item === null || item === undefined;
  }

}));

window.showAngularStats();
// Sometimes you want to do the same thing to all scopes on a page, or find which scope has a specific property.
// Provide the starting scope (or it will use the $rootScope) and it'll iterate through all scopes below starting with the given scope

/*
Usage:

iterateScopes($scope, function(scope) {
  console.log(scope);
});

iterateScopes(function(scope) {
  console.log(scope);
});

iterateScopes(function(scope) {
  if (scope.hasOwnProperty('propOfInterest')) {
    debugger;
    // so you can do stuff with that scope
  }
});

var coolScope;
iterateScopes(function(scope) {
  if (scope.isCool) {
    coolScope = scope;
    return false; // early exit
  }
});
*/

(function(root) {
  
  function iterateScopes(current, fn) {
    if (typeof current === 'function') {
      fn = current;
      current = null;
    }
    current = current || getRootScope();
    var ret = fn(current);
    if (ret === false) {
      return ret;
    }
    return iterateChildren(current, fn);
  }
  
  function iterateSiblings(start, fn) {
    var ret;
    while (start = start.$$nextSibling) {
      ret = fn(start);
      if (ret === false) {
        break;
      }
      
      ret = iterateChildren(start, fn);
      if (ret === false) {
        break;
      }
    }
    return ret;
  }
  
  function iterateChildren(start, fn) {
    var ret;
    while(start = start.$$childHead) {
      ret = fn(start);
      if (ret === false) {
        break;
      }
      
      ret = iterateSiblings(start, fn);
      if (ret === false) {
        break;
      }
    }
    return ret;
  }
  
  function getRootScope() {
    var firstElementWithScope = document.documentElement.querySelector('.ng-scope');
    var scope = angular.element(firstElementWithScope).scope();
    return scope.$root;
  }
  
  root.iterateScopes = iterateScopes;
})(window);
// When you have an id for a scope, but not the scope itself.

/*
Usage:
var scope25 = getScopeById(25);
*/

(function(root) {
  
  function getScopeById(id) {
    var myScope = null;
    iterateScopes(function(scope) {
      if (scope.$id === id) {
        myScope = scope;
        return false;
      }
    });
    return myScope;
  }
  
  // copied from iterateScopes.js
  function iterateScopes(current, fn) {
    if (typeof current === 'function') {
      fn = current;
      current = null;
    }
    current = current || getRootScope();
    var ret = fn(current);
    if (ret === false) {
      return ret;
    }
    return iterateChildren(current, fn);
  }
  
  function iterateSiblings(start, fn) {
    var ret;
    while (start = start.$$nextSibling) {
      ret = fn(start);
      if (ret === false) {
        break;
      }
      
      ret = iterateChildren(start, fn);
      if (ret === false) {
        break;
      }
    }
    return ret;
  }
  
  function iterateChildren(start, fn) {
    var ret;
    while(start = start.$$childHead) {
      ret = fn(start);
      if (ret === false) {
        break;
      }
      
      ret = iterateSiblings(start, fn);
      if (ret === false) {
        break;
      }
    }
    return ret;
  }
  
  function getRootScope() {
    var firstElementWithScope = document.documentElement.querySelector('.ng-scope');
    var scope = angular.element(firstElementWithScope).scope();
    return scope.$root;
  }
  
  root.getScopeById = getScopeById;
})(window);
// Sometimes you have a scope ID and you have no idea what element that scope belongs to, use this to find the element.
/*
Usage:

var element = getElementByScopeId('003');
// NOTE: Some scopes don't have an element (aparently)...

*/
(function(root) {
    
  function getElementByScopeId(id) {
    var ngScopes = document.documentElement.querySelectorAll('.ng-scope');
    for (var i = 0; i < ngScopes.length; i++) {
      var element = angular.element(ngScopes.item(i));
      var scope = element.scope();
      var isolateScope = element.isolateScope();
      if ((scope && id === scope.$id) || (isolateScope && id === isolateScope.$id)) {
        return element;
      }
    }
  }
  
  root.getElementByScopeId = getElementByScopeId;
})(window);
// Sometimes you have two scopes that are supposedly related, but you don't know how far back to go before you find that relation
/*
Usage:

var closestParent = closestScopeParent(scope1, scope2);
// or
var closestParent = closestScopeParent('003', '0M4');

*/
(function(root) {
  function closestScopeParent(scope1, scope2) {
    if (typeof scope1 === 'string') {
      scope1 = getScopeById(scope1);
    }
    if (typeof scope2 === 'string') {
      scope2 = getScopeById(scope2);
    }
    var bump1 = false;
    while(scope1 !== scope2 && scope1 && scope2) {
      if (bump1) {
        scope1 = scope1.$parent;
      } else {
        scope2 = scope2.$parent;
      }
    }
    return scope1;
  }
  
  function getScopeById(id) {
    var ngScopes = document.documentElement.querySelectorAll('.ng-scope');
    for (var i = 0; i < ngScopes.length; i++) {
      var scope = angular.element(ngScopes.item(i)).scope();
      if (id === scope.$id) {
        return scope;
      }
    }
  }
  
  root.closestScopeParent = closestScopeParent;
})(window);

Angular Snippets

Some snippets for Chrome that I've made or found/modified and thought were useful.