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);
Some snippets for Chrome that I've made or found/modified and thought were useful.