diff --git a/js/controllers/PerfCtrl.js b/js/controllers/PerfCtrl.js index 782dc1e..796157f 100644 --- a/js/controllers/PerfCtrl.js +++ b/js/controllers/PerfCtrl.js @@ -39,32 +39,8 @@ panelApp.controller('PerfCtrl', function PerfCtrl($scope, appContext, filesystem 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 () { + $scope.histogram = appContext.getHistogram(); var roots = appContext.getListOfRoots(); if (!roots) { return; @@ -86,5 +62,4 @@ panelApp.controller('PerfCtrl', function PerfCtrl($scope, appContext, filesystem } }; appContext.watchPoll(updateTree); - appContext.watchPoll(updateHistogram); }); diff --git a/js/filters/precision.js b/js/filters/precision.js new file mode 100644 index 0000000..57af9d2 --- /dev/null +++ b/js/filters/precision.js @@ -0,0 +1,6 @@ +// returns the number's first 4 decimals +panelApp.filter('precision', function () { + return function (input, output) { + return input.toPrecision(4); + }; +}); diff --git a/js/inject/debug.js b/js/inject/debug.js index a697e1e..abda6a6 100644 --- a/js/inject/debug.js +++ b/js/inject/debug.js @@ -49,16 +49,15 @@ var inject = function () { return; } + // Helpers + // ======= + // polyfill for performance.now on older webkit if (!performance.now) { performance.now = performance.webkitNow; } - // Helpers - // ======= - // Based on cycle.js - // 2011-08-24 // 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 @@ -66,7 +65,7 @@ var inject = function () { // duplicate references (which might be forming cycles) are replaced with // an object of the form // {$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 objects = [], // Keep a reference to each unique object or array paths = []; // Keep the path to each unique object or array @@ -112,9 +111,8 @@ var inject = function () { // End // === - // Instrumentation - // --------------- - + // given a scope object, return an object with deep clones + // of the models exposed on that scope var getScopeLocals = function (scope) { var scopeLocals = {}, prop; for (prop in scope) { @@ -125,16 +123,73 @@ var inject = function () { return scopeLocals; }; - //var bootstrap = window.angular.bootstrap; - var debug = window.__ngDebug = { - watchers: {}, // map of scopes --> watchers + // helper to extract dependencies from function arguments + // not all versions of AngularJS expose annotate + var annotate = angular.injector().annotate; + if (!annotate) { + annotate = (function () { - watchPerf: {}, // maps of watch/apply exp/fns to perf data - applyPerf: {}, + var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; + var FN_ARG_SPLIT = /,/; + var FN_ARG = /^\s*(_?)(.+?)\1\s*$/; + var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; - scopes: {}, // map of scope.$ids --> model objects - rootScopes: {}, // map of $ids --> refs to root scopes - rootScopeDirty: {}, + // TODO: should I keep these assertions? + function assertArg(arg, name, reason) { + 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 () { var ids = []; @@ -143,6 +198,7 @@ var inject = function () { }); return ids; }, + getScopeTree: function (id) { if (debug.rootScopeDirty[id] === false) { return; @@ -174,6 +230,20 @@ var inject = function () { 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) { var traverse = function (sc) { var tree = { @@ -196,68 +266,37 @@ var inject = function () { var tree = traverse(root); return tree; - }, - - deps: [] + } }; - var annotate = angular.injector().annotate; - // not all versions of AngularJS expose annotate - if (!annotate) { - annotate = (function () { + // Private state + // ============= - var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; - var FN_ARG_SPLIT = /,/; - var FN_ARG = /^\s*(_?)(.+?)\1\s*$/; - var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; + //var bootstrap = window.angular.bootstrap; + var debug = { + // map of scopes --> watcher function name strings + watchers: {}, - // TODO: should I keep these assertions? - function assertArg(arg, name, reason) { - 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]; - } + // maps of watch/apply exp/fns to perf data + watchPerf: {}, + applyPerf: {}, - assertArg(angular.isFunction(arg), name, 'not a function, got ' + - (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); - return arg; - } + // map of scope.$ids --> model objects + scopes: {}, - return function (fn) { - var $inject, - fnText, - argDecl, - last; + // map of $ids --> refs to root scopes + rootScopes: {}, - 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; - }; - }()); - } + // map of $ids --> bools + rootScopeDirty: {}, + + deps: [] + }; + + + // Instrumentation + // =============== var ng = angular.module('ng'); ng.config(function ($provide) { @@ -327,7 +366,6 @@ var inject = function () { // patch watchExpression // --------------------- - var w = watchExpression; if (typeof w === 'function') { watchExpression = function () { @@ -378,9 +416,8 @@ var inject = function () { }; - // patch destroy - // ------------- - + // patch $destroy + // -------------- var _destroy = $delegate.__proto__.$destroy; $delegate.__proto__.$destroy = function () { if (debug.watchers[this.$id]) { @@ -392,6 +429,8 @@ var inject = function () { return _destroy.apply(this, arguments); }; + // patch $new + // ---------- var _new = $delegate.__proto__.$new; $delegate.__proto__.$new = function () { @@ -410,8 +449,8 @@ var inject = function () { return ret; }; - // patch apply - // ----------- + // patch $apply + // ------------ var _apply = $delegate.__proto__.$apply; $delegate.__proto__.$apply = function (fn) { var start = performance.now(); diff --git a/js/services/appContext.js b/js/services/appContext.js index 6bb3623..525906c 100644 --- a/js/services/appContext.js +++ b/js/services/appContext.js @@ -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) { // Private vars @@ -10,6 +10,7 @@ panelApp.factory('appContext', function (chromeExtension) { _pollListeners = [], _pollInterval = 500; + // TODO: make this private and have it automatically poll? var getDebugData = function (callback) { chromeExtension.eval(function (window) { @@ -17,15 +18,15 @@ panelApp.factory('appContext', function (chromeExtension) { return {}; } return { - deps: window.__ngDebug.deps, - applyPerf: window.__ngDebug.applyPerf, - watchPerf: window.__ngDebug.watchPerf, + deps: window.__ngDebug.getDeps(), + watchPerf: window.__ngDebug.getWatchPerf(), roots: window.__ngDebug.getRootScopeIds() }; }, function (data) { if (data) { _debugCache = data; + _incomingHistogramData = data.watchPerf; } _pollListeners.forEach(function (fn) { fn(); @@ -37,10 +38,41 @@ panelApp.factory('appContext', function (chromeExtension) { }; 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 // ========== return { - // Fix selection of scope + // TODO: Fix selection of scope // https://github.com/angular/angularjs-batarang/issues/6 executeOnScope: function(scopeId, fn, args, cb) { if (typeof args === 'function') { @@ -69,7 +101,8 @@ panelApp.factory('appContext', function (chromeExtension) { // ------- getHistogram: function () { - return _debugCache.watchPerf; + processHistogram(); + return _histogramCache; }, getListOfRoots: function () { @@ -139,7 +172,7 @@ panelApp.factory('appContext', function (chromeExtension) { clearHistogram: function (cb) { chromeExtension.eval(function (window) { - window.__ngDebug.watchExp = {}; + window.__ngDebug.watchPerf = {}; }, cb); }, diff --git a/panel.html b/panel.html index 03dd221..6d6aa82 100644 --- a/panel.html +++ b/panel.html @@ -23,6 +23,7 @@ + diff --git a/panes/perf.html b/panes/perf.html index 15e1b1e..1091fe8 100644 --- a/panes/perf.html +++ b/panes/perf.html @@ -29,7 +29,7 @@
{{watch.name | first}} - | {{watch.percent}}% | {{watch.time}}ms + | {{watch.percent}}% | {{watch.time | precision}}ms
@@ -48,4 +48,4 @@
-
\ No newline at end of file +