finished refactor of instrumentation

test-unit-sauce
Brian Ford 12 years ago
parent d8aa313eba
commit bd61693243

@ -39,32 +39,8 @@ panelApp.controller('PerfCtrl', function PerfCtrl($scope, appContext, filesystem
appContext.inspect(this.val.id); appContext.inspect(this.val.id);
}; };
var updateHistogram = function () {
var info = appContext.getHistogram();
if (!info) {
return;
}
var total = 0;
info.forEach(function (elt) {
total += elt.time;
});
var i, elt, his;
for (i = 0; (i < $scope.histogram.length && i < info.length); i++) {
elt = info[i];
his = $scope.histogram[i];
his.time = elt.time.toPrecision(3);
his.percent = (100 * elt.time / total).toPrecision(3);
}
for ( ; i < info.length; i++) {
elt = info[i];
elt.time = elt.time.toPrecision(3);
elt.percent = (100 * elt.time / total).toPrecision(3);
$scope.histogram.push(elt);
}
$scope.histogram.length = info.length;
};
var updateTree = function () { var updateTree = function () {
$scope.histogram = appContext.getHistogram();
var roots = appContext.getListOfRoots(); var roots = appContext.getListOfRoots();
if (!roots) { if (!roots) {
return; return;
@ -86,5 +62,4 @@ panelApp.controller('PerfCtrl', function PerfCtrl($scope, appContext, filesystem
} }
}; };
appContext.watchPoll(updateTree); appContext.watchPoll(updateTree);
appContext.watchPoll(updateHistogram);
}); });

@ -0,0 +1,6 @@
// returns the number's first 4 decimals
panelApp.filter('precision', function () {
return function (input, output) {
return input.toPrecision(4);
};
});

@ -49,16 +49,15 @@ var inject = function () {
return; return;
} }
// Helpers
// =======
// polyfill for performance.now on older webkit // polyfill for performance.now on older webkit
if (!performance.now) { if (!performance.now) {
performance.now = performance.webkitNow; performance.now = performance.webkitNow;
} }
// Helpers
// =======
// Based on cycle.js // Based on cycle.js
// 2011-08-24
// https://github.com/douglascrockford/JSON-js/blob/master/cycle.js // https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
// Make a deep copy of an object or array, assuring that there is at most // Make a deep copy of an object or array, assuring that there is at most
@ -66,7 +65,7 @@ var inject = function () {
// duplicate references (which might be forming cycles) are replaced with // duplicate references (which might be forming cycles) are replaced with
// an object of the form // an object of the form
// {$ref: PATH} // {$ref: PATH}
// where the PATH is a JSONPath string that locates the first occurance. // where the PATH is a JSONPath string that locates the first occurrence.
var decycle = function (object) { var decycle = function (object) {
var objects = [], // Keep a reference to each unique object or array var objects = [], // Keep a reference to each unique object or array
paths = []; // Keep the path to each unique object or array paths = []; // Keep the path to each unique object or array
@ -112,9 +111,8 @@ var inject = function () {
// End // End
// === // ===
// Instrumentation // given a scope object, return an object with deep clones
// --------------- // of the models exposed on that scope
var getScopeLocals = function (scope) { var getScopeLocals = function (scope) {
var scopeLocals = {}, prop; var scopeLocals = {}, prop;
for (prop in scope) { for (prop in scope) {
@ -125,16 +123,73 @@ var inject = function () {
return scopeLocals; return scopeLocals;
}; };
//var bootstrap = window.angular.bootstrap; // helper to extract dependencies from function arguments
var debug = window.__ngDebug = { // not all versions of AngularJS expose annotate
watchers: {}, // map of scopes --> watchers var annotate = angular.injector().annotate;
if (!annotate) {
annotate = (function () {
watchPerf: {}, // maps of watch/apply exp/fns to perf data var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
applyPerf: {}, var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(.+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
scopes: {}, // map of scope.$ids --> model objects // TODO: should I keep these assertions?
rootScopes: {}, // map of $ids --> refs to root scopes function assertArg(arg, name, reason) {
rootScopeDirty: {}, if (!arg) {
throw new Error("Argument '" + (name || '?') + "' is " + (reason || "required"));
}
return arg;
}
function assertArgFn(arg, name, acceptArrayAnnotation) {
if (acceptArrayAnnotation && angular.isArray(arg)) {
arg = arg[arg.length - 1];
}
assertArg(angular.isFunction(arg), name, 'not a function, got ' +
(arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg));
return arg;
}
return function (fn) {
var $inject,
fnText,
argDecl,
last;
if (typeof fn == 'function') {
if (!($inject = fn.$inject)) {
$inject = [];
fnText = fn.toString().replace(STRIP_COMMENTS, '');
argDecl = fnText.match(FN_ARGS);
argDecl[1].split(FN_ARG_SPLIT).forEach(function(arg) {
arg.replace(FN_ARG, function(all, underscore, name) {
$inject.push(name);
});
});
fn.$inject = $inject;
}
} else if (angular.isArray(fn)) {
last = fn.length - 1;
assertArgFn(fn[last], 'fn');
$inject = fn.slice(0, last);
} else {
assertArgFn(fn, 'fn', true);
}
return $inject;
};
}());
}
// Public API
// ==========
var api = window.__ngDebug = {
getDeps: function () {
return debug.deps;
},
getRootScopeIds: function () { getRootScopeIds: function () {
var ids = []; var ids = [];
@ -143,6 +198,7 @@ var inject = function () {
}); });
return ids; return ids;
}, },
getScopeTree: function (id) { getScopeTree: function (id) {
if (debug.rootScopeDirty[id] === false) { if (debug.rootScopeDirty[id] === false) {
return; return;
@ -174,6 +230,20 @@ var inject = function () {
return tree; return tree;
}, },
getWatchPerf: function () {
var changes = [];
angular.forEach(debug.watchPerf, function (info, name) {
if (info.time > 0) {
changes.push({
name: name,
time: info.time
});
info.time = 0;
}
});
return changes;
},
getWatchTree: function (id) { getWatchTree: function (id) {
var traverse = function (sc) { var traverse = function (sc) {
var tree = { var tree = {
@ -196,68 +266,37 @@ var inject = function () {
var tree = traverse(root); var tree = traverse(root);
return tree; return tree;
}, }
deps: []
}; };
var annotate = angular.injector().annotate;
// not all versions of AngularJS expose annotate // Private state
if (!annotate) { // =============
annotate = (function () {
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; //var bootstrap = window.angular.bootstrap;
var FN_ARG_SPLIT = /,/; var debug = {
var FN_ARG = /^\s*(_?)(.+?)\1\s*$/; // map of scopes --> watcher function name strings
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; watchers: {},
// TODO: should I keep these assertions? // maps of watch/apply exp/fns to perf data
function assertArg(arg, name, reason) { watchPerf: {},
if (!arg) { applyPerf: {},
throw new Error("Argument '" + (name || '?') + "' is " + (reason || "required"));
}
return arg;
}
function assertArgFn(arg, name, acceptArrayAnnotation) {
if (acceptArrayAnnotation && angular.isArray(arg)) {
arg = arg[arg.length - 1];
}
assertArg(angular.isFunction(arg), name, 'not a function, got ' + // map of scope.$ids --> model objects
(arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); scopes: {},
return arg;
}
return function (fn) { // map of $ids --> refs to root scopes
var $inject, rootScopes: {},
fnText,
argDecl,
last;
if (typeof fn == 'function') { // map of $ids --> bools
if (!($inject = fn.$inject)) { rootScopeDirty: {},
$inject = [];
fnText = fn.toString().replace(STRIP_COMMENTS, ''); deps: []
argDecl = fnText.match(FN_ARGS); };
argDecl[1].split(FN_ARG_SPLIT).forEach(function(arg) {
arg.replace(FN_ARG, function(all, underscore, name) {
$inject.push(name); // Instrumentation
}); // ===============
});
fn.$inject = $inject;
}
} else if (angular.isArray(fn)) {
last = fn.length - 1;
assertArgFn(fn[last], 'fn');
$inject = fn.slice(0, last);
} else {
assertArgFn(fn, 'fn', true);
}
return $inject;
};
}());
}
var ng = angular.module('ng'); var ng = angular.module('ng');
ng.config(function ($provide) { ng.config(function ($provide) {
@ -327,7 +366,6 @@ var inject = function () {
// patch watchExpression // patch watchExpression
// --------------------- // ---------------------
var w = watchExpression; var w = watchExpression;
if (typeof w === 'function') { if (typeof w === 'function') {
watchExpression = function () { watchExpression = function () {
@ -378,9 +416,8 @@ var inject = function () {
}; };
// patch destroy // patch $destroy
// ------------- // --------------
var _destroy = $delegate.__proto__.$destroy; var _destroy = $delegate.__proto__.$destroy;
$delegate.__proto__.$destroy = function () { $delegate.__proto__.$destroy = function () {
if (debug.watchers[this.$id]) { if (debug.watchers[this.$id]) {
@ -392,6 +429,8 @@ var inject = function () {
return _destroy.apply(this, arguments); return _destroy.apply(this, arguments);
}; };
// patch $new
// ----------
var _new = $delegate.__proto__.$new; var _new = $delegate.__proto__.$new;
$delegate.__proto__.$new = function () { $delegate.__proto__.$new = function () {
@ -410,8 +449,8 @@ var inject = function () {
return ret; return ret;
}; };
// patch apply // patch $apply
// ----------- // ------------
var _apply = $delegate.__proto__.$apply; var _apply = $delegate.__proto__.$apply;
$delegate.__proto__.$apply = function (fn) { $delegate.__proto__.$apply = function (fn) {
var start = performance.now(); var start = performance.now();

@ -1,4 +1,4 @@
// Service for doing stuff in the context of the application being debugged // Service for running code in the context of the application being debugged
panelApp.factory('appContext', function (chromeExtension) { panelApp.factory('appContext', function (chromeExtension) {
// Private vars // Private vars
@ -10,6 +10,7 @@ panelApp.factory('appContext', function (chromeExtension) {
_pollListeners = [], _pollListeners = [],
_pollInterval = 500; _pollInterval = 500;
// TODO: make this private and have it automatically poll? // TODO: make this private and have it automatically poll?
var getDebugData = function (callback) { var getDebugData = function (callback) {
chromeExtension.eval(function (window) { chromeExtension.eval(function (window) {
@ -17,15 +18,15 @@ panelApp.factory('appContext', function (chromeExtension) {
return {}; return {};
} }
return { return {
deps: window.__ngDebug.deps, deps: window.__ngDebug.getDeps(),
applyPerf: window.__ngDebug.applyPerf, watchPerf: window.__ngDebug.getWatchPerf(),
watchPerf: window.__ngDebug.watchPerf,
roots: window.__ngDebug.getRootScopeIds() roots: window.__ngDebug.getRootScopeIds()
}; };
}, },
function (data) { function (data) {
if (data) { if (data) {
_debugCache = data; _debugCache = data;
_incomingHistogramData = data.watchPerf;
} }
_pollListeners.forEach(function (fn) { _pollListeners.forEach(function (fn) {
fn(); fn();
@ -37,10 +38,41 @@ panelApp.factory('appContext', function (chromeExtension) {
}; };
getDebugData(); getDebugData();
var _histogramCache = [];
var _incomingHistogramData = [];
var _watchNameToPerf = {};
var _totalCache = 0;
var processHistogram = function () {
if (_incomingHistogramData.length === 0) {
return;
}
_incomingHistogramData.forEach(function (info) {
_totalCache += info.time;
if (_watchNameToPerf[info.name]) {
_watchNameToPerf[info.name].time += info.time;
} else {
_watchNameToPerf[info.name] = info;
_histogramCache.push(info);
}
});
// recalculate all percentages
_histogramCache.forEach(function (item) {
item.percent = (100 * item.time / _totalCache).toPrecision(3);
});
// clear the incoming queue
_incomingHistogramData = [];
};
// Public API // Public API
// ========== // ==========
return { return {
// Fix selection of scope // TODO: Fix selection of scope
// https://github.com/angular/angularjs-batarang/issues/6 // https://github.com/angular/angularjs-batarang/issues/6
executeOnScope: function(scopeId, fn, args, cb) { executeOnScope: function(scopeId, fn, args, cb) {
if (typeof args === 'function') { if (typeof args === 'function') {
@ -69,7 +101,8 @@ panelApp.factory('appContext', function (chromeExtension) {
// ------- // -------
getHistogram: function () { getHistogram: function () {
return _debugCache.watchPerf; processHistogram();
return _histogramCache;
}, },
getListOfRoots: function () { getListOfRoots: function () {
@ -139,7 +172,7 @@ panelApp.factory('appContext', function (chromeExtension) {
clearHistogram: function (cb) { clearHistogram: function (cb) {
chromeExtension.eval(function (window) { chromeExtension.eval(function (window) {
window.__ngDebug.watchExp = {}; window.__ngDebug.watchPerf = {};
}, cb); }, cb);
}, },

@ -23,6 +23,7 @@
<script src="js/directives/watcherTree.js"></script> <script src="js/directives/watcherTree.js"></script>
<script src="js/filters/first.js"></script> <script src="js/filters/first.js"></script>
<script src="js/filters/precision.js"></script>
<script src="js/filters/sortByTime.js"></script> <script src="js/filters/sortByTime.js"></script>
<script src="js/services/appContext.js"></script> <script src="js/services/appContext.js"></script>

@ -29,7 +29,7 @@
<div class="well well-top" style="height: 400px; overflow-y: auto;"> <div class="well well-top" style="height: 400px; overflow-y: auto;">
<div ng-repeat="watch in histogram|sortByTime:min:max"> <div ng-repeat="watch in histogram|sortByTime:min:max">
<span style="font-family: monospace;">{{watch.name | first}} </span> <span style="font-family: monospace;">{{watch.name | first}} </span>
<span> | {{watch.percent}}% | {{watch.time}}ms</span> <span> | {{watch.percent}}% | {{watch.time | precision}}ms</span>
<div class="progress"> <div class="progress">
<div ng-style="{width: (watch.percent) + '%'}" class= "bar"> <div ng-style="{width: (watch.percent) + '%'}" class= "bar">
</div> </div>
@ -48,4 +48,4 @@
</div> </div>
</div> </div>
</div> </div>