├── demo ├── ng-stats.js ├── angular.js └── index.html ├── .gitignore ├── .npmignore ├── .editorconfig ├── .eslintignore ├── karma.conf.js ├── .eslintrc ├── scripts └── karma-overrides.js ├── LICENSE ├── bower.json ├── CHANGELOG.md ├── src ├── index.test.js └── index.js ├── package.json ├── dist ├── ng-stats.min.js ├── ng-stats.js └── ng-stats.min.js.map └── README.md /demo/ng-stats.js: -------------------------------------------------------------------------------- 1 | ../dist/ng-stats.js -------------------------------------------------------------------------------- /demo/angular.js: -------------------------------------------------------------------------------- 1 | ../node_modules/angular/angular.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/kcd-common-tools/shared/link/gitignore -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/kcd-common-tools/shared/link/npmignore -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | node_modules/kcd-common-tools/shared/link/editorconfig -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/kcd-common-tools/shared/link/eslintignore -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = require('kcd-common-tools/shared/karma.conf'); 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/kcd-common-tools/shared/.eslintrc", 3 | "globals": { 4 | "chrome": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /scripts/karma-overrides.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var here = require('kcd-common-tools/utils/here'); 3 | 4 | module.exports = function(defaultConfig) { 5 | defaultConfig.files = _.union([ 6 | here('./node_modules/angular/angular.js'), 7 | here('./node_modules/angular-mocks/angular-mocks.js') 8 | ], defaultConfig.files); 9 | return defaultConfig; 10 | }; 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-stats", 3 | "main": "dist/ng-stats.js", 4 | "authors": [ 5 | "Kent C. Dodds (http://kent.doddsfamily.us)", 6 | "Viper Bailey (http://jinxidoru.blogspot.com)" 7 | ], 8 | "contributors": [ 9 | "Kent C. Dodds (http://kent.doddsfamily.us)", 10 | "Viper Bailey (http://jinxidoru.blogspot.com)", 11 | "Daniel Lamb (http://daniellmb.com)" 12 | ], 13 | "description": "Little utility to show stats about your page's angular digest/watches.", 14 | "moduleType": [ 15 | "amd", 16 | "globals", 17 | "node" 18 | ], 19 | "keywords": [ 20 | "angular", 21 | "ng-stats", 22 | "angularjs", 23 | "utilities" 24 | ], 25 | "license": "MIT", 26 | "homepage": "https://github.com/kentcdodds/ng-stats", 27 | "ignore": [ 28 | "src", 29 | ".gitignore", 30 | ".bowerrc", 31 | "Gruntfile.js", 32 | "server.js" 33 | ], 34 | "dependencies": { 35 | "angular": "^1.2.x || >= 1.4.0-beta.0 || >= 1.5.0-beta.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.5.2 2 | 3 | - Fix issues when calling `showAngularStats(false)` initially. 4 | 5 | # 2.5.1 6 | 7 | - Fix jspm issues 8 | 9 | # 2.5.0 10 | 11 | - Add ability to specify a `rootScope`. Thanks [@pvadam](https://github.com/pvadam) 12 | 13 | # 2.4.0 14 | 15 | - Adding different threshold and color for watch count. Thanks [@daniellmb](https://github.com/daniellmb) 16 | [#41](/../../issues/41) 17 | - Adding warning color Thanks [@daniellmb](https://github.com/daniellmb) [#42](/../../issues/42) 18 | 19 | # 2.3.2 20 | 21 | - Making this work with isolate scope-only apps. Thanks [@daniellmb](https://github.com/daniellmb) 22 | [#37](/../../issues/37) 23 | 24 | # 2.3.1 25 | 26 | - Using `kcd-common-tools` to build with ES6 and actually have some (brittle and odd) tests. 27 | 28 | # 2.3.0 29 | 30 | - Adding fix for Chrome extensions 31 | 32 | # 2.2.0 33 | 34 | - Adding the ability to run this in an iframe [#25](/../../pull/25) 35 | 36 | # 2.1.4 37 | 38 | - Adding localStorage [#19](/../../pull/19) 39 | - Fixing options issue [#20](/../../pull/20) 40 | 41 | # 2.1.2 42 | 43 | - In a moment of pure idiocy, I ignored the dist directory... 44 | 45 | # 2.1.1 46 | 47 | - Fixing angular bower dependency (again... [#14](/../../issues/14)) 48 | 49 | # 2.1.0 50 | 51 | - Adding the ability to specify your own styles ([#12](/../../issues/12)) 52 | - Fixing angular bower dependency ([#13](/../../issues/13)) 53 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import angular from 'angular'; 3 | import showAngularStats from './index'; 4 | describe(`ng-stats`, () => { 5 | 6 | describe(`function`, () => { 7 | // TODO... 8 | it(`should expose give me a function I can call`, () => { 9 | expect(showAngularStats).to.be.a('function'); 10 | }); 11 | }); 12 | 13 | describe('angular-stats', () => { 14 | let $injector; 15 | let $compile, scope, el, node, appNode; 16 | let basicTemplate = ` 17 |
20 | Watch Count:
21 | Digest Cycle Length: 22 |
23 | `; 24 | 25 | beforeEach(() => { 26 | appNode = document.createElement('div'); 27 | document.body.appendChild(appNode); 28 | angular.module('app', ['angularStats']); 29 | $injector = angular.bootstrap(appNode, ['app']); 30 | $compile = $injector.get('$compile'); 31 | scope = $injector.get('$rootScope').$new(); 32 | scope.onWatchCountUpdate = function(count) { 33 | scope.onWatchCountUpdate.invokes.push(arguments); 34 | }; 35 | scope.onWatchCountUpdate.invokes = []; 36 | scope.onDigestLengthUpdate = function(digestLength) { 37 | scope.onDigestLengthUpdate.invokes.push(arguments); 38 | }; 39 | scope.onDigestLengthUpdate.invokes = []; 40 | }); 41 | 42 | it(`invoke on-digest-length-update and update the node`, () => { 43 | compileAndDigest(); 44 | const digestLengthNode = node.querySelector('.digest-length'); 45 | 46 | expect(scope.onDigestLengthUpdate.invokes).to.have.length(1); 47 | nodeContentSameAsLatestValue(digestLengthNode, scope.onDigestLengthUpdate); 48 | 49 | scope.$digest(); 50 | 51 | expect(scope.onDigestLengthUpdate.invokes).to.have.length(2); 52 | nodeContentSameAsLatestValue(digestLengthNode, scope.onDigestLengthUpdate); 53 | }); 54 | 55 | // not sure what's up with this. I think it has to do with the 350ms timeout on updating the watch count... 56 | it.skip(`should invoke on-watch-count-update`, () => { 57 | compileAndDigest(); 58 | const watchCountNode = node.querySelector('.watch-count'); 59 | 60 | expect(scope.onWatchCountUpdate.invokes).to.have.length(1); 61 | nodeContentSameAsLatestValue(watchCountNode, scope.onWatchCountUpdate); 62 | 63 | scope.$digest(); 64 | 65 | expect(scope.onWatchCountUpdate.invokes).to.have.length(2); 66 | nodeContentSameAsLatestValue(watchCountNode, scope.onWatchCountUpdate); 67 | }); 68 | 69 | afterEach(() => { 70 | appNode.remove(); 71 | }); 72 | 73 | function compileAndDigest(template, extraProps = {}) { 74 | _.assign(scope, extraProps); 75 | el = $compile(template || basicTemplate)(scope); 76 | node = el[0]; 77 | scope.$digest(); 78 | } 79 | 80 | function nodeContentSameAsLatestValue(theNode, tracker) { 81 | const nodeNumber = parseFloat(theNode.textContent); 82 | const latestInvoke = parseFloat(getLatestFirstArg(tracker).toFixed(2)); 83 | expect(nodeNumber).to.equal(latestInvoke); 84 | } 85 | 86 | function getLatestFirstArg(invokeable) { 87 | return invokeable.invokes[invokeable.invokes.length - 1][0]; 88 | } 89 | }); 90 | }); 91 | 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-stats", 3 | "version": "2.5.4", 4 | "description": "Little utility to show stats about your page's angular digest/watches.", 5 | "main": "dist/ng-stats.js", 6 | "scripts": { 7 | "start": "npm run test", 8 | "serve": "NODE_ENV=development webpack-dev-server --config node_modules/kcd-common-tools/shared/webpack.config.js --content-base demo/ --port 8080 --colors --progress", 9 | "test": "COVERAGE=true NODE_ENV=test karma start", 10 | "test:single": "COVERAGE=true NODE_ENV=test karma start --single-run", 11 | "test:debug": "echo 'WARNING: This is currently not working quite right...' && NODE_ENV=test karma start --browsers Chrome", 12 | "build:dist": "NODE_ENV=development webpack --config node_modules/kcd-common-tools/shared/webpack.config.js --progress --colors", 13 | "build:prod": "NODE_ENV=production webpack --config node_modules/kcd-common-tools/shared/webpack.config.js --progress --colors", 14 | "build": "npm run build:dist & npm run build:prod", 15 | "ci": "npm run code-checks && npm run test:single && npm run check-coverage && npm run build", 16 | "check-coverage": "./node_modules/istanbul/lib/cli.js check-coverage --statements 35 --functions 37 --lines 33 --branches 15", 17 | "report-coverage": "echo 'Reporting coverage stats' && cat ./coverage/lcov.info | codecov", 18 | "predeploy": "npm run deployClean && npm run deployCopy", 19 | "deploy": "surge -p deploy.ignored -d ng-stats.surge.sh", 20 | "deployCopy": "cp demo/index.html deploy.ignored/ && cp dist/ng-stats.js deploy.ignored/ && cp node_modules/angular/angular.js deploy.ignored/", 21 | "deployClean": "rm -rf deploy.ignored/ && mkdir deploy.ignored/", 22 | "only-check": "node node_modules/kcd-common-tools/shared/scripts/only-check.js", 23 | "code-checks": "eslint src/ && npm run only-check", 24 | "release": "npm run build && with-package git commit -am pkg.version && with-package git tag pkg.version && git push && npm publish && git push --tags", 25 | "release:beta": "npm run release && npm run tag:beta", 26 | "tag:beta": "with-package npm dist-tag add pkg.name@pkg.version beta" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/kentcdodds/ng-stats.git" 31 | }, 32 | "keywords": [ 33 | "angular", 34 | "ng-stats", 35 | "angularjs", 36 | "utilities" 37 | ], 38 | "author": "Kent C. Dodds (http://kent.doddsfamily.us)", 39 | "contributors": [ 40 | "Kent C. Dodds (http://kent.doddsfamily.us)", 41 | "Viper Bailey (http://jinxidoru.blogspot.com)", 42 | "Daniel Lamb (http://daniellmb.com)" 43 | ], 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/kentcdodds/ng-stats/issues" 47 | }, 48 | "homepage": "https://github.com/kentcdodds/ng-stats", 49 | "config": { 50 | "ghooks": { 51 | "pre-commit": "npm run code-checks && npm run test:single && npm run check-coverage" 52 | } 53 | }, 54 | "peerDependencies": { 55 | "angular": "^1.2.x || >= 1.4.0-beta.0 || >= 1.5.0-beta.0" 56 | }, 57 | "devDependencies": { 58 | "angular": "1.4.1", 59 | "angular-mocks": "1.4.1", 60 | "babel": "5.5.8", 61 | "babel-core": "5.8.25", 62 | "babel-eslint": "3.1.17", 63 | "babel-loader": "5.1.4", 64 | "chai": "3.3.0", 65 | "codecov.io": "0.1.4", 66 | "eslint": "1.5.1", 67 | "eslint-loader": "0.14.0", 68 | "ghooks": "0.3.2", 69 | "isparta": "3.0.3", 70 | "isparta-loader": "0.2.0", 71 | "istanbul": "0.3.21", 72 | "karma": "0.12.36", 73 | "karma-chai": "0.1.0", 74 | "karma-chrome-launcher": "0.1.12", 75 | "karma-coverage": "0.4.2", 76 | "karma-firefox-launcher": "0.1.6", 77 | "karma-mocha": "0.1.10", 78 | "karma-webpack": "1.7.0", 79 | "kcd-common-tools": "1.0.0-beta.9", 80 | "lodash": "3.10.1", 81 | "mocha": "2.3.3", 82 | "node-libs-browser": "0.5.2", 83 | "surge": "0.14.2", 84 | "uglify-loader": "1.2.0", 85 | "webpack": "1.9.11", 86 | "webpack-dev-server": "1.9.0", 87 | "with-package": "0.2.0" 88 | }, 89 | "jspm": { 90 | "peerDependencies": { 91 | "angular": "*" 92 | } 93 | }, 94 | "kcdCommon": { 95 | "webpack": { 96 | "output": { 97 | "library": "showAngularStats", 98 | "libraryTarget": "umd" 99 | }, 100 | "externals": { 101 | "angular": "angular" 102 | } 103 | }, 104 | "karma": "scripts/karma-overrides.js" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /dist/ng-stats.min.js: -------------------------------------------------------------------------------- 1 | //! ng-stats version 2.5.4 built with ♥ by Kent C. Dodds (http://kent.doddsfamily.us), Viper Bailey (http://jinxidoru.blogspot.com), Daniel Lamb (http://daniellmb.com) (ó ì_í)=óò=(ì_í ò) 2 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("angular")):"function"==typeof define&&define.amd?define(["angular"],e):"object"==typeof exports?exports.showAngularStats=e(require("angular")):t.showAngularStats=e(t.angular)}(this,function(t){return function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={exports:{},id:o,loaded:!1};return t[o].call(r.exports,r,r.exports,e),r.loaded=!0,r.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function o(t){return t&&t.__esModule?t:{"default":t}}function r(){if(!P){P=!0;var t=Object.getPrototypeOf(l()),e=t.$digest;t.$digest=function(){var t=U();e.apply(this,arguments);var n=U()-t;h(s(),n)}}}function i(){return"undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage&&"undefined"!=typeof chrome.storage.local}function a(t){window.self.angular&&l()?c(t):setTimeout(function(){a(t)},200)}function u(t){return t!==!1&&t.autoload||(sessionStorage.removeItem(L),localStorage.removeItem(L),t!==!1)?(t.position=t.position||"top-left",t=T.extend({htmlId:null,rootScope:void 0,digestTimeThreshold:16,watchCountThreshold:2e3,autoload:!1,trackDigest:!1,trackWatches:!1,logDigest:!1,logWatches:!1,styles:{position:"fixed",background:"black",borderBottom:"1px solid #666",borderRight:"1px solid #666",color:"#666",fontFamily:"Courier",width:130,zIndex:9999,textAlign:"right",top:-1===t.position.indexOf("top")?null:0,bottom:-1===t.position.indexOf("bottom")?null:0,right:-1===t.position.indexOf("right")?null:0,left:-1===t.position.indexOf("left")?null:0}},t||{}),t.rootScope&&(D=t.rootScope),t):void 0}function c(t){function e(e,n,o){var r=e.charAt(0).toUpperCase()+e.slice(1);t["track"+r]&&(l[e]=[],n["track + capThingToTrack"]=function(t){o&&l[e][l.length-1]===t||(l[e][l.length-1]=t,l[e].push(t))})}function n(e,n,o){var r=e.charAt(0).toUpperCase()+e.slice(1);if(t["log"+r]){var a;n["log"+r]=function(t){if(!o||a!==t){a=t;var n=i(e,t);n?console.log("%c"+e+":",n,t):console.log(e+":",t)}}}}function o(t,e){return t>e?"red":t>.7*e?"orange":"green"}function i(e,n){var r;return"digest"===e?r="color:"+o(n,t.digestTimeThreshold):"watches"===e&&(r="color:"+o(n,t.watchCountThreshold)),r}function a(e,n){var r=n||A,i=o(r,t.digestTimeThreshold);R=p(e)?R:e;var a=o(R,t.watchCountThreshold);if(A=p(n)?A:n,h.text(R).css({color:a}),v.text(A.toFixed(2)).css({color:i}),n){var u=m.getContext("2d");f>0&&(f=0,u.fillStyle="#333",u.fillRect(w.width-1,0,1,w.height)),u.fillStyle=i,u.fillRect(w.width-1,Math.max(0,w.height-r),2,2)}}function c(){if(s.active){setTimeout(c,250);var t=m.getContext("2d"),e=t.getImageData(1,0,w.width-1,w.height);t.putImageData(e,0,0),t.fillStyle=f++>2?"black":"#333",t.fillRect(w.width-1,0,1,w.height)}}t=void 0!==t?t:{};var l={listeners:E};if(O&&(O.$el&&O.$el.remove(),O.active=!1,O=null),t=u(t)){r();var s=O={active:!0};if(t.autoload)if("localStorage"===t.autoload)localStorage.setItem(L,JSON.stringify(t));else{if("sessionStorage"!==t.autoload&&"boolean"!=typeof t.autoload)throw new Error("Invalid value for autoload: "+t.autoload+' can only be "localStorage" "sessionStorage" or boolean.');sessionStorage.setItem(L,JSON.stringify(t))}var d=T.element(document.body),f=0,g=t.htmlId?' id="'+t.htmlId+'"':"";s.$el=T.element("
|
").css(t.styles),d.append(s.$el);var h=s.$el.find("span"),v=h.next(),w={width:130,height:40},m=s.$el.find("canvas").attr(w)[0];return E.digestLength.ngStatsAddToCanvas=function(t){a(null,t)},E.watchCount.ngStatsAddToCanvas=function(t){a(t)},e("digest",E.digestLength),e("watches",E.watchCount,!0),n("digest",E.digestLength),n("watches",E.watchCount,!0),c(),D.$$phase||D.$digest(),l}}function l(){if(D)return D;var t=document.querySelector(q);return t?D=T.element(t).scope().$root:null}function s(){clearTimeout(k);var t=U();return t-I>300?(I=t,R=v()):k=setTimeout(function(){h(s())},350),R}function d(t){var e=f(t);return v(e)}function f(t){t=T.element(t);var e=t.scope();return e||(t=T.element(t.querySelector(q)),e=t.scope()),e}function g(t){return t&&t.$$watchers?t.$$watchers:[]}function h(t,e){p(t)||T.forEach(E.watchCount,function(e){e(t)}),p(e)||T.forEach(E.digestLength,function(t){t(e)})}function p(t){return null===t||void 0===t}function v(t){var e=0;return w(t,function(t){e+=g(t).length}),e}function w(t,e){if("function"==typeof t&&(e=t,t=null),t=t||l(),t=C(t)){var n=e(t);return n===!1?n:S(t,e)}}function m(t,e){for(var n;(t=t.$$nextSibling)&&(n=e(t),n!==!1)&&(n=S(t,e),n!==!1););return n}function S(t,e){for(var n;(t=t.$$childHead)&&(n=e(t),n!==!1)&&(n=m(t,e),n!==!1););return n}function y(t){var e=null;return w(function(n){return n.$id===t?(e=n,!1):void 0}),e}function C(t){return x(t)&&(t=y(t)),t}function x(t){return"string"==typeof t||"number"==typeof t}Object.defineProperty(e,"__esModule",{value:!0});var $=n(1),b=o($),T=b["default"];T.version||(T=window.angular),e["default"]=c;var D,L="showAngularStats_autoload",O=null,U=window.self.performance&&window.self.performance.now?function(){return window.self.performance.now()}:function(){return Date.now()},I=U(),k=null,R=s()||0,A=0,q=".ng-scope, .ng-isolate-scope",P=!1,E={watchCount:{},digestLength:{}},j=sessionStorage[L]||!i()&&localStorage[L];j&&a(JSON.parse(j)),T.module("angularStats",[]).directive("angularStats",function(){function t(t){for(var e=t[0];e.parentElement;)e=e.parentElement;return e}var e=1;return{scope:{digestLength:"@",watchCount:"@",watchCountRoot:"@",onDigestLengthUpdate:"&?",onWatchCountUpdate:"&?"},link:function(n,o,i){function a(){if(i.hasOwnProperty("digestLength")){var t=o;i.digestLength&&(t=T.element(o[0].querySelector(i.digestLength))),E.digestLength["ngStatsDirective"+f]=function(e){window.dirDigestNode=t[0],t.text((e||0).toFixed(2))}}}function u(){if(i.hasOwnProperty("watchCount")){var e,r=o;if(n.watchCount&&(r=T.element(o[0].querySelector(i.watchCount))),n.watchCountRoot)if("this"===n.watchCountRoot)e=o;else{var a;if(a=i.hasOwnProperty("watchCountOfChild")?o[0]:t(o),e=T.element(a.querySelector(n.watchCountRoot)),!e.length)throw new Error("no element at selector: "+n.watchCountRoot)}E.watchCount["ngStatsDirective"+f]=function(t){var n=t;e&&(n=d(e)),r.text(n)}}}function c(){i.hasOwnProperty("onWatchCountUpdate")&&(E.watchCount["ngStatsDirectiveUpdate"+f]=function(t){n.onWatchCountUpdate({watchCount:t})})}function l(){i.hasOwnProperty("onDigestLengthUpdate")&&(E.digestLength["ngStatsDirectiveUpdate"+f]=function(t){n.onDigestLengthUpdate({digestLength:t})})}function s(){delete E.digestLength["ngStatsDirectiveUpdate"+f],delete E.watchCount["ngStatsDirectiveUpdate"+f],delete E.digestLength["ngStatsDirective"+f],delete E.watchCount["ngStatsDirective"+f]}r();var f=e++;a(),u(),c(),l(),n.$on("$destroy",s)}}}),t.exports=e["default"]},function(e,n){e.exports=t}])}); 3 | //# sourceMappingURL=ng-stats.min.js.map -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ng-stats demo 6 | 32 | 33 | 34 | 35 |
36 |

ng-stats demo

37 | Repo on GitHub 38 |
39 |
40 | This is a demo for a little utility to show stats about your page's angular digest/watches. This library currently 41 | has a simple script to produce a chart. It also creates a module called `angularStats` which has a directive called 42 | `angular-stats` which can be used to put angular stats on a specific place on the page that you specify. 43 |
44 |
45 |

Chart

46 | 47 |
48 |
49 |

Directive

50 |
53 | Watch Count:
54 | Digest Cycle Length: 55 |
56 |
57 |
58 |

Watcher & Digest Simulation

59 |
60 |

Control Watch Count and Digest Length

61 |
62 | 63 |
64 |

Force Digests

65 | You can force digests by mousing over the items below. Notice that the filter wait impacts performance more 66 | quickly than the watch count... In fact, see the huge difference between 0 and 1 milliseconds on the filter wait. 67 | This scenario is most likely to be the case in most apps, however, this demo illustrates the importance of keeping 68 | your watcher expressions lightweight. 69 |
70 |
71 |
72 | {{item | longFilter}} 73 |
74 |
75 |
76 | 77 | 80 | 81 | 82 | 83 | 116 | 233 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng-stats 2 | 3 | ![unmaintained](https://img.shields.io/badge/status-unmaintained-red.svg?style=flat-square) 4 | 5 | [![npm version](https://img.shields.io/npm/v/ng-stats.svg?style=flat-square)](https://www.npmjs.org/package/ng-stats) 6 | [![npm downloads](https://img.shields.io/npm/dm/ng-stats.svg?style=flat-square)](http://npm-stat.com/charts.html?package=ng-stats&from=2015-01-01) 7 | 8 | Little utility to show stats about your page's angular digest/watches. This library currently has a simple script to 9 | produce a chart (see below). It also creates a module called `angularStats` which has a directive called `angular-stats` 10 | which can be used to put angular stats on a specific place on the page that you specify. 11 | 12 | Example Green (digests are running smoothly): 13 | 14 | ![Example Green](http://cl.ly/image/2H1X2Q222i0F/ng-stats-good.png) 15 | 16 | Example Red (digests are taking a bit...): 17 | 18 | ![Example Red](http://cl.ly/image/2f3L1B3b1q2V/ng-stats-bad.png) 19 | 20 | [Interactive Demo](https://kentcdodds.github.io/ng-stats/) 21 | 22 | The first number is the number of watchers on the page (including `{{variables}}`, `$scope.$watch`, etc.). The second 23 | number is how long (in milliseconds) it takes angular to go through each digest cycle on average (bigger is worse). The 24 | graph shows a trend of the digest cycle average time. 25 | 26 | ## Thanks 27 | 28 | [Viper Bailey](https://github.com/jinxidoru) for writing the initial version (and most of the graph stuff). 29 | 30 | ## Development 31 | 32 | 1. `npm install` 33 | 2. `bower install` 34 | 3. `grunt` for server 35 | 4. `grunt release` for release 36 | 37 | ## Installation 38 | 39 | ### Bookmarklet 40 | 41 | Copy the code below and create a bookmarklet for ng-stats to use it on any angular website (so long as the debug info is 42 | enabled, if not, you'll need to run `angular.reloadWithDebugInfo()` first). 43 | 44 | ```javascript 45 | javascript: (function() {var a = document.createElement("script");a.src = "https://rawgit.com/kentcdodds/ng-stats/master/dist/ng-stats.js";a.onload=function(){window.showAngularStats()};document.head.appendChild(a)})(); 46 | ``` 47 | 48 | If you just want the chart for development purposes, it's actually easiest to use as a 49 | [Chrome DevTools Snippet](https://developer.chrome.com/devtools/docs/authoring-development-workflow#snippets). 50 | Just copy/paste the `dist/ng-stats.js` file into a snippet. 51 | 52 | However, it uses UMD, so you can also include it in your app if you want via: 53 | 54 | `$ npm|bower install ng-stats` 55 | 56 | or download `dist/ng-stats.js` and 57 | 58 | `` 59 | 60 | or 61 | 62 | ```javascript 63 | var showAngularStats = require('path-to-ng-stats'); 64 | ``` 65 | 66 | You now have a `angularStats` module and `showAngularStats` function you can call 67 | 68 | ## Chart 69 | 70 | ### Usage 71 | 72 | Simply invoke `showAngularStats( { options } )` and the chart will appear. It also returns an object with a few handy 73 | things depending on your options. One of these things is `listeners` which is an object that has two objects: 74 | `digestLength` and `watchCount`. You can add a custom listener that is called when the digest cycles happen (though 75 | for performance reasons when calculating the watchCount, the `watchCount` listeners are throttled). Here's an example 76 | of adding custom listeners: 77 | 78 | ```javascript 79 | var ngStats = showAngularStats(); 80 | 81 | ngStats.listeners.digestLength.nameOfYourListener = function(digestLength) { 82 | console.log('Digest: ' + digestLength); 83 | }; 84 | 85 | ngStats.listeners.watchCount.nameOfYourListener = function(watchCount) { 86 | console.log('Watches: ' + watchCount); 87 | }; 88 | ``` 89 | 90 | ### Options 91 | 92 | You can pass the function one (optional) argument. If you pass `false` it will turn off "autoload" and do nothing. You can also pass an object with other options: 93 | 94 | #### position (object) - default: `'topleft'` 95 | 96 | Controls the position of the graphic. 97 | Possible values: Any combination of `top`, `left`, `right`, `bottom`. 98 | 99 | #### digestTimeThreshold (number) - default: 16 100 | 101 | The time (in milliseconds) where it goes from red to green. 102 | 103 | #### autoload (string or boolean) - default: false 104 | 105 | Uses the [Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Storage) to store whether the graphic should be automatically loaded every time the page is reloaded. Pass in `'localStorage'` for persistent loading or `'sessionStorage'` to load ng-stats for only the current session. 106 | 107 | Note, if you pass `false` as options, it will simply remove the stats window and exit: `showAngularStats(false)` 108 | 109 | #### trackDigest (boolean) - default: false 110 | 111 | `showAngularStats` returns an object. Setting this to true will add an array to that object called `digest` that holds 112 | all of the digest lengths. 113 | 114 | #### trackWatches (boolean) - default: false 115 | 116 | `showAngularStats` returns an object. Setting this to true will add an array to that object called `watches` that holds 117 | all of the watch counts as they change. 118 | 119 | #### logDigest (boolean) - default: false 120 | 121 | Setting this to true will cause ng-stats to log out the digest lengths to the console. It will be colored green or red 122 | based on the digestTimeThreshold. 123 | 124 | #### logWatches (boolean) - default: false 125 | 126 | Setting this to true will cause ng-stats to log out the watch count to the console as it changes. 127 | 128 | #### htmlId (string) - default: null 129 | 130 | Sets an HTML ID attribute to the rendered stats element. 131 | 132 | #### rootScope (object) - default: undefined 133 | 134 | Passes the $rootScope to ng-stats. This parameter is only required for Ionic support where the ng-scope and ng-isolate-scope classes are removed. The only way of using the ng-stats with Ionic is invoking `showAngularStats( { options } )` in your code and passing the `$rootScope` manually. 135 | 136 | ## Module 137 | 138 | Simply declare it as a dependency `angular.module('your-mod', ['angularStats']);` 139 | 140 | Then use the directive: 141 | 142 | ``` 143 |
146 | Watch Count:
147 | Digest Cycle Length: 148 |
149 | ``` 150 | 151 | ### angular-stats attributes 152 | 153 | #### angular-stats 154 | 155 | The directive itself. No value is expected 156 | 157 | #### watch-count 158 | 159 | Having this attribute will keep track of the watch count and update the `text` of a specified element. 160 | Possible values are: 161 | 162 | 1. Selector for a child element to update 163 | 2. no value - refers to the current element (updates the text of the current element) 164 | 165 | #### watch-count-root 166 | 167 | `angular-stats` defaults to keeping track of the watch count for the whole page, however if you want to keep track of a 168 | specific element (and its children), provide this with a element query selector. As a convenience, if `this` is provided 169 | then the `watch-count-root` will be set to the element itself. Also, if you want to scope the query selector to the 170 | element, add `watch-count-of-child` as an attribute (no value) 171 | 172 | #### on-watch-count-update 173 | 174 | Because of the performance implications of calculating the watch count, this is not called every digest but a maximum 175 | of once every 300ms. Still avoid invoking another digest here though. The name of the variable passed is `watchCount` 176 | (like you see in the example). 177 | 178 | #### digest-length 179 | 180 | This works similar to the `watch-count` attribute. It's presence will cause the directive to keep track of the 181 | `digest-length` and will update the `text` of a specified element (rounds to two decimal places). Possible values are: 182 | 183 | 1. Selector for a child element to update 184 | 2. no value - refers to the current element (updates the text of the current element) 185 | 186 | #### on-digest-length-update 187 | 188 | Pass an expression to evaluate with every digest length update. This gets called on every digest (so be sure you don't 189 | invoke another digest in this handler or you'll get an infinite loop of doom). The name of the variable passed is 190 | `digestLength` (as in the example). 191 | 192 | ## Roadmap 193 | 194 | - Add analysis to highlight areas on the page that have highest watch counts. 195 | - Somehow find out which watches are taking the longest... Ideas on implementation are welcome... 196 | - See what could be done with the new scoped digest coming in Angular version 1.3. 197 | - Count the number of digests or provide some analytics for frequency? 198 | - Create a Chrome Extension for the chart or integrate with [batarang](https://github.com/angular/batarang)? 199 | - Other ideas? 200 | 201 | ## Other notes 202 | 203 | ### Performance impact 204 | 205 | This will not impact the speed of your application at all until you actually use it. It also will hopefully only 206 | negatively impact your app's performance minimally. This is intended to be used in development only for debugging 207 | purposes so it shouldn't matter much anyway. It should be noted that calculating the watch count can be pretty 208 | expensive, so it's throttled to be calculated a minimum of 300ms. 209 | 210 | ### Using in an iframe 211 | 212 | Thanks to [this brilliant PR](https://github.com/kentcdodds/ng-stats/pull/25) from 213 | [@jinyangzhen](https://github.com/jinyangzhen), you can run ng-stats in an iframe (like plunker!). See the PR for 214 | an example of how to accomplish this. 215 | 216 | ## License 217 | 218 | MIT 219 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import ng from 'angular'; 3 | 4 | let angular = ng; 5 | 6 | /* istanbul ignore next */ 7 | if (!angular.version) { 8 | // we're doing this because some versions 9 | // of angular don't expose itself correctly 10 | angular = window.angular; 11 | } 12 | 13 | export default showAngularStats; 14 | 15 | var autoloadKey = 'showAngularStats_autoload'; 16 | var current = null; 17 | // define the timer function to use based upon whether or not 'performance is available' 18 | var timerNow = window.self.performance && window.self.performance.now 19 | ? () => window.self.performance.now() 20 | : () => Date.now(); 21 | 22 | var lastWatchCountRun = timerNow(); 23 | var watchCountTimeout = null; 24 | var lastWatchCount = getWatcherCount() || 0; 25 | var lastDigestLength = 0; 26 | var scopeSelectors = '.ng-scope, .ng-isolate-scope'; 27 | var $rootScope; 28 | 29 | var digestIsHijacked = false; 30 | 31 | var listeners = { 32 | watchCount: {}, 33 | digestLength: {} 34 | }; 35 | 36 | // Hijack $digest to time it and update data on every digest. 37 | function hijackDigest() { 38 | if (digestIsHijacked) { 39 | return; 40 | } 41 | digestIsHijacked = true; 42 | var scopePrototype = Object.getPrototypeOf(getRootScope()); 43 | var oldDigest = scopePrototype.$digest; 44 | scopePrototype.$digest = function $digest() { 45 | var start = timerNow(); 46 | oldDigest.apply(this, arguments); 47 | var diff = (timerNow() - start); 48 | updateData(getWatcherCount(), diff); 49 | }; 50 | } 51 | 52 | // used to prevent localstorage error in chrome packaged apps 53 | function isChromeApp() { 54 | return (typeof chrome !== 'undefined' && 55 | typeof chrome.storage !== 'undefined' && 56 | typeof chrome.storage.local !== 'undefined'); 57 | } 58 | 59 | // check for autoload 60 | var autoloadOptions = sessionStorage[autoloadKey] || (!isChromeApp() && localStorage[autoloadKey]); 61 | if (autoloadOptions) { 62 | autoload(JSON.parse(autoloadOptions)); 63 | } 64 | 65 | function autoload(options) { 66 | if (window.self.angular && getRootScope()) { 67 | showAngularStats(options); 68 | } else { 69 | // wait for angular to load... 70 | setTimeout(function() { 71 | autoload(options); 72 | }, 200); 73 | } 74 | } 75 | 76 | function initOptions(opts) { 77 | 78 | // Remove autoload if they did not specifically request it 79 | if (opts === false || !opts.autoload) { 80 | sessionStorage.removeItem(autoloadKey); 81 | localStorage.removeItem(autoloadKey); 82 | // do nothing if the argument is false 83 | if (opts === false) { 84 | return; 85 | } 86 | } 87 | 88 | opts.position = opts.position || 'top-left'; 89 | opts = angular.extend({ 90 | htmlId: null, 91 | rootScope: undefined, 92 | digestTimeThreshold: 16, 93 | watchCountThreshold: 2000, 94 | autoload: false, 95 | trackDigest: false, 96 | trackWatches: false, 97 | logDigest: false, 98 | logWatches: false, 99 | styles: { 100 | position: 'fixed', 101 | background: 'black', 102 | borderBottom: '1px solid #666', 103 | borderRight: '1px solid #666', 104 | color: '#666', 105 | fontFamily: 'Courier', 106 | width: 130, 107 | zIndex: 9999, 108 | textAlign: 'right', 109 | top: opts.position.indexOf('top') === -1 ? null : 0, 110 | bottom: opts.position.indexOf('bottom') === -1 ? null : 0, 111 | right: opts.position.indexOf('right') === -1 ? null : 0, 112 | left: opts.position.indexOf('left') === -1 ? null : 0 113 | } 114 | }, opts || {}); 115 | 116 | // for ionic support 117 | if (opts.rootScope) { 118 | $rootScope = opts.rootScope; 119 | } 120 | 121 | return opts; 122 | } 123 | 124 | function showAngularStats(opts) { 125 | /* eslint max-statements:[2, 45] */ 126 | /* eslint complexity:[2, 18] */ 127 | /* eslint consistent-return:0 */ 128 | // TODO ^^ fix these things... 129 | opts = opts !== undefined ? opts : {}; 130 | var returnData = { 131 | listeners: listeners 132 | }; 133 | // delete the previous one 134 | if (current) { 135 | current.$el && current.$el.remove(); 136 | current.active = false; 137 | current = null; 138 | } 139 | 140 | // Implemented in separate function due to webpack's statement count limit 141 | opts = initOptions(opts); 142 | 143 | if(!opts) { 144 | return; 145 | } 146 | 147 | hijackDigest(); 148 | 149 | // setup the state 150 | var state = current = {active: true}; 151 | 152 | // auto-load on startup 153 | if (opts.autoload) { 154 | if (opts.autoload === 'localStorage') { 155 | localStorage.setItem(autoloadKey, JSON.stringify(opts)); 156 | } else if (opts.autoload === 'sessionStorage' || typeof opts.autoload === 'boolean') { 157 | sessionStorage.setItem(autoloadKey, JSON.stringify(opts)); 158 | } else { 159 | throw new Error( 160 | 'Invalid value for autoload: ' + opts.autoload + ' can only be "localStorage" "sessionStorage" or boolean.' 161 | ); 162 | } 163 | } 164 | 165 | // general variables 166 | var bodyEl = angular.element(document.body); 167 | var noDigestSteps = 0; 168 | 169 | // add the DOM element 170 | var htmlId = opts.htmlId ? (' id="' + opts.htmlId + '"') : ''; 171 | state.$el = angular.element('
|
').css(opts.styles); 173 | bodyEl.append(state.$el); 174 | var $watchCount = state.$el.find('span'); 175 | var $digestTime = $watchCount.next(); 176 | 177 | // initialize the canvas 178 | var graphSz = {width: 130, height: 40}; 179 | var cvs = state.$el.find('canvas').attr(graphSz)[0]; 180 | 181 | 182 | // add listeners 183 | listeners.digestLength.ngStatsAddToCanvas = function(digestLength) { 184 | addDataToCanvas(null, digestLength); 185 | }; 186 | 187 | listeners.watchCount.ngStatsAddToCanvas = function(watchCount) { 188 | addDataToCanvas(watchCount); 189 | }; 190 | 191 | track('digest', listeners.digestLength); 192 | track('watches', listeners.watchCount, true); 193 | 194 | log('digest', listeners.digestLength); 195 | log('watches', listeners.watchCount, true); 196 | 197 | function track(thingToTrack, listenerCollection, diffOnly) { 198 | var capThingToTrack = thingToTrack.charAt(0).toUpperCase() + thingToTrack.slice(1); 199 | if (opts['track' + capThingToTrack]) { 200 | returnData[thingToTrack] = []; 201 | listenerCollection['track + capThingToTrack'] = function(tracked) { 202 | if (!diffOnly || returnData[thingToTrack][returnData.length - 1] !== tracked) { 203 | returnData[thingToTrack][returnData.length - 1] = tracked; 204 | returnData[thingToTrack].push(tracked); 205 | } 206 | }; 207 | } 208 | } 209 | 210 | function log(thingToLog, listenerCollection, diffOnly) { 211 | var capThingToLog = thingToLog.charAt(0).toUpperCase() + thingToLog.slice(1); 212 | if (opts['log' + capThingToLog]) { 213 | var last; 214 | listenerCollection['log' + capThingToLog] = function(tracked) { 215 | if (!diffOnly || last !== tracked) { 216 | last = tracked; 217 | var color = colorLog(thingToLog, tracked); 218 | if (color) { 219 | console.log('%c' + thingToLog + ':', color, tracked); 220 | } else { 221 | console.log(thingToLog + ':', tracked); 222 | } 223 | } 224 | }; 225 | } 226 | } 227 | 228 | function getColor(metric, threshold) { 229 | if (metric > threshold) { 230 | return 'red'; 231 | } else if (metric > 0.7 * threshold) { 232 | return 'orange'; 233 | } 234 | return 'green'; 235 | } 236 | 237 | function colorLog(thingToLog, tracked) { 238 | var color; 239 | if (thingToLog === 'digest') { 240 | color = 'color:' + getColor(tracked, opts.digestTimeThreshold); 241 | } else if (thingToLog === 'watches') { 242 | color = 'color:' + getColor(tracked, opts.watchCountThreshold); 243 | } 244 | return color; 245 | } 246 | 247 | function addDataToCanvas(watchCount, digestLength) { 248 | var averageDigest = digestLength || lastDigestLength; 249 | var digestColor = getColor(averageDigest, opts.digestTimeThreshold); 250 | lastWatchCount = nullOrUndef(watchCount) ? lastWatchCount : watchCount; 251 | var watchColor = getColor(lastWatchCount, opts.watchCountThreshold); 252 | lastDigestLength = nullOrUndef(digestLength) ? lastDigestLength : digestLength; 253 | $watchCount.text(lastWatchCount).css({color: watchColor}); 254 | $digestTime.text(lastDigestLength.toFixed(2)).css({color: digestColor}); 255 | 256 | if (!digestLength) { 257 | return; 258 | } 259 | 260 | // color the sliver if this is the first step 261 | var ctx = cvs.getContext('2d'); 262 | if (noDigestSteps > 0) { 263 | noDigestSteps = 0; 264 | ctx.fillStyle = '#333'; 265 | ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height); 266 | } 267 | 268 | // mark the point on the graph 269 | ctx.fillStyle = digestColor; 270 | ctx.fillRect(graphSz.width - 1, Math.max(0, graphSz.height - averageDigest), 2, 2); 271 | } 272 | 273 | // Shift the canvas to the left. 274 | function shiftLeft() { 275 | if (state.active) { 276 | setTimeout(shiftLeft, 250); 277 | var ctx = cvs.getContext('2d'); 278 | var imageData = ctx.getImageData(1, 0, graphSz.width - 1, graphSz.height); 279 | ctx.putImageData(imageData, 0, 0); 280 | ctx.fillStyle = ((noDigestSteps++) > 2) ? 'black' : '#333'; 281 | ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height); 282 | } 283 | } 284 | 285 | // start everything 286 | shiftLeft(); 287 | if (!$rootScope.$$phase) { 288 | $rootScope.$digest(); 289 | } 290 | 291 | return returnData; 292 | } 293 | 294 | angular.module('angularStats', []).directive('angularStats', function() { 295 | var index = 1; 296 | return { 297 | scope: { 298 | digestLength: '@', 299 | watchCount: '@', 300 | watchCountRoot: '@', 301 | onDigestLengthUpdate: '&?', 302 | onWatchCountUpdate: '&?' 303 | }, 304 | link: function(scope, el, attrs) { 305 | hijackDigest(); 306 | var directiveIndex = index++; 307 | 308 | setupDigestLengthElement(); 309 | setupWatchCountElement(); 310 | addWatchCountListener(); 311 | addDigestLengthListener(); 312 | scope.$on('$destroy', destroyListeners); 313 | 314 | function setupDigestLengthElement() { 315 | if (attrs.hasOwnProperty('digestLength')) { 316 | var digestEl = el; 317 | if (attrs.digestLength) { 318 | digestEl = angular.element(el[0].querySelector(attrs.digestLength)); 319 | } 320 | listeners.digestLength['ngStatsDirective' + directiveIndex] = function(length) { 321 | window.dirDigestNode = digestEl[0]; 322 | digestEl.text((length || 0).toFixed(2)); 323 | }; 324 | } 325 | } 326 | 327 | function setupWatchCountElement() { 328 | if (attrs.hasOwnProperty('watchCount')) { 329 | var watchCountRoot; 330 | var watchCountEl = el; 331 | if (scope.watchCount) { 332 | watchCountEl = angular.element(el[0].querySelector(attrs.watchCount)); 333 | } 334 | 335 | if (scope.watchCountRoot) { 336 | if (scope.watchCountRoot === 'this') { 337 | watchCountRoot = el; 338 | } else { 339 | // In the case this directive is being compiled and it's not in the dom, 340 | // we're going to do the find from the root of what we have... 341 | var rootParent; 342 | if (attrs.hasOwnProperty('watchCountOfChild')) { 343 | rootParent = el[0]; 344 | } else { 345 | rootParent = findRootOfElement(el); 346 | } 347 | watchCountRoot = angular.element(rootParent.querySelector(scope.watchCountRoot)); 348 | if (!watchCountRoot.length) { 349 | throw new Error('no element at selector: ' + scope.watchCountRoot); 350 | } 351 | } 352 | } 353 | 354 | listeners.watchCount['ngStatsDirective' + directiveIndex] = function(count) { 355 | var watchCount = count; 356 | if (watchCountRoot) { 357 | watchCount = getWatcherCountForElement(watchCountRoot); 358 | } 359 | watchCountEl.text(watchCount); 360 | }; 361 | } 362 | } 363 | 364 | function addWatchCountListener() { 365 | if (attrs.hasOwnProperty('onWatchCountUpdate')) { 366 | listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex] = function(count) { 367 | scope.onWatchCountUpdate({watchCount: count}); 368 | }; 369 | } 370 | } 371 | 372 | function addDigestLengthListener() { 373 | if (attrs.hasOwnProperty('onDigestLengthUpdate')) { 374 | listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex] = function(length) { 375 | scope.onDigestLengthUpdate({digestLength: length}); 376 | }; 377 | } 378 | } 379 | 380 | function destroyListeners() { 381 | delete listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex]; 382 | delete listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex]; 383 | delete listeners.digestLength['ngStatsDirective' + directiveIndex]; 384 | delete listeners.watchCount['ngStatsDirective' + directiveIndex]; 385 | } 386 | } 387 | }; 388 | 389 | function findRootOfElement(el) { 390 | var parent = el[0]; 391 | while (parent.parentElement) { 392 | parent = parent.parentElement; 393 | } 394 | return parent; 395 | } 396 | }); 397 | 398 | // UTILITY FUNCTIONS 399 | 400 | function getRootScope() { 401 | if ($rootScope) { 402 | return $rootScope; 403 | } 404 | var scopeEl = document.querySelector(scopeSelectors); 405 | if (!scopeEl) { 406 | return null; 407 | } 408 | $rootScope = angular.element(scopeEl).scope().$root; 409 | return $rootScope; 410 | } 411 | 412 | // Uses timeouts to ensure that this is only run every 300ms (it's a perf bottleneck) 413 | function getWatcherCount() { 414 | clearTimeout(watchCountTimeout); 415 | var now = timerNow(); 416 | if (now - lastWatchCountRun > 300) { 417 | lastWatchCountRun = now; 418 | lastWatchCount = getWatcherCountForScope(); 419 | } else { 420 | watchCountTimeout = setTimeout(function() { 421 | updateData(getWatcherCount()); 422 | }, 350); 423 | } 424 | return lastWatchCount; 425 | } 426 | 427 | function getWatcherCountForElement(element) { 428 | var startingScope = getClosestChildScope(element); 429 | return getWatcherCountForScope(startingScope); 430 | } 431 | 432 | function getClosestChildScope(element) { 433 | element = angular.element(element); 434 | var scope = element.scope(); 435 | if (!scope) { 436 | element = angular.element(element.querySelector(scopeSelectors)); 437 | scope = element.scope(); 438 | } 439 | return scope; 440 | } 441 | 442 | function getWatchersFromScope(scope) { 443 | return scope && scope.$$watchers ? scope.$$watchers : []; 444 | } 445 | 446 | // iterate through listeners to call them with the watchCount and digestLength 447 | function updateData(watchCount, digestLength) { 448 | // update the listeners 449 | if (!nullOrUndef(watchCount)) { 450 | angular.forEach(listeners.watchCount, function(listener) { 451 | listener(watchCount); 452 | }); 453 | } 454 | if (!nullOrUndef(digestLength)) { 455 | angular.forEach(listeners.digestLength, function(listener) { 456 | listener(digestLength); 457 | }); 458 | } 459 | } 460 | 461 | function nullOrUndef(item) { 462 | return item === null || item === undefined; 463 | } 464 | 465 | function getWatcherCountForScope(scope) { 466 | var count = 0; 467 | iterateScopes(scope, function(childScope) { 468 | count += getWatchersFromScope(childScope).length; 469 | }); 470 | return count; 471 | } 472 | 473 | function iterateScopes(currentScope, fn) { 474 | if (typeof currentScope === 'function') { 475 | fn = currentScope; 476 | currentScope = null; 477 | } 478 | currentScope = currentScope || getRootScope(); 479 | currentScope = _makeScopeReference(currentScope); 480 | if (!currentScope) { 481 | return; 482 | } 483 | var ret = fn(currentScope); 484 | if (ret === false) { 485 | return ret; 486 | } 487 | return iterateChildren(currentScope, fn); 488 | } 489 | 490 | function iterateSiblings(start, fn) { 491 | var ret; 492 | /* eslint no-extra-boolean-cast:0 */ 493 | while (!!(start = start.$$nextSibling)) { 494 | ret = fn(start); 495 | if (ret === false) { 496 | break; 497 | } 498 | 499 | ret = iterateChildren(start, fn); 500 | if (ret === false) { 501 | break; 502 | } 503 | } 504 | return ret; 505 | } 506 | 507 | function iterateChildren(start, fn) { 508 | var ret; 509 | while (!!(start = start.$$childHead)) { 510 | ret = fn(start); 511 | if (ret === false) { 512 | break; 513 | } 514 | 515 | ret = iterateSiblings(start, fn); 516 | if (ret === false) { 517 | break; 518 | } 519 | } 520 | return ret; 521 | } 522 | 523 | 524 | function getScopeById(id) { 525 | var myScope = null; 526 | iterateScopes(function(scope) { 527 | if (scope.$id === id) { 528 | myScope = scope; 529 | return false; 530 | } 531 | }); 532 | return myScope; 533 | } 534 | 535 | function _makeScopeReference(scope) { 536 | if (_isScopeId(scope)) { 537 | scope = getScopeById(scope); 538 | } 539 | return scope; 540 | } 541 | 542 | function _isScopeId(scope) { 543 | return typeof scope === 'string' || typeof scope === 'number'; 544 | } 545 | -------------------------------------------------------------------------------- /dist/ng-stats.js: -------------------------------------------------------------------------------- 1 | //! ng-stats version 2.5.4 built with ♥ by Kent C. Dodds (http://kent.doddsfamily.us), Viper Bailey (http://jinxidoru.blogspot.com), Daniel Lamb (http://daniellmb.com) (ó ì_í)=óò=(ì_í ò) 2 | 3 | (function webpackUniversalModuleDefinition(root, factory) { 4 | if(typeof exports === 'object' && typeof module === 'object') 5 | module.exports = factory(require("angular")); 6 | else if(typeof define === 'function' && define.amd) 7 | define(["angular"], factory); 8 | else if(typeof exports === 'object') 9 | exports["showAngularStats"] = factory(require("angular")); 10 | else 11 | root["showAngularStats"] = factory(root["angular"]); 12 | })(this, function(__WEBPACK_EXTERNAL_MODULE_1__) { 13 | return /******/ (function(modules) { // webpackBootstrap 14 | /******/ // The module cache 15 | /******/ var installedModules = {}; 16 | 17 | /******/ // The require function 18 | /******/ function __webpack_require__(moduleId) { 19 | 20 | /******/ // Check if module is in cache 21 | /******/ if(installedModules[moduleId]) 22 | /******/ return installedModules[moduleId].exports; 23 | 24 | /******/ // Create a new module (and put it into the cache) 25 | /******/ var module = installedModules[moduleId] = { 26 | /******/ exports: {}, 27 | /******/ id: moduleId, 28 | /******/ loaded: false 29 | /******/ }; 30 | 31 | /******/ // Execute the module function 32 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 33 | 34 | /******/ // Flag the module as loaded 35 | /******/ module.loaded = true; 36 | 37 | /******/ // Return the exports of the module 38 | /******/ return module.exports; 39 | /******/ } 40 | 41 | 42 | /******/ // expose the modules object (__webpack_modules__) 43 | /******/ __webpack_require__.m = modules; 44 | 45 | /******/ // expose the module cache 46 | /******/ __webpack_require__.c = installedModules; 47 | 48 | /******/ // __webpack_public_path__ 49 | /******/ __webpack_require__.p = ""; 50 | 51 | /******/ // Load entry module and return exports 52 | /******/ return __webpack_require__(0); 53 | /******/ }) 54 | /************************************************************************/ 55 | /******/ ([ 56 | /* 0 */ 57 | /***/ function(module, exports, __webpack_require__) { 58 | 59 | /* eslint no-console:0 */ 60 | 'use strict'; 61 | 62 | Object.defineProperty(exports, '__esModule', { 63 | value: true 64 | }); 65 | 66 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 67 | 68 | var _angular = __webpack_require__(1); 69 | 70 | var _angular2 = _interopRequireDefault(_angular); 71 | 72 | var angular = _angular2['default']; 73 | 74 | /* istanbul ignore next */ 75 | if (!angular.version) { 76 | // we're doing this because some versions 77 | // of angular don't expose itself correctly 78 | angular = window.angular; 79 | } 80 | 81 | exports['default'] = showAngularStats; 82 | 83 | var autoloadKey = 'showAngularStats_autoload'; 84 | var current = null; 85 | // define the timer function to use based upon whether or not 'performance is available' 86 | var timerNow = window.self.performance && window.self.performance.now ? function () { 87 | return window.self.performance.now(); 88 | } : function () { 89 | return Date.now(); 90 | }; 91 | 92 | var lastWatchCountRun = timerNow(); 93 | var watchCountTimeout = null; 94 | var lastWatchCount = getWatcherCount() || 0; 95 | var lastDigestLength = 0; 96 | var scopeSelectors = '.ng-scope, .ng-isolate-scope'; 97 | var $rootScope; 98 | 99 | var digestIsHijacked = false; 100 | 101 | var listeners = { 102 | watchCount: {}, 103 | digestLength: {} 104 | }; 105 | 106 | // Hijack $digest to time it and update data on every digest. 107 | function hijackDigest() { 108 | if (digestIsHijacked) { 109 | return; 110 | } 111 | digestIsHijacked = true; 112 | var scopePrototype = Object.getPrototypeOf(getRootScope()); 113 | var oldDigest = scopePrototype.$digest; 114 | scopePrototype.$digest = function $digest() { 115 | var start = timerNow(); 116 | oldDigest.apply(this, arguments); 117 | var diff = timerNow() - start; 118 | updateData(getWatcherCount(), diff); 119 | }; 120 | } 121 | 122 | // used to prevent localstorage error in chrome packaged apps 123 | function isChromeApp() { 124 | return typeof chrome !== 'undefined' && typeof chrome.storage !== 'undefined' && typeof chrome.storage.local !== 'undefined'; 125 | } 126 | 127 | // check for autoload 128 | var autoloadOptions = sessionStorage[autoloadKey] || !isChromeApp() && localStorage[autoloadKey]; 129 | if (autoloadOptions) { 130 | autoload(JSON.parse(autoloadOptions)); 131 | } 132 | 133 | function autoload(options) { 134 | if (window.self.angular && getRootScope()) { 135 | showAngularStats(options); 136 | } else { 137 | // wait for angular to load... 138 | setTimeout(function () { 139 | autoload(options); 140 | }, 200); 141 | } 142 | } 143 | 144 | function initOptions(opts) { 145 | 146 | // Remove autoload if they did not specifically request it 147 | if (opts === false || !opts.autoload) { 148 | sessionStorage.removeItem(autoloadKey); 149 | localStorage.removeItem(autoloadKey); 150 | // do nothing if the argument is false 151 | if (opts === false) { 152 | return; 153 | } 154 | } 155 | 156 | opts.position = opts.position || 'top-left'; 157 | opts = angular.extend({ 158 | htmlId: null, 159 | rootScope: undefined, 160 | digestTimeThreshold: 16, 161 | watchCountThreshold: 2000, 162 | autoload: false, 163 | trackDigest: false, 164 | trackWatches: false, 165 | logDigest: false, 166 | logWatches: false, 167 | styles: { 168 | position: 'fixed', 169 | background: 'black', 170 | borderBottom: '1px solid #666', 171 | borderRight: '1px solid #666', 172 | color: '#666', 173 | fontFamily: 'Courier', 174 | width: 130, 175 | zIndex: 9999, 176 | textAlign: 'right', 177 | top: opts.position.indexOf('top') === -1 ? null : 0, 178 | bottom: opts.position.indexOf('bottom') === -1 ? null : 0, 179 | right: opts.position.indexOf('right') === -1 ? null : 0, 180 | left: opts.position.indexOf('left') === -1 ? null : 0 181 | } 182 | }, opts || {}); 183 | 184 | // for ionic support 185 | if (opts.rootScope) { 186 | $rootScope = opts.rootScope; 187 | } 188 | 189 | return opts; 190 | } 191 | 192 | function showAngularStats(opts) { 193 | /* eslint max-statements:[2, 45] */ 194 | /* eslint complexity:[2, 18] */ 195 | /* eslint consistent-return:0 */ 196 | // TODO ^^ fix these things... 197 | opts = opts !== undefined ? opts : {}; 198 | var returnData = { 199 | listeners: listeners 200 | }; 201 | // delete the previous one 202 | if (current) { 203 | current.$el && current.$el.remove(); 204 | current.active = false; 205 | current = null; 206 | } 207 | 208 | // Implemented in separate function due to webpack's statement count limit 209 | opts = initOptions(opts); 210 | 211 | if (!opts) { 212 | return; 213 | } 214 | 215 | hijackDigest(); 216 | 217 | // setup the state 218 | var state = current = { active: true }; 219 | 220 | // auto-load on startup 221 | if (opts.autoload) { 222 | if (opts.autoload === 'localStorage') { 223 | localStorage.setItem(autoloadKey, JSON.stringify(opts)); 224 | } else if (opts.autoload === 'sessionStorage' || typeof opts.autoload === 'boolean') { 225 | sessionStorage.setItem(autoloadKey, JSON.stringify(opts)); 226 | } else { 227 | throw new Error('Invalid value for autoload: ' + opts.autoload + ' can only be "localStorage" "sessionStorage" or boolean.'); 228 | } 229 | } 230 | 231 | // general variables 232 | var bodyEl = angular.element(document.body); 233 | var noDigestSteps = 0; 234 | 235 | // add the DOM element 236 | var htmlId = opts.htmlId ? ' id="' + opts.htmlId + '"' : ''; 237 | state.$el = angular.element('
|
').css(opts.styles); 238 | bodyEl.append(state.$el); 239 | var $watchCount = state.$el.find('span'); 240 | var $digestTime = $watchCount.next(); 241 | 242 | // initialize the canvas 243 | var graphSz = { width: 130, height: 40 }; 244 | var cvs = state.$el.find('canvas').attr(graphSz)[0]; 245 | 246 | // add listeners 247 | listeners.digestLength.ngStatsAddToCanvas = function (digestLength) { 248 | addDataToCanvas(null, digestLength); 249 | }; 250 | 251 | listeners.watchCount.ngStatsAddToCanvas = function (watchCount) { 252 | addDataToCanvas(watchCount); 253 | }; 254 | 255 | track('digest', listeners.digestLength); 256 | track('watches', listeners.watchCount, true); 257 | 258 | log('digest', listeners.digestLength); 259 | log('watches', listeners.watchCount, true); 260 | 261 | function track(thingToTrack, listenerCollection, diffOnly) { 262 | var capThingToTrack = thingToTrack.charAt(0).toUpperCase() + thingToTrack.slice(1); 263 | if (opts['track' + capThingToTrack]) { 264 | returnData[thingToTrack] = []; 265 | listenerCollection['track + capThingToTrack'] = function (tracked) { 266 | if (!diffOnly || returnData[thingToTrack][returnData.length - 1] !== tracked) { 267 | returnData[thingToTrack][returnData.length - 1] = tracked; 268 | returnData[thingToTrack].push(tracked); 269 | } 270 | }; 271 | } 272 | } 273 | 274 | function log(thingToLog, listenerCollection, diffOnly) { 275 | var capThingToLog = thingToLog.charAt(0).toUpperCase() + thingToLog.slice(1); 276 | if (opts['log' + capThingToLog]) { 277 | var last; 278 | listenerCollection['log' + capThingToLog] = function (tracked) { 279 | if (!diffOnly || last !== tracked) { 280 | last = tracked; 281 | var color = colorLog(thingToLog, tracked); 282 | if (color) { 283 | console.log('%c' + thingToLog + ':', color, tracked); 284 | } else { 285 | console.log(thingToLog + ':', tracked); 286 | } 287 | } 288 | }; 289 | } 290 | } 291 | 292 | function getColor(metric, threshold) { 293 | if (metric > threshold) { 294 | return 'red'; 295 | } else if (metric > 0.7 * threshold) { 296 | return 'orange'; 297 | } 298 | return 'green'; 299 | } 300 | 301 | function colorLog(thingToLog, tracked) { 302 | var color; 303 | if (thingToLog === 'digest') { 304 | color = 'color:' + getColor(tracked, opts.digestTimeThreshold); 305 | } else if (thingToLog === 'watches') { 306 | color = 'color:' + getColor(tracked, opts.watchCountThreshold); 307 | } 308 | return color; 309 | } 310 | 311 | function addDataToCanvas(watchCount, digestLength) { 312 | var averageDigest = digestLength || lastDigestLength; 313 | var digestColor = getColor(averageDigest, opts.digestTimeThreshold); 314 | lastWatchCount = nullOrUndef(watchCount) ? lastWatchCount : watchCount; 315 | var watchColor = getColor(lastWatchCount, opts.watchCountThreshold); 316 | lastDigestLength = nullOrUndef(digestLength) ? lastDigestLength : digestLength; 317 | $watchCount.text(lastWatchCount).css({ color: watchColor }); 318 | $digestTime.text(lastDigestLength.toFixed(2)).css({ color: digestColor }); 319 | 320 | if (!digestLength) { 321 | return; 322 | } 323 | 324 | // color the sliver if this is the first step 325 | var ctx = cvs.getContext('2d'); 326 | if (noDigestSteps > 0) { 327 | noDigestSteps = 0; 328 | ctx.fillStyle = '#333'; 329 | ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height); 330 | } 331 | 332 | // mark the point on the graph 333 | ctx.fillStyle = digestColor; 334 | ctx.fillRect(graphSz.width - 1, Math.max(0, graphSz.height - averageDigest), 2, 2); 335 | } 336 | 337 | // Shift the canvas to the left. 338 | function shiftLeft() { 339 | if (state.active) { 340 | setTimeout(shiftLeft, 250); 341 | var ctx = cvs.getContext('2d'); 342 | var imageData = ctx.getImageData(1, 0, graphSz.width - 1, graphSz.height); 343 | ctx.putImageData(imageData, 0, 0); 344 | ctx.fillStyle = noDigestSteps++ > 2 ? 'black' : '#333'; 345 | ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height); 346 | } 347 | } 348 | 349 | // start everything 350 | shiftLeft(); 351 | if (!$rootScope.$$phase) { 352 | $rootScope.$digest(); 353 | } 354 | 355 | return returnData; 356 | } 357 | 358 | angular.module('angularStats', []).directive('angularStats', function () { 359 | var index = 1; 360 | return { 361 | scope: { 362 | digestLength: '@', 363 | watchCount: '@', 364 | watchCountRoot: '@', 365 | onDigestLengthUpdate: '&?', 366 | onWatchCountUpdate: '&?' 367 | }, 368 | link: function link(scope, el, attrs) { 369 | hijackDigest(); 370 | var directiveIndex = index++; 371 | 372 | setupDigestLengthElement(); 373 | setupWatchCountElement(); 374 | addWatchCountListener(); 375 | addDigestLengthListener(); 376 | scope.$on('$destroy', destroyListeners); 377 | 378 | function setupDigestLengthElement() { 379 | if (attrs.hasOwnProperty('digestLength')) { 380 | var digestEl = el; 381 | if (attrs.digestLength) { 382 | digestEl = angular.element(el[0].querySelector(attrs.digestLength)); 383 | } 384 | listeners.digestLength['ngStatsDirective' + directiveIndex] = function (length) { 385 | window.dirDigestNode = digestEl[0]; 386 | digestEl.text((length || 0).toFixed(2)); 387 | }; 388 | } 389 | } 390 | 391 | function setupWatchCountElement() { 392 | if (attrs.hasOwnProperty('watchCount')) { 393 | var watchCountRoot; 394 | var watchCountEl = el; 395 | if (scope.watchCount) { 396 | watchCountEl = angular.element(el[0].querySelector(attrs.watchCount)); 397 | } 398 | 399 | if (scope.watchCountRoot) { 400 | if (scope.watchCountRoot === 'this') { 401 | watchCountRoot = el; 402 | } else { 403 | // In the case this directive is being compiled and it's not in the dom, 404 | // we're going to do the find from the root of what we have... 405 | var rootParent; 406 | if (attrs.hasOwnProperty('watchCountOfChild')) { 407 | rootParent = el[0]; 408 | } else { 409 | rootParent = findRootOfElement(el); 410 | } 411 | watchCountRoot = angular.element(rootParent.querySelector(scope.watchCountRoot)); 412 | if (!watchCountRoot.length) { 413 | throw new Error('no element at selector: ' + scope.watchCountRoot); 414 | } 415 | } 416 | } 417 | 418 | listeners.watchCount['ngStatsDirective' + directiveIndex] = function (count) { 419 | var watchCount = count; 420 | if (watchCountRoot) { 421 | watchCount = getWatcherCountForElement(watchCountRoot); 422 | } 423 | watchCountEl.text(watchCount); 424 | }; 425 | } 426 | } 427 | 428 | function addWatchCountListener() { 429 | if (attrs.hasOwnProperty('onWatchCountUpdate')) { 430 | listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex] = function (count) { 431 | scope.onWatchCountUpdate({ watchCount: count }); 432 | }; 433 | } 434 | } 435 | 436 | function addDigestLengthListener() { 437 | if (attrs.hasOwnProperty('onDigestLengthUpdate')) { 438 | listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex] = function (length) { 439 | scope.onDigestLengthUpdate({ digestLength: length }); 440 | }; 441 | } 442 | } 443 | 444 | function destroyListeners() { 445 | delete listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex]; 446 | delete listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex]; 447 | delete listeners.digestLength['ngStatsDirective' + directiveIndex]; 448 | delete listeners.watchCount['ngStatsDirective' + directiveIndex]; 449 | } 450 | } 451 | }; 452 | 453 | function findRootOfElement(el) { 454 | var parent = el[0]; 455 | while (parent.parentElement) { 456 | parent = parent.parentElement; 457 | } 458 | return parent; 459 | } 460 | }); 461 | 462 | // UTILITY FUNCTIONS 463 | 464 | function getRootScope() { 465 | if ($rootScope) { 466 | return $rootScope; 467 | } 468 | var scopeEl = document.querySelector(scopeSelectors); 469 | if (!scopeEl) { 470 | return null; 471 | } 472 | $rootScope = angular.element(scopeEl).scope().$root; 473 | return $rootScope; 474 | } 475 | 476 | // Uses timeouts to ensure that this is only run every 300ms (it's a perf bottleneck) 477 | function getWatcherCount() { 478 | clearTimeout(watchCountTimeout); 479 | var now = timerNow(); 480 | if (now - lastWatchCountRun > 300) { 481 | lastWatchCountRun = now; 482 | lastWatchCount = getWatcherCountForScope(); 483 | } else { 484 | watchCountTimeout = setTimeout(function () { 485 | updateData(getWatcherCount()); 486 | }, 350); 487 | } 488 | return lastWatchCount; 489 | } 490 | 491 | function getWatcherCountForElement(element) { 492 | var startingScope = getClosestChildScope(element); 493 | return getWatcherCountForScope(startingScope); 494 | } 495 | 496 | function getClosestChildScope(element) { 497 | element = angular.element(element); 498 | var scope = element.scope(); 499 | if (!scope) { 500 | element = angular.element(element.querySelector(scopeSelectors)); 501 | scope = element.scope(); 502 | } 503 | return scope; 504 | } 505 | 506 | function getWatchersFromScope(scope) { 507 | return scope && scope.$$watchers ? scope.$$watchers : []; 508 | } 509 | 510 | // iterate through listeners to call them with the watchCount and digestLength 511 | function updateData(watchCount, digestLength) { 512 | // update the listeners 513 | if (!nullOrUndef(watchCount)) { 514 | angular.forEach(listeners.watchCount, function (listener) { 515 | listener(watchCount); 516 | }); 517 | } 518 | if (!nullOrUndef(digestLength)) { 519 | angular.forEach(listeners.digestLength, function (listener) { 520 | listener(digestLength); 521 | }); 522 | } 523 | } 524 | 525 | function nullOrUndef(item) { 526 | return item === null || item === undefined; 527 | } 528 | 529 | function getWatcherCountForScope(scope) { 530 | var count = 0; 531 | iterateScopes(scope, function (childScope) { 532 | count += getWatchersFromScope(childScope).length; 533 | }); 534 | return count; 535 | } 536 | 537 | function iterateScopes(currentScope, fn) { 538 | if (typeof currentScope === 'function') { 539 | fn = currentScope; 540 | currentScope = null; 541 | } 542 | currentScope = currentScope || getRootScope(); 543 | currentScope = _makeScopeReference(currentScope); 544 | if (!currentScope) { 545 | return; 546 | } 547 | var ret = fn(currentScope); 548 | if (ret === false) { 549 | return ret; 550 | } 551 | return iterateChildren(currentScope, fn); 552 | } 553 | 554 | function iterateSiblings(start, fn) { 555 | var ret; 556 | /* eslint no-extra-boolean-cast:0 */ 557 | while (!!(start = start.$$nextSibling)) { 558 | ret = fn(start); 559 | if (ret === false) { 560 | break; 561 | } 562 | 563 | ret = iterateChildren(start, fn); 564 | if (ret === false) { 565 | break; 566 | } 567 | } 568 | return ret; 569 | } 570 | 571 | function iterateChildren(start, fn) { 572 | var ret; 573 | while (!!(start = start.$$childHead)) { 574 | ret = fn(start); 575 | if (ret === false) { 576 | break; 577 | } 578 | 579 | ret = iterateSiblings(start, fn); 580 | if (ret === false) { 581 | break; 582 | } 583 | } 584 | return ret; 585 | } 586 | 587 | function getScopeById(id) { 588 | var myScope = null; 589 | iterateScopes(function (scope) { 590 | if (scope.$id === id) { 591 | myScope = scope; 592 | return false; 593 | } 594 | }); 595 | return myScope; 596 | } 597 | 598 | function _makeScopeReference(scope) { 599 | if (_isScopeId(scope)) { 600 | scope = getScopeById(scope); 601 | } 602 | return scope; 603 | } 604 | 605 | function _isScopeId(scope) { 606 | return typeof scope === 'string' || typeof scope === 'number'; 607 | } 608 | module.exports = exports['default']; 609 | 610 | /***/ }, 611 | /* 1 */ 612 | /***/ function(module, exports) { 613 | 614 | module.exports = __WEBPACK_EXTERNAL_MODULE_1__; 615 | 616 | /***/ } 617 | /******/ ]) 618 | }); 619 | ; -------------------------------------------------------------------------------- /dist/ng-stats.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/universalModuleDefinition","webpack:///dist/ng-stats.min.js","webpack:///webpack/bootstrap 17fe47d2f2434b59ad2e","webpack:///./index.js","webpack:///external \"angular\""],"names":["root","factory","exports","module","require","define","amd","this","__WEBPACK_EXTERNAL_MODULE_1__","modules","__webpack_require__","moduleId","installedModules","id","loaded","call","m","c","p","_interopRequireDefault","obj","__esModule","default","hijackDigest","digestIsHijacked","scopePrototype","Object","getPrototypeOf","getRootScope","oldDigest","$digest","start","timerNow","apply","arguments","diff","updateData","getWatcherCount","isChromeApp","chrome","storage","local","autoload","options","window","self","angular","showAngularStats","setTimeout","initOptions","opts","sessionStorage","removeItem","autoloadKey","localStorage","position","extend","htmlId","rootScope","undefined","digestTimeThreshold","watchCountThreshold","trackDigest","trackWatches","logDigest","logWatches","styles","background","borderBottom","borderRight","color","fontFamily","width","zIndex","textAlign","top","indexOf","bottom","right","left","$rootScope","track","thingToTrack","listenerCollection","diffOnly","capThingToTrack","charAt","toUpperCase","slice","returnData","tracked","length","push","log","thingToLog","capThingToLog","last","colorLog","console","getColor","metric","threshold","addDataToCanvas","watchCount","digestLength","averageDigest","lastDigestLength","digestColor","lastWatchCount","nullOrUndef","watchColor","$watchCount","text","css","$digestTime","toFixed","ctx","cvs","getContext","noDigestSteps","fillStyle","fillRect","graphSz","height","Math","max","shiftLeft","state","active","imageData","getImageData","putImageData","listeners","current","$el","remove","setItem","JSON","stringify","Error","bodyEl","element","document","body","append","find","next","attr","ngStatsAddToCanvas","$$phase","scopeEl","querySelector","scopeSelectors","scope","$root","clearTimeout","watchCountTimeout","now","lastWatchCountRun","getWatcherCountForScope","getWatcherCountForElement","startingScope","getClosestChildScope","getWatchersFromScope","$$watchers","forEach","listener","item","count","iterateScopes","childScope","currentScope","fn","_makeScopeReference","ret","iterateChildren","iterateSiblings","$$nextSibling","$$childHead","getScopeById","myScope","$id","_isScopeId","defineProperty","value","_angular","_angular2","version","performance","Date","autoloadOptions","parse","directive","findRootOfElement","el","parent","parentElement","index","watchCountRoot","onDigestLengthUpdate","onWatchCountUpdate","link","attrs","setupDigestLengthElement","hasOwnProperty","digestEl","directiveIndex","dirDigestNode","setupWatchCountElement","watchCountEl","rootParent","addWatchCountListener","addDigestLengthListener","destroyListeners","$on"],"mappings":"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","file":"dist/ng-stats.min.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"angular\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"angular\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"showAngularStats\"] = factory(require(\"angular\"));\n\telse\n\t\troot[\"showAngularStats\"] = factory(root[\"angular\"]);\n})(this, function(__WEBPACK_EXTERNAL_MODULE_1__) {\nreturn \n\n\n/** WEBPACK FOOTER **\n ** webpack/universalModuleDefinition\n **/","//! ng-stats version 2.5.4 built with ♥ by Kent C. Dodds (http://kent.doddsfamily.us), Viper Bailey (http://jinxidoru.blogspot.com), Daniel Lamb (http://daniellmb.com) (ó ì_í)=óò=(ì_í ò)\n\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"angular\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"angular\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"showAngularStats\"] = factory(require(\"angular\"));\n\telse\n\t\troot[\"showAngularStats\"] = factory(root[\"angular\"]);\n})(this, function(__WEBPACK_EXTERNAL_MODULE_1__) {\nreturn /******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId])\n/******/ \t\t\treturn installedModules[moduleId].exports;\n/******/\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\texports: {},\n/******/ \t\t\tid: moduleId,\n/******/ \t\t\tloaded: false\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.loaded = true;\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/******/\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n/******/\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"\";\n/******/\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(0);\n/******/ })\n/************************************************************************/\n/******/ ([\n/* 0 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t/* eslint no-console:0 */\n\t'use strict';\n\t\n\tObject.defineProperty(exports, '__esModule', {\n\t value: true\n\t});\n\t\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\t\n\tvar _angular = __webpack_require__(1);\n\t\n\tvar _angular2 = _interopRequireDefault(_angular);\n\t\n\tvar angular = _angular2['default'];\n\t\n\t/* istanbul ignore next */\n\tif (!angular.version) {\n\t // we're doing this because some versions\n\t // of angular don't expose itself correctly\n\t angular = window.angular;\n\t}\n\t\n\texports['default'] = showAngularStats;\n\t\n\tvar autoloadKey = 'showAngularStats_autoload';\n\tvar current = null;\n\t// define the timer function to use based upon whether or not 'performance is available'\n\tvar timerNow = window.self.performance && window.self.performance.now ? function () {\n\t return window.self.performance.now();\n\t} : function () {\n\t return Date.now();\n\t};\n\t\n\tvar lastWatchCountRun = timerNow();\n\tvar watchCountTimeout = null;\n\tvar lastWatchCount = getWatcherCount() || 0;\n\tvar lastDigestLength = 0;\n\tvar scopeSelectors = '.ng-scope, .ng-isolate-scope';\n\tvar $rootScope;\n\t\n\tvar digestIsHijacked = false;\n\t\n\tvar listeners = {\n\t watchCount: {},\n\t digestLength: {}\n\t};\n\t\n\t// Hijack $digest to time it and update data on every digest.\n\tfunction hijackDigest() {\n\t if (digestIsHijacked) {\n\t return;\n\t }\n\t digestIsHijacked = true;\n\t var scopePrototype = Object.getPrototypeOf(getRootScope());\n\t var oldDigest = scopePrototype.$digest;\n\t scopePrototype.$digest = function $digest() {\n\t var start = timerNow();\n\t oldDigest.apply(this, arguments);\n\t var diff = timerNow() - start;\n\t updateData(getWatcherCount(), diff);\n\t };\n\t}\n\t\n\t// used to prevent localstorage error in chrome packaged apps\n\tfunction isChromeApp() {\n\t return typeof chrome !== 'undefined' && typeof chrome.storage !== 'undefined' && typeof chrome.storage.local !== 'undefined';\n\t}\n\t\n\t// check for autoload\n\tvar autoloadOptions = sessionStorage[autoloadKey] || !isChromeApp() && localStorage[autoloadKey];\n\tif (autoloadOptions) {\n\t autoload(JSON.parse(autoloadOptions));\n\t}\n\t\n\tfunction autoload(options) {\n\t if (window.self.angular && getRootScope()) {\n\t showAngularStats(options);\n\t } else {\n\t // wait for angular to load...\n\t setTimeout(function () {\n\t autoload(options);\n\t }, 200);\n\t }\n\t}\n\t\n\tfunction initOptions(opts) {\n\t\n\t // Remove autoload if they did not specifically request it\n\t if (opts === false || !opts.autoload) {\n\t sessionStorage.removeItem(autoloadKey);\n\t localStorage.removeItem(autoloadKey);\n\t // do nothing if the argument is false\n\t if (opts === false) {\n\t return;\n\t }\n\t }\n\t\n\t opts.position = opts.position || 'top-left';\n\t opts = angular.extend({\n\t htmlId: null,\n\t rootScope: undefined,\n\t digestTimeThreshold: 16,\n\t watchCountThreshold: 2000,\n\t autoload: false,\n\t trackDigest: false,\n\t trackWatches: false,\n\t logDigest: false,\n\t logWatches: false,\n\t styles: {\n\t position: 'fixed',\n\t background: 'black',\n\t borderBottom: '1px solid #666',\n\t borderRight: '1px solid #666',\n\t color: '#666',\n\t fontFamily: 'Courier',\n\t width: 130,\n\t zIndex: 9999,\n\t textAlign: 'right',\n\t top: opts.position.indexOf('top') === -1 ? null : 0,\n\t bottom: opts.position.indexOf('bottom') === -1 ? null : 0,\n\t right: opts.position.indexOf('right') === -1 ? null : 0,\n\t left: opts.position.indexOf('left') === -1 ? null : 0\n\t }\n\t }, opts || {});\n\t\n\t // for ionic support\n\t if (opts.rootScope) {\n\t $rootScope = opts.rootScope;\n\t }\n\t\n\t return opts;\n\t}\n\t\n\tfunction showAngularStats(opts) {\n\t /* eslint max-statements:[2, 45] */\n\t /* eslint complexity:[2, 18] */\n\t /* eslint consistent-return:0 */\n\t // TODO ^^ fix these things...\n\t opts = opts !== undefined ? opts : {};\n\t var returnData = {\n\t listeners: listeners\n\t };\n\t // delete the previous one\n\t if (current) {\n\t current.$el && current.$el.remove();\n\t current.active = false;\n\t current = null;\n\t }\n\t\n\t // Implemented in separate function due to webpack's statement count limit\n\t opts = initOptions(opts);\n\t\n\t if (!opts) {\n\t return;\n\t }\n\t\n\t hijackDigest();\n\t\n\t // setup the state\n\t var state = current = { active: true };\n\t\n\t // auto-load on startup\n\t if (opts.autoload) {\n\t if (opts.autoload === 'localStorage') {\n\t localStorage.setItem(autoloadKey, JSON.stringify(opts));\n\t } else if (opts.autoload === 'sessionStorage' || typeof opts.autoload === 'boolean') {\n\t sessionStorage.setItem(autoloadKey, JSON.stringify(opts));\n\t } else {\n\t throw new Error('Invalid value for autoload: ' + opts.autoload + ' can only be \"localStorage\" \"sessionStorage\" or boolean.');\n\t }\n\t }\n\t\n\t // general variables\n\t var bodyEl = angular.element(document.body);\n\t var noDigestSteps = 0;\n\t\n\t // add the DOM element\n\t var htmlId = opts.htmlId ? ' id=\"' + opts.htmlId + '\"' : '';\n\t state.$el = angular.element('
|
').css(opts.styles);\n\t bodyEl.append(state.$el);\n\t var $watchCount = state.$el.find('span');\n\t var $digestTime = $watchCount.next();\n\t\n\t // initialize the canvas\n\t var graphSz = { width: 130, height: 40 };\n\t var cvs = state.$el.find('canvas').attr(graphSz)[0];\n\t\n\t // add listeners\n\t listeners.digestLength.ngStatsAddToCanvas = function (digestLength) {\n\t addDataToCanvas(null, digestLength);\n\t };\n\t\n\t listeners.watchCount.ngStatsAddToCanvas = function (watchCount) {\n\t addDataToCanvas(watchCount);\n\t };\n\t\n\t track('digest', listeners.digestLength);\n\t track('watches', listeners.watchCount, true);\n\t\n\t log('digest', listeners.digestLength);\n\t log('watches', listeners.watchCount, true);\n\t\n\t function track(thingToTrack, listenerCollection, diffOnly) {\n\t var capThingToTrack = thingToTrack.charAt(0).toUpperCase() + thingToTrack.slice(1);\n\t if (opts['track' + capThingToTrack]) {\n\t returnData[thingToTrack] = [];\n\t listenerCollection['track + capThingToTrack'] = function (tracked) {\n\t if (!diffOnly || returnData[thingToTrack][returnData.length - 1] !== tracked) {\n\t returnData[thingToTrack][returnData.length - 1] = tracked;\n\t returnData[thingToTrack].push(tracked);\n\t }\n\t };\n\t }\n\t }\n\t\n\t function log(thingToLog, listenerCollection, diffOnly) {\n\t var capThingToLog = thingToLog.charAt(0).toUpperCase() + thingToLog.slice(1);\n\t if (opts['log' + capThingToLog]) {\n\t var last;\n\t listenerCollection['log' + capThingToLog] = function (tracked) {\n\t if (!diffOnly || last !== tracked) {\n\t last = tracked;\n\t var color = colorLog(thingToLog, tracked);\n\t if (color) {\n\t console.log('%c' + thingToLog + ':', color, tracked);\n\t } else {\n\t console.log(thingToLog + ':', tracked);\n\t }\n\t }\n\t };\n\t }\n\t }\n\t\n\t function getColor(metric, threshold) {\n\t if (metric > threshold) {\n\t return 'red';\n\t } else if (metric > 0.7 * threshold) {\n\t return 'orange';\n\t }\n\t return 'green';\n\t }\n\t\n\t function colorLog(thingToLog, tracked) {\n\t var color;\n\t if (thingToLog === 'digest') {\n\t color = 'color:' + getColor(tracked, opts.digestTimeThreshold);\n\t } else if (thingToLog === 'watches') {\n\t color = 'color:' + getColor(tracked, opts.watchCountThreshold);\n\t }\n\t return color;\n\t }\n\t\n\t function addDataToCanvas(watchCount, digestLength) {\n\t var averageDigest = digestLength || lastDigestLength;\n\t var digestColor = getColor(averageDigest, opts.digestTimeThreshold);\n\t lastWatchCount = nullOrUndef(watchCount) ? lastWatchCount : watchCount;\n\t var watchColor = getColor(lastWatchCount, opts.watchCountThreshold);\n\t lastDigestLength = nullOrUndef(digestLength) ? lastDigestLength : digestLength;\n\t $watchCount.text(lastWatchCount).css({ color: watchColor });\n\t $digestTime.text(lastDigestLength.toFixed(2)).css({ color: digestColor });\n\t\n\t if (!digestLength) {\n\t return;\n\t }\n\t\n\t // color the sliver if this is the first step\n\t var ctx = cvs.getContext('2d');\n\t if (noDigestSteps > 0) {\n\t noDigestSteps = 0;\n\t ctx.fillStyle = '#333';\n\t ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height);\n\t }\n\t\n\t // mark the point on the graph\n\t ctx.fillStyle = digestColor;\n\t ctx.fillRect(graphSz.width - 1, Math.max(0, graphSz.height - averageDigest), 2, 2);\n\t }\n\t\n\t // Shift the canvas to the left.\n\t function shiftLeft() {\n\t if (state.active) {\n\t setTimeout(shiftLeft, 250);\n\t var ctx = cvs.getContext('2d');\n\t var imageData = ctx.getImageData(1, 0, graphSz.width - 1, graphSz.height);\n\t ctx.putImageData(imageData, 0, 0);\n\t ctx.fillStyle = noDigestSteps++ > 2 ? 'black' : '#333';\n\t ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height);\n\t }\n\t }\n\t\n\t // start everything\n\t shiftLeft();\n\t if (!$rootScope.$$phase) {\n\t $rootScope.$digest();\n\t }\n\t\n\t return returnData;\n\t}\n\t\n\tangular.module('angularStats', []).directive('angularStats', function () {\n\t var index = 1;\n\t return {\n\t scope: {\n\t digestLength: '@',\n\t watchCount: '@',\n\t watchCountRoot: '@',\n\t onDigestLengthUpdate: '&?',\n\t onWatchCountUpdate: '&?'\n\t },\n\t link: function link(scope, el, attrs) {\n\t hijackDigest();\n\t var directiveIndex = index++;\n\t\n\t setupDigestLengthElement();\n\t setupWatchCountElement();\n\t addWatchCountListener();\n\t addDigestLengthListener();\n\t scope.$on('$destroy', destroyListeners);\n\t\n\t function setupDigestLengthElement() {\n\t if (attrs.hasOwnProperty('digestLength')) {\n\t var digestEl = el;\n\t if (attrs.digestLength) {\n\t digestEl = angular.element(el[0].querySelector(attrs.digestLength));\n\t }\n\t listeners.digestLength['ngStatsDirective' + directiveIndex] = function (length) {\n\t window.dirDigestNode = digestEl[0];\n\t digestEl.text((length || 0).toFixed(2));\n\t };\n\t }\n\t }\n\t\n\t function setupWatchCountElement() {\n\t if (attrs.hasOwnProperty('watchCount')) {\n\t var watchCountRoot;\n\t var watchCountEl = el;\n\t if (scope.watchCount) {\n\t watchCountEl = angular.element(el[0].querySelector(attrs.watchCount));\n\t }\n\t\n\t if (scope.watchCountRoot) {\n\t if (scope.watchCountRoot === 'this') {\n\t watchCountRoot = el;\n\t } else {\n\t // In the case this directive is being compiled and it's not in the dom,\n\t // we're going to do the find from the root of what we have...\n\t var rootParent;\n\t if (attrs.hasOwnProperty('watchCountOfChild')) {\n\t rootParent = el[0];\n\t } else {\n\t rootParent = findRootOfElement(el);\n\t }\n\t watchCountRoot = angular.element(rootParent.querySelector(scope.watchCountRoot));\n\t if (!watchCountRoot.length) {\n\t throw new Error('no element at selector: ' + scope.watchCountRoot);\n\t }\n\t }\n\t }\n\t\n\t listeners.watchCount['ngStatsDirective' + directiveIndex] = function (count) {\n\t var watchCount = count;\n\t if (watchCountRoot) {\n\t watchCount = getWatcherCountForElement(watchCountRoot);\n\t }\n\t watchCountEl.text(watchCount);\n\t };\n\t }\n\t }\n\t\n\t function addWatchCountListener() {\n\t if (attrs.hasOwnProperty('onWatchCountUpdate')) {\n\t listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex] = function (count) {\n\t scope.onWatchCountUpdate({ watchCount: count });\n\t };\n\t }\n\t }\n\t\n\t function addDigestLengthListener() {\n\t if (attrs.hasOwnProperty('onDigestLengthUpdate')) {\n\t listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex] = function (length) {\n\t scope.onDigestLengthUpdate({ digestLength: length });\n\t };\n\t }\n\t }\n\t\n\t function destroyListeners() {\n\t delete listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex];\n\t delete listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex];\n\t delete listeners.digestLength['ngStatsDirective' + directiveIndex];\n\t delete listeners.watchCount['ngStatsDirective' + directiveIndex];\n\t }\n\t }\n\t };\n\t\n\t function findRootOfElement(el) {\n\t var parent = el[0];\n\t while (parent.parentElement) {\n\t parent = parent.parentElement;\n\t }\n\t return parent;\n\t }\n\t});\n\t\n\t// UTILITY FUNCTIONS\n\t\n\tfunction getRootScope() {\n\t if ($rootScope) {\n\t return $rootScope;\n\t }\n\t var scopeEl = document.querySelector(scopeSelectors);\n\t if (!scopeEl) {\n\t return null;\n\t }\n\t $rootScope = angular.element(scopeEl).scope().$root;\n\t return $rootScope;\n\t}\n\t\n\t// Uses timeouts to ensure that this is only run every 300ms (it's a perf bottleneck)\n\tfunction getWatcherCount() {\n\t clearTimeout(watchCountTimeout);\n\t var now = timerNow();\n\t if (now - lastWatchCountRun > 300) {\n\t lastWatchCountRun = now;\n\t lastWatchCount = getWatcherCountForScope();\n\t } else {\n\t watchCountTimeout = setTimeout(function () {\n\t updateData(getWatcherCount());\n\t }, 350);\n\t }\n\t return lastWatchCount;\n\t}\n\t\n\tfunction getWatcherCountForElement(element) {\n\t var startingScope = getClosestChildScope(element);\n\t return getWatcherCountForScope(startingScope);\n\t}\n\t\n\tfunction getClosestChildScope(element) {\n\t element = angular.element(element);\n\t var scope = element.scope();\n\t if (!scope) {\n\t element = angular.element(element.querySelector(scopeSelectors));\n\t scope = element.scope();\n\t }\n\t return scope;\n\t}\n\t\n\tfunction getWatchersFromScope(scope) {\n\t return scope && scope.$$watchers ? scope.$$watchers : [];\n\t}\n\t\n\t// iterate through listeners to call them with the watchCount and digestLength\n\tfunction updateData(watchCount, digestLength) {\n\t // update the listeners\n\t if (!nullOrUndef(watchCount)) {\n\t angular.forEach(listeners.watchCount, function (listener) {\n\t listener(watchCount);\n\t });\n\t }\n\t if (!nullOrUndef(digestLength)) {\n\t angular.forEach(listeners.digestLength, function (listener) {\n\t listener(digestLength);\n\t });\n\t }\n\t}\n\t\n\tfunction nullOrUndef(item) {\n\t return item === null || item === undefined;\n\t}\n\t\n\tfunction getWatcherCountForScope(scope) {\n\t var count = 0;\n\t iterateScopes(scope, function (childScope) {\n\t count += getWatchersFromScope(childScope).length;\n\t });\n\t return count;\n\t}\n\t\n\tfunction iterateScopes(currentScope, fn) {\n\t if (typeof currentScope === 'function') {\n\t fn = currentScope;\n\t currentScope = null;\n\t }\n\t currentScope = currentScope || getRootScope();\n\t currentScope = _makeScopeReference(currentScope);\n\t if (!currentScope) {\n\t return;\n\t }\n\t var ret = fn(currentScope);\n\t if (ret === false) {\n\t return ret;\n\t }\n\t return iterateChildren(currentScope, fn);\n\t}\n\t\n\tfunction iterateSiblings(start, fn) {\n\t var ret;\n\t /* eslint no-extra-boolean-cast:0 */\n\t while (!!(start = start.$$nextSibling)) {\n\t ret = fn(start);\n\t if (ret === false) {\n\t break;\n\t }\n\t\n\t ret = iterateChildren(start, fn);\n\t if (ret === false) {\n\t break;\n\t }\n\t }\n\t return ret;\n\t}\n\t\n\tfunction iterateChildren(start, fn) {\n\t var ret;\n\t while (!!(start = start.$$childHead)) {\n\t ret = fn(start);\n\t if (ret === false) {\n\t break;\n\t }\n\t\n\t ret = iterateSiblings(start, fn);\n\t if (ret === false) {\n\t break;\n\t }\n\t }\n\t return ret;\n\t}\n\t\n\tfunction getScopeById(id) {\n\t var myScope = null;\n\t iterateScopes(function (scope) {\n\t if (scope.$id === id) {\n\t myScope = scope;\n\t return false;\n\t }\n\t });\n\t return myScope;\n\t}\n\t\n\tfunction _makeScopeReference(scope) {\n\t if (_isScopeId(scope)) {\n\t scope = getScopeById(scope);\n\t }\n\t return scope;\n\t}\n\t\n\tfunction _isScopeId(scope) {\n\t return typeof scope === 'string' || typeof scope === 'number';\n\t}\n\tmodule.exports = exports['default'];\n\n/***/ },\n/* 1 */\n/***/ function(module, exports) {\n\n\tmodule.exports = __WEBPACK_EXTERNAL_MODULE_1__;\n\n/***/ }\n/******/ ])\n});\n;\n\n\n/** WEBPACK FOOTER **\n ** dist/ng-stats.min.js\n **/"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap 17fe47d2f2434b59ad2e\n **/","/* eslint no-console:0 */\nimport ng from 'angular';\n\nlet angular = ng;\n\n/* istanbul ignore next */\nif (!angular.version) {\n // we're doing this because some versions\n // of angular don't expose itself correctly\n angular = window.angular;\n}\n\nexport default showAngularStats;\n\nvar autoloadKey = 'showAngularStats_autoload';\nvar current = null;\n// define the timer function to use based upon whether or not 'performance is available'\nvar timerNow = window.self.performance && window.self.performance.now\n ? () => window.self.performance.now()\n : () => Date.now();\n\nvar lastWatchCountRun = timerNow();\nvar watchCountTimeout = null;\nvar lastWatchCount = getWatcherCount() || 0;\nvar lastDigestLength = 0;\nvar scopeSelectors = '.ng-scope, .ng-isolate-scope';\nvar $rootScope;\n\nvar digestIsHijacked = false;\n\nvar listeners = {\n watchCount: {},\n digestLength: {}\n};\n\n// Hijack $digest to time it and update data on every digest.\nfunction hijackDigest() {\n if (digestIsHijacked) {\n return;\n }\n digestIsHijacked = true;\n var scopePrototype = Object.getPrototypeOf(getRootScope());\n var oldDigest = scopePrototype.$digest;\n scopePrototype.$digest = function $digest() {\n var start = timerNow();\n oldDigest.apply(this, arguments);\n var diff = (timerNow() - start);\n updateData(getWatcherCount(), diff);\n };\n}\n\n// used to prevent localstorage error in chrome packaged apps\nfunction isChromeApp() {\n return (typeof chrome !== 'undefined' &&\n typeof chrome.storage !== 'undefined' &&\n typeof chrome.storage.local !== 'undefined');\n}\n\n// check for autoload\nvar autoloadOptions = sessionStorage[autoloadKey] || (!isChromeApp() && localStorage[autoloadKey]);\nif (autoloadOptions) {\n autoload(JSON.parse(autoloadOptions));\n}\n\nfunction autoload(options) {\n if (window.self.angular && getRootScope()) {\n showAngularStats(options);\n } else {\n // wait for angular to load...\n setTimeout(function() {\n autoload(options);\n }, 200);\n }\n}\n\nfunction initOptions(opts) {\n\n // Remove autoload if they did not specifically request it\n if (opts === false || !opts.autoload) {\n sessionStorage.removeItem(autoloadKey);\n localStorage.removeItem(autoloadKey);\n // do nothing if the argument is false\n if (opts === false) {\n return;\n }\n }\n\n opts.position = opts.position || 'top-left';\n opts = angular.extend({\n htmlId: null,\n rootScope: undefined,\n digestTimeThreshold: 16,\n watchCountThreshold: 2000,\n autoload: false,\n trackDigest: false,\n trackWatches: false,\n logDigest: false,\n logWatches: false,\n styles: {\n position: 'fixed',\n background: 'black',\n borderBottom: '1px solid #666',\n borderRight: '1px solid #666',\n color: '#666',\n fontFamily: 'Courier',\n width: 130,\n zIndex: 9999,\n textAlign: 'right',\n top: opts.position.indexOf('top') === -1 ? null : 0,\n bottom: opts.position.indexOf('bottom') === -1 ? null : 0,\n right: opts.position.indexOf('right') === -1 ? null : 0,\n left: opts.position.indexOf('left') === -1 ? null : 0\n }\n }, opts || {});\n\n // for ionic support\n if (opts.rootScope) {\n $rootScope = opts.rootScope;\n }\n\n return opts;\n}\n\nfunction showAngularStats(opts) {\n /* eslint max-statements:[2, 45] */\n /* eslint complexity:[2, 18] */\n /* eslint consistent-return:0 */\n // TODO ^^ fix these things...\n opts = opts !== undefined ? opts : {};\n var returnData = {\n listeners: listeners\n };\n // delete the previous one\n if (current) {\n current.$el && current.$el.remove();\n current.active = false;\n current = null;\n }\n\n // Implemented in separate function due to webpack's statement count limit\n opts = initOptions(opts);\n\n if(!opts) {\n return;\n }\n\n hijackDigest();\n\n // setup the state\n var state = current = {active: true};\n\n // auto-load on startup\n if (opts.autoload) {\n if (opts.autoload === 'localStorage') {\n localStorage.setItem(autoloadKey, JSON.stringify(opts));\n } else if (opts.autoload === 'sessionStorage' || typeof opts.autoload === 'boolean') {\n sessionStorage.setItem(autoloadKey, JSON.stringify(opts));\n } else {\n throw new Error(\n 'Invalid value for autoload: ' + opts.autoload + ' can only be \"localStorage\" \"sessionStorage\" or boolean.'\n );\n }\n }\n\n // general variables\n var bodyEl = angular.element(document.body);\n var noDigestSteps = 0;\n\n // add the DOM element\n var htmlId = opts.htmlId ? (' id=\"' + opts.htmlId + '\"') : '';\n state.$el = angular.element('
|
').css(opts.styles);\n bodyEl.append(state.$el);\n var $watchCount = state.$el.find('span');\n var $digestTime = $watchCount.next();\n\n // initialize the canvas\n var graphSz = {width: 130, height: 40};\n var cvs = state.$el.find('canvas').attr(graphSz)[0];\n\n\n // add listeners\n listeners.digestLength.ngStatsAddToCanvas = function(digestLength) {\n addDataToCanvas(null, digestLength);\n };\n\n listeners.watchCount.ngStatsAddToCanvas = function(watchCount) {\n addDataToCanvas(watchCount);\n };\n\n track('digest', listeners.digestLength);\n track('watches', listeners.watchCount, true);\n\n log('digest', listeners.digestLength);\n log('watches', listeners.watchCount, true);\n\n function track(thingToTrack, listenerCollection, diffOnly) {\n var capThingToTrack = thingToTrack.charAt(0).toUpperCase() + thingToTrack.slice(1);\n if (opts['track' + capThingToTrack]) {\n returnData[thingToTrack] = [];\n listenerCollection['track + capThingToTrack'] = function(tracked) {\n if (!diffOnly || returnData[thingToTrack][returnData.length - 1] !== tracked) {\n returnData[thingToTrack][returnData.length - 1] = tracked;\n returnData[thingToTrack].push(tracked);\n }\n };\n }\n }\n\n function log(thingToLog, listenerCollection, diffOnly) {\n var capThingToLog = thingToLog.charAt(0).toUpperCase() + thingToLog.slice(1);\n if (opts['log' + capThingToLog]) {\n var last;\n listenerCollection['log' + capThingToLog] = function(tracked) {\n if (!diffOnly || last !== tracked) {\n last = tracked;\n var color = colorLog(thingToLog, tracked);\n if (color) {\n console.log('%c' + thingToLog + ':', color, tracked);\n } else {\n console.log(thingToLog + ':', tracked);\n }\n }\n };\n }\n }\n\n function getColor(metric, threshold) {\n if (metric > threshold) {\n return 'red';\n } else if (metric > 0.7 * threshold) {\n return 'orange';\n }\n return 'green';\n }\n\n function colorLog(thingToLog, tracked) {\n var color;\n if (thingToLog === 'digest') {\n color = 'color:' + getColor(tracked, opts.digestTimeThreshold);\n } else if (thingToLog === 'watches') {\n color = 'color:' + getColor(tracked, opts.watchCountThreshold);\n }\n return color;\n }\n\n function addDataToCanvas(watchCount, digestLength) {\n var averageDigest = digestLength || lastDigestLength;\n var digestColor = getColor(averageDigest, opts.digestTimeThreshold);\n lastWatchCount = nullOrUndef(watchCount) ? lastWatchCount : watchCount;\n var watchColor = getColor(lastWatchCount, opts.watchCountThreshold);\n lastDigestLength = nullOrUndef(digestLength) ? lastDigestLength : digestLength;\n $watchCount.text(lastWatchCount).css({color: watchColor});\n $digestTime.text(lastDigestLength.toFixed(2)).css({color: digestColor});\n\n if (!digestLength) {\n return;\n }\n\n // color the sliver if this is the first step\n var ctx = cvs.getContext('2d');\n if (noDigestSteps > 0) {\n noDigestSteps = 0;\n ctx.fillStyle = '#333';\n ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height);\n }\n\n // mark the point on the graph\n ctx.fillStyle = digestColor;\n ctx.fillRect(graphSz.width - 1, Math.max(0, graphSz.height - averageDigest), 2, 2);\n }\n\n // Shift the canvas to the left.\n function shiftLeft() {\n if (state.active) {\n setTimeout(shiftLeft, 250);\n var ctx = cvs.getContext('2d');\n var imageData = ctx.getImageData(1, 0, graphSz.width - 1, graphSz.height);\n ctx.putImageData(imageData, 0, 0);\n ctx.fillStyle = ((noDigestSteps++) > 2) ? 'black' : '#333';\n ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height);\n }\n }\n\n // start everything\n shiftLeft();\n if (!$rootScope.$$phase) {\n $rootScope.$digest();\n }\n\n return returnData;\n}\n\nangular.module('angularStats', []).directive('angularStats', function() {\n var index = 1;\n return {\n scope: {\n digestLength: '@',\n watchCount: '@',\n watchCountRoot: '@',\n onDigestLengthUpdate: '&?',\n onWatchCountUpdate: '&?'\n },\n link: function(scope, el, attrs) {\n hijackDigest();\n var directiveIndex = index++;\n\n setupDigestLengthElement();\n setupWatchCountElement();\n addWatchCountListener();\n addDigestLengthListener();\n scope.$on('$destroy', destroyListeners);\n\n function setupDigestLengthElement() {\n if (attrs.hasOwnProperty('digestLength')) {\n var digestEl = el;\n if (attrs.digestLength) {\n digestEl = angular.element(el[0].querySelector(attrs.digestLength));\n }\n listeners.digestLength['ngStatsDirective' + directiveIndex] = function(length) {\n window.dirDigestNode = digestEl[0];\n digestEl.text((length || 0).toFixed(2));\n };\n }\n }\n\n function setupWatchCountElement() {\n if (attrs.hasOwnProperty('watchCount')) {\n var watchCountRoot;\n var watchCountEl = el;\n if (scope.watchCount) {\n watchCountEl = angular.element(el[0].querySelector(attrs.watchCount));\n }\n\n if (scope.watchCountRoot) {\n if (scope.watchCountRoot === 'this') {\n watchCountRoot = el;\n } else {\n // In the case this directive is being compiled and it's not in the dom,\n // we're going to do the find from the root of what we have...\n var rootParent;\n if (attrs.hasOwnProperty('watchCountOfChild')) {\n rootParent = el[0];\n } else {\n rootParent = findRootOfElement(el);\n }\n watchCountRoot = angular.element(rootParent.querySelector(scope.watchCountRoot));\n if (!watchCountRoot.length) {\n throw new Error('no element at selector: ' + scope.watchCountRoot);\n }\n }\n }\n\n listeners.watchCount['ngStatsDirective' + directiveIndex] = function(count) {\n var watchCount = count;\n if (watchCountRoot) {\n watchCount = getWatcherCountForElement(watchCountRoot);\n }\n watchCountEl.text(watchCount);\n };\n }\n }\n\n function addWatchCountListener() {\n if (attrs.hasOwnProperty('onWatchCountUpdate')) {\n listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex] = function(count) {\n scope.onWatchCountUpdate({watchCount: count});\n };\n }\n }\n\n function addDigestLengthListener() {\n if (attrs.hasOwnProperty('onDigestLengthUpdate')) {\n listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex] = function(length) {\n scope.onDigestLengthUpdate({digestLength: length});\n };\n }\n }\n\n function destroyListeners() {\n delete listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex];\n delete listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex];\n delete listeners.digestLength['ngStatsDirective' + directiveIndex];\n delete listeners.watchCount['ngStatsDirective' + directiveIndex];\n }\n }\n };\n\n function findRootOfElement(el) {\n var parent = el[0];\n while (parent.parentElement) {\n parent = parent.parentElement;\n }\n return parent;\n }\n});\n\n// UTILITY FUNCTIONS\n\nfunction getRootScope() {\n if ($rootScope) {\n return $rootScope;\n }\n var scopeEl = document.querySelector(scopeSelectors);\n if (!scopeEl) {\n return null;\n }\n $rootScope = angular.element(scopeEl).scope().$root;\n return $rootScope;\n}\n\n// Uses timeouts to ensure that this is only run every 300ms (it's a perf bottleneck)\nfunction getWatcherCount() {\n clearTimeout(watchCountTimeout);\n var now = timerNow();\n if (now - lastWatchCountRun > 300) {\n lastWatchCountRun = now;\n lastWatchCount = getWatcherCountForScope();\n } else {\n watchCountTimeout = setTimeout(function() {\n updateData(getWatcherCount());\n }, 350);\n }\n return lastWatchCount;\n}\n\nfunction getWatcherCountForElement(element) {\n var startingScope = getClosestChildScope(element);\n return getWatcherCountForScope(startingScope);\n}\n\nfunction getClosestChildScope(element) {\n element = angular.element(element);\n var scope = element.scope();\n if (!scope) {\n element = angular.element(element.querySelector(scopeSelectors));\n scope = element.scope();\n }\n return scope;\n}\n\nfunction getWatchersFromScope(scope) {\n return scope && scope.$$watchers ? scope.$$watchers : [];\n}\n\n// iterate through listeners to call them with the watchCount and digestLength\nfunction updateData(watchCount, digestLength) {\n // update the listeners\n if (!nullOrUndef(watchCount)) {\n angular.forEach(listeners.watchCount, function(listener) {\n listener(watchCount);\n });\n }\n if (!nullOrUndef(digestLength)) {\n angular.forEach(listeners.digestLength, function(listener) {\n listener(digestLength);\n });\n }\n}\n\nfunction nullOrUndef(item) {\n return item === null || item === undefined;\n}\n\nfunction getWatcherCountForScope(scope) {\n var count = 0;\n iterateScopes(scope, function(childScope) {\n count += getWatchersFromScope(childScope).length;\n });\n return count;\n}\n\nfunction iterateScopes(currentScope, fn) {\n if (typeof currentScope === 'function') {\n fn = currentScope;\n currentScope = null;\n }\n currentScope = currentScope || getRootScope();\n currentScope = _makeScopeReference(currentScope);\n if (!currentScope) {\n return;\n }\n var ret = fn(currentScope);\n if (ret === false) {\n return ret;\n }\n return iterateChildren(currentScope, fn);\n}\n\nfunction iterateSiblings(start, fn) {\n var ret;\n /* eslint no-extra-boolean-cast:0 */\n while (!!(start = start.$$nextSibling)) {\n ret = fn(start);\n if (ret === false) {\n break;\n }\n\n ret = iterateChildren(start, fn);\n if (ret === false) {\n break;\n }\n }\n return ret;\n}\n\nfunction iterateChildren(start, fn) {\n var ret;\n while (!!(start = start.$$childHead)) {\n ret = fn(start);\n if (ret === false) {\n break;\n }\n\n ret = iterateSiblings(start, fn);\n if (ret === false) {\n break;\n }\n }\n return ret;\n}\n\n\nfunction getScopeById(id) {\n var myScope = null;\n iterateScopes(function(scope) {\n if (scope.$id === id) {\n myScope = scope;\n return false;\n }\n });\n return myScope;\n}\n\nfunction _makeScopeReference(scope) {\n if (_isScopeId(scope)) {\n scope = getScopeById(scope);\n }\n return scope;\n}\n\nfunction _isScopeId(scope) {\n return typeof scope === 'string' || typeof scope === 'number';\n}\n\n\n\n/** WEBPACK FOOTER **\n ** ./index.js\n **/","module.exports = __WEBPACK_EXTERNAL_MODULE_1__;\n\n\n/*****************\n ** WEBPACK FOOTER\n ** external \"angular\"\n ** module id = 1\n ** module chunks = 0\n **/"],"sourceRoot":""} --------------------------------------------------------------------------------