instrumentation rewrite complete, refactored plugin-app model API

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

@ -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) {

@ -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);

@ -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 += '<div class="scope-branch">['
html += '<div class="scope-branch">[';
for (i = 0; i < object.length; i++) {
html += buildDom(object[i]) + ', ';
}
html += ']</div>'
html += ']</div>';
} else if (object instanceof Object) {
for (prop in object) {
if (object.hasOwnProperty(prop)) {

@ -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(
'<div class="scope-branch">' +
'<a href ng-click="inspect()">Scope ({{val.id}})</a> | ' +
'<a href ng-click="hideScopes = !hideScopes">scopes</a> | ' +
'<a href ng-click="showModels = !showModels">models</a>' +
'<a href ng-click="inspect()">Scope ({{val.id}})</a>' +
'<span ng-show="val.children.length"> | <a href ng-click="scopeState[val.id] = !scopeState[val.id]">scopes</a></span>' +
'<span ng-show="val.locals"> | <a href ng-click="modelState[val.id] = !modelState[val.id]">models</a></span>' +
'<div ng-show="showModels">' +
'<div ng-show="modelState[val.id]">' +
'<bat-json-tree val="val.locals" ></bat-json-tree>' +
'</div>' +
'<div ng-hide="hideScopes">' +
'<div ng-hide="scopeState[val.id]">' +
'<div ng-repeat="child in val.children">' +
'<bat-model-tree val="child" inspect="inspect" edit="edit"></bat-model-tree>' +
'</div>' +
@ -29,7 +33,11 @@ panelApp.directive('batModelTree', function($compile) {
'</div>');
$compile(element.contents())(scope.$new());
var childScope = scope.$new();
childScope.modelState = modelState;
childScope.scopeState = scopeState;
$compile(element.contents())(childScope);
}
};
});

@ -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(
'<div class="scope-branch">' +
'<a href ng-click="inspect()">Scope ({{val.id}})</a> | ' +
'<a href ng-click="showState = !showState">toggle</a>' +
'<div ng-hide="showState">' +
'<a href ng-click="scopeState[val.id] = !scopeState[val.id]">toggle</a>' +
'<div ng-hide="scopeState[val.id]">' +
'<ul>' +
'<li ng-repeat="item in val.watchers">' +
'<a href ng-hide="item.split(\'\n\').length < 2" ng-click="showState = !showState">toggle</a> ' +
@ -30,7 +34,10 @@ panelApp.directive('batWatcherTree', function($compile) {
'</div>' +
'</div>');
$compile(element.contents())(scope.$new());
var childScope = scope.$new();
childScope.scopeState = scopeState;
$compile(element.contents())(childScope);
}
};
});

@ -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
// --------------------------------------------------

@ -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';" +

@ -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('<div style="position: fixed; left: 10px; top: 10px; z-index: 9999; background-color: white; padding: 10px;"></div>');
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);
});
});
}
};
});

@ -1,9 +1,9 @@
<div ng-controller="ModelCtrl">
<h2>Model Tree</h2>
<h2>Models</h2>
<div ng-hide="roots.length <= 1">
<label for="select-root">Root <select id="select-root" ng-options="p.value as p.label for p in roots" ng-model="selectedRoot"></select></label>
</div>
<pre>
<bat-model-tree val="trees[selectedRoot]" inspect="inspect" edit="edit"></bat-model-tree>
<bat-model-tree val="tree" inspect="inspect" edit="edit"></bat-model-tree>
</pre>
</div>
</div>

@ -14,7 +14,7 @@
<div class="span6">
<h3>Watch Tree</h3>
<div class="well well-top" style="height: 400px; overflow-y: auto;">
<bat-watcher-tree val="trees[selectedRoot]" inspect="inspect"></bat-watcher-tree>
<bat-watcher-tree val="tree" inspect="inspect"></bat-watcher-tree>
</div>
<div class="well well-bottom">
<label for="select-root" ng-hide="roots.length <= 1">Root