From d8aa313eba946441e6b43e8eb5d61069ec291db2 Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Sun, 11 Nov 2012 22:03:15 -0500 Subject: [PATCH] instrumentation rewrite complete, refactored plugin-app model API --- js/controllers/ModelCtrl.js | 34 +---- js/controllers/PerfCtrl.js | 53 ++------ js/directives/jsonTree.js | 7 +- js/directives/modelTree.js | 24 ++-- js/directives/watcherTree.js | 13 +- js/inject/debug.js | 244 +++++++++++++++++++++++------------ js/services/appContext.js | 39 ++++-- js/services/appInspect.js | 27 ++++ panes/model.html | 6 +- panes/perf.html | 2 +- 10 files changed, 265 insertions(+), 184 deletions(-) create mode 100644 js/services/appInspect.js diff --git a/js/controllers/ModelCtrl.js b/js/controllers/ModelCtrl.js index b3a9b3a..6e99882 100644 --- a/js/controllers/ModelCtrl.js +++ b/js/controllers/ModelCtrl.js @@ -17,44 +17,14 @@ panelApp.controller('ModelCtrl', function ModelCtrl($scope, appContext) { $scope.roots = []; + var updateTree = function () { - if ($('input:focus').length > 0) { - return; - } var roots = appContext.getListOfRoots(); if (!roots) { return; } - var trees = appContext.getModelTrees(); - if (!$scope.trees || $scope.trees.length !== trees.length) { - $scope.trees = trees; - } else { - - var syncBranch = function (oldTree, newTree) { - if (!oldTree || !newTree) { - return; - } - oldTree.locals = newTree.locals; - if (oldTree.children.length !== newTree.children.length) { - oldTree.children = newTree.children; - } else { - oldTree.children.forEach(function (oldBranch, i) { - var newBranch = newTree.children[i]; - syncBranch(oldBranch, newBranch); - }); - } - }; - - var treeId, oldTree, newTree; - for (treeId in $scope.trees) { - if ($scope.trees.hasOwnProperty(treeId)) { - oldTree = $scope.trees[treeId]; - newTree = trees[treeId]; - syncBranch(oldTree, newTree); - } - } - } + $scope.tree = appContext.getModelTree($scope.selectedRoot); $scope.roots.length = roots.length; roots.forEach(function (item, i) { diff --git a/js/controllers/PerfCtrl.js b/js/controllers/PerfCtrl.js index 8daccfb..782dc1e 100644 --- a/js/controllers/PerfCtrl.js +++ b/js/controllers/PerfCtrl.js @@ -2,6 +2,8 @@ panelApp.controller('PerfCtrl', function PerfCtrl($scope, appContext, filesystem $scope.histogram = []; + $scope.roots = []; + $scope.min = 0; $scope.max = 100; @@ -63,56 +65,25 @@ panelApp.controller('PerfCtrl', function PerfCtrl($scope, appContext, filesystem }; var updateTree = function () { - var rts = appContext.getListOfRoots(); - if (!rts) { - // if app not bootstrapped, return undefined + var roots = appContext.getListOfRoots(); + if (!roots) { return; } - var roots = []; - rts.forEach(function (item) { - roots.push({ + + $scope.tree = appContext.getWatchTree($scope.selectedRoot); + + $scope.roots.length = roots.length; + roots.forEach(function (item, i) { + $scope.roots[i] = { label: item, value: item - }); - }); - - $scope.roots = roots; - var trees = appContext.getModelTrees(); - if (!$scope.trees || $scope.trees.length !== trees.length) { - $scope.trees = trees; - } else { - - var syncBranch = function (oldTree, newTree) { - if (!oldTree || !newTree) { - return; - } - oldTree.locals = newTree.locals; - if (oldTree.children.length !== newTree.children.length) { - oldTree.children = newTree.children; - } else { - oldTree.children.forEach(function (oldBranch, i) { - var newBranch = newTree.children[i]; - syncBranch(newBranch, oldBranch); - }); - } }; - - var treeId, oldTree, newTree; - for (treeId in $scope.trees) { - if ($scope.trees.hasOwnProperty(treeId)) { - oldTree = $scope.trees[treeId]; - newTree = trees[treeId]; - syncBranch(oldTree, newTree); - } - } - } - + }); if (roots.length === 0) { $scope.selectedRoot = null; } else if (!$scope.selectedRoot) { - $scope.selectedRoot = roots[0].value; + $scope.selectedRoot = $scope.roots[0].value; } - $scope.$apply(); }; appContext.watchPoll(updateTree); appContext.watchPoll(updateHistogram); diff --git a/js/directives/jsonTree.js b/js/directives/jsonTree.js index a2d3aca..0496cfb 100644 --- a/js/directives/jsonTree.js +++ b/js/directives/jsonTree.js @@ -1,10 +1,9 @@ -// JSON tree panelApp.directive('batJsonTree', function($compile) { return { restrict: 'E', terminal: true, scope: { - val: '=', + val: '=' //edit: '=', }, link: function (scope, element, attrs) { @@ -17,11 +16,11 @@ panelApp.directive('batJsonTree', function($compile) { html += 'null'; } else if (object instanceof Array) { var i; - html += '
[' + html += '
['; for (i = 0; i < object.length; i++) { html += buildDom(object[i]) + ', '; } - html += ']
' + html += ']
'; } else if (object instanceof Object) { for (prop in object) { if (object.hasOwnProperty(prop)) { diff --git a/js/directives/modelTree.js b/js/directives/modelTree.js index 41a3f7e..ad1327e 100644 --- a/js/directives/modelTree.js +++ b/js/directives/modelTree.js @@ -1,5 +1,9 @@ -// model tree -panelApp.directive('batModelTree', function($compile) { +panelApp.directive('batModelTree', function ($compile) { + + // make toggle settings persist across $compile + var modelState = {}; + var scopeState = {}; + return { restrict: 'E', terminal: true, @@ -13,15 +17,15 @@ panelApp.directive('batModelTree', function($compile) { // see: https://github.com/angular/angular.js/issues/898 element.append( '
' + - 'Scope ({{val.id}}) | ' + - 'scopes | ' + - 'models' + + 'Scope ({{val.id}})' + + ' | scopes' + + ' | models' + - '
' + + '
' + '' + '
' + - '
' + + '
' + '
' + '' + '
' + @@ -29,7 +33,11 @@ panelApp.directive('batModelTree', function($compile) { '
'); - $compile(element.contents())(scope.$new()); + var childScope = scope.$new(); + childScope.modelState = modelState; + childScope.scopeState = scopeState; + + $compile(element.contents())(childScope); } }; }); diff --git a/js/directives/watcherTree.js b/js/directives/watcherTree.js index 74bf5ec..4bedb99 100644 --- a/js/directives/watcherTree.js +++ b/js/directives/watcherTree.js @@ -1,5 +1,9 @@ // watchers tree panelApp.directive('batWatcherTree', function($compile) { + + // make toggle settings persist across $compile + var scopeState = {}; + return { restrict: 'E', terminal: true, @@ -13,8 +17,8 @@ panelApp.directive('batWatcherTree', function($compile) { element.append( '
' + 'Scope ({{val.id}}) | ' + - 'toggle' + - '
' + + 'toggle' + + '
' + '
    ' + '
  • ' + 'toggle ' + @@ -30,7 +34,10 @@ panelApp.directive('batWatcherTree', function($compile) { '
' + '
'); - $compile(element.contents())(scope.$new()); + var childScope = scope.$new(); + childScope.scopeState = scopeState; + + $compile(element.contents())(childScope); } }; }); diff --git a/js/inject/debug.js b/js/inject/debug.js index 54806de..a697e1e 100644 --- a/js/inject/debug.js +++ b/js/inject/debug.js @@ -1,64 +1,6 @@ var inject = function () { document.head.appendChild((function () { - // 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 - // one instance of each object or array in the resulting structure. The - // 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. - 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 - - return (function derez(value, path) { - var i, // The loop counter - name, // Property name - nu; // The new object or array - switch (typeof value) { - case 'object': - if (!value) { - return null; - } - for (i = 0; i < objects.length; i += 1) { - if (objects[i] === value) { - return {$ref: paths[i]}; - } - } - objects.push(value); - paths.push(path); - if (Object.prototype.toString.apply(value) === '[object Array]') { - nu = []; - for (i = 0; i < value.length; i += 1) { - nu[i] = derez(value[i], path + '[' + i + ']'); - } - } else { - nu = {}; - for (name in value) { - if (Object.prototype.hasOwnProperty.call(value, name)) { - nu[name] = derez(value[name], - path + '[' + JSON.stringify(name) + ']'); - } - } - } - return nu; - case 'number': - case 'string': - case 'boolean': - return value; - } - }(object, '$')); - }; - // End - // === - var fn = function bootstrap (window) { var angular = window.angular; @@ -107,9 +49,82 @@ var inject = function () { return; } + // 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 + // one instance of each object or array in the resulting structure. The + // 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. + 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 + + return (function derez(value, path) { + var i, // The loop counter + name, // Property name + nu; // The new object or array + switch (typeof value) { + case 'object': + if (!value) { + return null; + } + for (i = 0; i < objects.length; i += 1) { + if (objects[i] === value) { + return {$ref: paths[i]}; + } + } + objects.push(value); + paths.push(path); + if (Object.prototype.toString.apply(value) === '[object Array]') { + nu = []; + for (i = 0; i < value.length; i += 1) { + nu[i] = derez(value[i], path + '[' + i + ']'); + } + } else { + nu = {}; + for (name in value) { + if (Object.prototype.hasOwnProperty.call(value, name)) { + nu[name] = derez(value[name], + path + '[' + JSON.stringify(name) + ']'); + } + } + } + return nu; + case 'number': + case 'string': + case 'boolean': + return value; + } + }(object, '$')); + }; + // End + // === + // Instrumentation // --------------- + var getScopeLocals = function (scope) { + var scopeLocals = {}, prop; + for (prop in scope) { + if (scope.hasOwnProperty(prop) && prop !== 'this' && prop[0] !== '$') { + scopeLocals[prop] = decycle(scope[prop]); + } + } + return scopeLocals; + }; + //var bootstrap = window.angular.bootstrap; var debug = window.__ngDebug = { watchers: {}, // map of scopes --> watchers @@ -117,21 +132,73 @@ var inject = function () { watchPerf: {}, // maps of watch/apply exp/fns to perf data applyPerf: {}, - scopes: {}, // map of scope.$ids --> scope objects + scopes: {}, // map of scope.$ids --> model objects rootScopes: {}, // map of $ids --> refs to root scopes + rootScopeDirty: {}, + + getRootScopeIds: function () { + var ids = []; + angular.forEach(debug.rootScopes, function (elt, id) { + ids.push(id); + }); + return ids; + }, + getScopeTree: function (id) { + if (debug.rootScopeDirty[id] === false) { + return; + } + var traverse = function (sc) { + var tree = { + id: sc.$id, + locals: debug.scopes[sc.$id], + children: [] + }; + + var child = sc.$$childHead; + if (child) { + do { + tree.children.push(traverse(child)); + } while (child !== sc.$$childTail && (child = child.$$nextSibling)); + } - deps: [] - }; + return tree; + }; + var root = debug.rootScopes[id]; + var tree = traverse(root); - var getScopeLocals = function (scope) { - var scopeLocals = {}, prop; - for (prop in scope) { - if (scope.hasOwnProperty(prop) && prop !== 'this' && prop[0] !== '$') { - scopeLocals[prop] = scope[prop]; + if (tree) { + debug.rootScopeDirty[id] = false; } - } - return scopeLocals; + + return tree; + }, + + getWatchTree: function (id) { + var traverse = function (sc) { + var tree = { + id: sc.$id, + watchers: debug.watchers[sc.$id], + children: [] + }; + + var child = sc.$$childHead; + if (child) { + do { + tree.children.push(traverse(child)); + } while (child !== sc.$$childTail && (child = child.$$nextSibling)); + } + + return tree; + }; + + var root = debug.rootScopes[id]; + var tree = traverse(root); + + return tree; + }, + + deps: [] }; var annotate = angular.injector().annotate; @@ -154,7 +221,7 @@ var inject = function () { } function assertArgFn(arg, name, acceptArrayAnnotation) { if (acceptArrayAnnotation && angular.isArray(arg)) { - arg = arg[arg.length - 1]; + arg = arg[arg.length - 1]; } assertArg(angular.isFunction(arg), name, 'not a function, got ' + @@ -182,7 +249,7 @@ var inject = function () { } } else if (angular.isArray(fn)) { last = fn.length - 1; - assertArgFn(fn[last], 'fn') + assertArgFn(fn[last], 'fn'); $inject = fn.slice(0, last); } else { assertArgFn(fn, 'fn', true); @@ -201,9 +268,9 @@ var inject = function () { 'service' ].forEach(function (met) { var temp = $provide[met]; - $provide[met] = function (thingName, definition) { + $provide[met] = function (name, definition) { debug.deps.push({ - name: thingName, + name: name, imports: annotate(definition) }); return temp.apply(this, arguments); @@ -243,7 +310,7 @@ var inject = function () { // ========================== var _watch = $delegate.__proto__.$watch; - $delegate.__proto__.$watch = function(watchExpression, applyFunction) { + $delegate.__proto__.$watch = function (watchExpression, applyFunction) { var thatScope = this; var watchStr = watchFnToHumanReadableString(watchExpression); @@ -253,6 +320,10 @@ var inject = function () { calls: 0 }; } + if (!debug.watchers[thatScope.$id]) { + debug.watchers[thatScope.$id] = []; + } + debug.watchers[thatScope.$id].push(watchStr); // patch watchExpression // --------------------- @@ -260,18 +331,18 @@ var inject = function () { var w = watchExpression; if (typeof w === 'function') { watchExpression = function () { - var start = window.performance.webkitNow(); + var start = performance.now(); var ret = w.apply(this, arguments); - var end = window.performance.webkitNow(); + var end = performance.now(); debug.watchPerf[watchStr].time += (end - start); debug.watchPerf[watchStr].calls += 1; return ret; }; } else { watchExpression = function () { - var start = window.performance.webkitNow(); + var start = performance.now(); var ret = thatScope.$eval(w); - var end = window.performance.webkitNow(); + var end = performance.now(); debug.watchPerf[watchStr].time += (end - start); debug.watchPerf[watchStr].calls += 1; return ret; @@ -284,11 +355,11 @@ var inject = function () { var applyStr = applyFunction.toString(); var unpatchedApplyFunction = applyFunction; applyFunction = function () { - var start = window.performance.webkitNow(); + var start = performance.now(); var ret = unpatchedApplyFunction.apply(this, arguments); - var end = window.performance.webkitNow(); + var end = performance.now(); - debug.scopes[thatScope.$id] = getScopeLocals(thatScope) + debug.scopes[thatScope.$id] = getScopeLocals(thatScope); //TODO: move these checks out of here and into registering the watcher if (!debug.applyPerf[applyStr]) { debug.applyPerf[applyStr] = { @@ -297,7 +368,8 @@ var inject = function () { }; } debug.applyPerf[applyStr].time += (end - start); - debug.applyPerf[applyStr].calls += (end - start); + debug.applyPerf[applyStr].calls += 1; + debug.rootScopeDirty[thatScope.$root.$id] = true; return ret; }; } @@ -313,6 +385,8 @@ var inject = function () { $delegate.__proto__.$destroy = function () { if (debug.watchers[this.$id]) { delete debug.watchers[this.$id]; + } + if (debug.scopes[this.$id]) { delete debug.scopes[this.$id]; } return _destroy.apply(this, arguments); @@ -320,8 +394,8 @@ var inject = function () { var _new = $delegate.__proto__.$new; $delegate.__proto__.$new = function () { - var ret = _new.apply(this, arguments); + var ret = _new.apply(this, arguments); if (ret.$root) { debug.rootScopes[ret.$root.$id] = ret.$root; } @@ -331,6 +405,8 @@ var inject = function () { debug.watchers[ret.$id] = []; } + debug.rootScopeDirty[ret.$root.$id] = true; + return ret; }; @@ -338,9 +414,9 @@ var inject = function () { // ----------- var _apply = $delegate.__proto__.$apply; $delegate.__proto__.$apply = function (fn) { - var start = window.performance.webkitNow(); + var start = performance.now(); var ret = _apply.apply(this, arguments); - var end = window.performance.webkitNow(); + var end = performance.now(); // If the debugging option is enabled, log to console // -------------------------------------------------- diff --git a/js/services/appContext.js b/js/services/appContext.js index cabb2ee..6bb3623 100644 --- a/js/services/appContext.js +++ b/js/services/appContext.js @@ -5,18 +5,23 @@ panelApp.factory('appContext', function (chromeExtension) { // ============ var _debugCache = {}, + _scopeCache = {}, + _watchCache = {}, _pollListeners = [], _pollInterval = 500; // TODO: make this private and have it automatically poll? var getDebugData = function (callback) { chromeExtension.eval(function (window) { - // Detect whether or not this is an AngularJS app - if (!window.angular || !window.__ngDebug) { + if (!window.__ngDebug) { return {}; - } else { - return window.__ngDebug; } + return { + deps: window.__ngDebug.deps, + applyPerf: window.__ngDebug.applyPerf, + watchPerf: window.__ngDebug.watchPerf, + roots: window.__ngDebug.getRootScopeIds() + }; }, function (data) { if (data) { @@ -64,15 +69,33 @@ panelApp.factory('appContext', function (chromeExtension) { // ------- getHistogram: function () { - return _debugCache.histogram; + return _debugCache.watchPerf; }, getListOfRoots: function () { return _debugCache.roots; }, - getModelTrees: function () { - return _debugCache.trees; + getModelTree: function (id) { + chromeExtension.eval("function (window, args) {" + + "return window.__ngDebug.getScopeTree(args.id);" + + "}", {id: id}, function (tree) { + if (tree) { + _scopeCache[id] = tree; + } + }); + return _scopeCache[id]; + }, + + getWatchTree: function (id) { + chromeExtension.eval("function (window, args) {" + + "return window.__ngDebug.getWatchTree(args.id);" + + "}", {id: id}, function (tree) { + if (tree) { + _watchCache[id] = tree; + } + }); + return _watchCache[id]; }, getDeps: function () { @@ -104,7 +127,7 @@ panelApp.factory('appContext', function (chromeExtension) { "return 'good';" + "} else {" + "return 'info';" + - "}" + + "}" + "}" + "}" + "return 'info';" + diff --git a/js/services/appInspect.js b/js/services/appInspect.js new file mode 100644 index 0000000..b4a92ac --- /dev/null +++ b/js/services/appInspect.js @@ -0,0 +1,27 @@ +// Service for highlighting parts of the application +panelApp.factory('appInspect', function (chromeExtension) { + return { + enable: function () { + chromeExtension.eval(function (window) { + var angular = window.angular; + var popover = angular.element('
'); + angular.element(window.document.body).append(popover); + angular.element('.ng-scope'). + on('mouseover', function () { + var thisElt = this; + var thisScope = angular.element(this).scope(); + var models = {}; + for (prop in thisScope) { + if (thisScope.hasOwnProperty(prop) && prop !== 'this' && prop[0] !== '$') { + models[prop] = thisScope[prop]; + } + } + var str = JSON.stringify(models); + console.log(str); + //console.log(thisScope); + popover.html(str); + }); + }); + } + }; +}); diff --git a/panes/model.html b/panes/model.html index 7de2a2f..17aac29 100644 --- a/panes/model.html +++ b/panes/model.html @@ -1,9 +1,9 @@
-

Model Tree

+

Models

-    
+    
   
-
\ No newline at end of file +
diff --git a/panes/perf.html b/panes/perf.html index ef81b3a..15e1b1e 100644 --- a/panes/perf.html +++ b/panes/perf.html @@ -14,7 +14,7 @@

Watch Tree

- +