├── README.md ├── ngPerformance.html └── ngPerformance.js /README.md: -------------------------------------------------------------------------------- 1 | ngPerformance 2 | ======== 3 | 4 | Angular performance tracking for scopes, watchers, and other page load statistics 5 | 6 | ## Basic Usage 7 | * download, clone or fork it 8 | * (TO BE COMPLETED, not yet available) Or install it using [bower](http://twitter.github.com/bower/) `bower install angular-ngPerformance` 9 | * Include the `ngPerformance.js` script provided by this component into your app. 10 | * Copy the `ngPerformance.html` template provided by this component into desired views folder. 11 | * Add `'blndspt.ngPerformance'` as a module dependency to your app: `angular.module('app', ['blndspt.ngPerformance'])` 12 | 13 | Install Video 14 | https://youtu.be/YQ52cZe0eLc 15 | 16 | **Template URL** (TO BE COMPLETED, not yet available) 17 | ```html 18 |
19 | ``` 20 | 21 | ## Demo 22 | TBD 23 | 24 | ## Overview 25 | While two-way binding in Angular is a blessing, it can also quickly become a kiss of death if not used with a meausre of care and caution. With the simple ease of 'ng-binding' expressions and variables, folks often don't ask the important question of 'if they should have'. Too often today you see pages where no caution was used within ng-repeats and the scopes and watchers exploded exponentially on the page. Further, with very fast development environments, the overall impact may not be as evident at the time. In combination with Angular's single binding features included in Angular > 1.3, or Pasquale Vazzana's bindonce module, responsible developers can ensure that only necessary items are two-way bound. 26 | 27 | This tool is simply a helper to 'give you the bad news' about how you are doing on any given page. In the current release, the following Angular metrics reported: 28 | * Total Scopes 29 | * Total Watchers 30 | * Dirty Checks 31 | * Digest Cycles 32 | * Average Digest Cycle 33 | * Maximum Digest Cycle 34 | 35 | Aside from Angular metrics, the tool also, using the Performance API, allows for tracking of: 36 | * Head Load 37 | * Body Load 38 | * Footer Load 39 | * Vendor Script Load 40 | * App Load 41 | * Metrics Load 42 | * Time To End-of-Page 43 | * Time To Angular 44 | 45 | ## Attribute Usage (TO BE COMPLETED, not yet available) 46 | | Attribute | Description | Example | 47 | |------------|----------------|-----| 48 | | `templateUrl`| Specifies the location to the HTML template (required) | `
` | 49 | | `on/off/param`| Specifies whether to display the panel always (on), never (off), or via Url parameter | `
` | 50 | 51 | ## Advanced Usage 52 | In order to use the non-angular page load metrics, the Performance API is required in your entry point. As such, this directive is really indended for SPA's or applications with some base layout file that is included on every page. In short, collection of several attribute value 'times' are required throughout your entry point (usually index.html). Needed variables are set by using simple Performance. Now offsets and then passed through to the directive via the $window object. See the Page Load Demo for examples of how to use the Page Load metrics. -------------------------------------------------------------------------------- /ngPerformance.html: -------------------------------------------------------------------------------- 1 |
2 | 23 |
24 |
25 |

ANGULAR STATS

26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | 64 | 65 | 66 |
Total Scopes:-
Total Watchers:-
Dirty Checks:-
Digest Cycles:-
Digest Cycle (last): 47 | - ms
48 | - FPS 49 |
Digest Cycle (avg): 54 | - ms
55 | - FPS 56 |
Digest Cycle (max): 61 | - ms
62 | - FPS 63 |
67 | 68 |

PAGELOAD STATS

69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
Head Load:- ms
Body Load:- ms
Footer Load:- ms
Vendor Script Load:- ms
App Load:- ms
Metrics Load:- ms
Time To End-of-Page:- ms
Time To Angular:- ms
105 |
106 |
107 |
108 | 109 | 110 | -------------------------------------------------------------------------------- /ngPerformance.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * @ngdoc directive 7 | * @name ngPerformanceApp.directive:ngPerformance 8 | * @description 9 | * # ngPerformance 10 | */ 11 | 12 | var _el; 13 | 14 | function _setText(id, text) { 15 | if (_el[id]) { 16 | _el[id].textContent = text; 17 | } 18 | } 19 | 20 | var ngPerformanceModule = angular.module('blndspt.ngPerformance', []); 21 | 22 | ngPerformanceModule.directive('ngPerformance', [ 23 | '$log', 24 | '$window', 25 | '$document', 26 | '$rootScope', 27 | function ($log, $window, $document, $rootScope) { 28 | 29 | //Initialize performance stats variables 30 | var ngStart = (performance != null) ? performance.now() : 0; 31 | var stats = ($window.perfStats) ? $window.perfStats : 32 | { 33 | TTLB: 0, 34 | appLoad: 0, 35 | bodyLoad: 0, 36 | footerLoad: 0, 37 | headLoad: 0, 38 | headStart: 0, 39 | metricsLoad: 0, 40 | timeToAngular: 0, 41 | vendorScriptLoad: 0 42 | }; 43 | 44 | stats.timeToAngular = (ngStart - stats.headStart); 45 | 46 | //Count Scopes and Watchers 47 | var _countScopesWatchers = function () { 48 | // This logic is borrowed from $digest(). Keep it in sync! 49 | var next, current, target = $rootScope; 50 | var scopes = 0, 51 | watchers = 0; 52 | 53 | current = target; 54 | do { 55 | scopes += 1; 56 | 57 | if (current.$$watchers) { 58 | watchers += current.$$watchers.length; 59 | } 60 | 61 | // Insanity Warning: scope depth-first traversal 62 | // yes, this code is a bit crazy, but it works and we have tests to prove it! 63 | // this piece should be kept in sync with the traversal in $broadcast 64 | if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { 65 | while (current !== target && !(next = current.$$nextSibling)) { 66 | current = current.$parent; 67 | } 68 | } 69 | } while ((current = next)); 70 | 71 | return [scopes, watchers]; 72 | }; 73 | 74 | return { 75 | templateUrl: 'views/ngPerformance.html', 76 | restrict: 'EA', 77 | link: function (/*scope, element, attrs*/) { 78 | 79 | // Cache DOM elements 80 | _el = { 81 | '#scopes': $document[0].querySelector('#scopes'), 82 | '#watchers': $document[0].querySelector('#watchers'), 83 | '#dirty-checks': $document[0].querySelector('#dirty-checks'), 84 | '#digest-cycles': $document[0].querySelector('#digest-cycles'), 85 | '#digest-ms': $document[0].querySelector('#digest-ms'), 86 | '#digest-fps': $document[0].querySelector('#digest-fps'), 87 | '#avg-digest-ms': $document[0].querySelector('#avg-digest-ms'), 88 | '#avg-digest-fps': $document[0].querySelector('#avg-digest-fps'), 89 | '#max-digest-ms': $document[0].querySelector('#max-digest-ms'), 90 | '#max-digest-fps': $document[0].querySelector('#max-digest-fps'), 91 | 92 | '#head-load': $document[0].querySelector('#head-load'), 93 | '#body-load': $document[0].querySelector('#body-load'), 94 | '#footer-load': $document[0].querySelector('#footer-load'), 95 | '#vendor-load': $document[0].querySelector('#vendor-load'), 96 | '#app-load': $document[0].querySelector('#app-load'), 97 | '#metrics-load': $document[0].querySelector('#metrics-load'), 98 | '#time-to-eop': $document[0].querySelector('#time-to-eop'), 99 | '#time-to-ng': $document[0].querySelector('#time-to-ng'), 100 | }; 101 | 102 | _setText('#head-load', stats.headLoad.toFixed(1)); 103 | _setText('#body-load', stats.bodyLoad.toFixed(1)); 104 | _setText('#footer-load', stats.footerLoad.toFixed(1)); 105 | _setText('#vendor-load', stats.vendorScriptLoad.toFixed(1)); 106 | _setText('#app-load', stats.appLoad.toFixed(1)); 107 | _setText('#metrics-load', stats.metricsLoad.toFixed(1)); 108 | _setText('#time-to-eop', stats.TTLB.toFixed(1)); 109 | _setText('#time-to-ng', stats.timeToAngular.toFixed(1)); 110 | 111 | 112 | // If the browser doesn't support Web Performance API 113 | // (I'm looking at you, Safari), don't even try. 114 | if (performance != null) { 115 | var digestCycles = 0, 116 | digestStart = 0, 117 | sumDigestMs = 0, 118 | maxDigestMs = 0, 119 | dirtyChecks = 0; 120 | 121 | // $digest loop uses a reverse while. 122 | // Pushing onto the end of $$watchers array makes this run first... 123 | $rootScope.$$watchers.push({ 124 | eq: false, 125 | last: null, 126 | fn: function () { 127 | }, 128 | exp: function () { 129 | }, 130 | get: function () { 131 | dirtyChecks++; 132 | 133 | // Only update digestStart if not set. This allows for multiple 134 | // iterations inside the "dirty loop." 135 | // 136 | // NOTE: This technique for timing the $digest cycles 137 | // DOES NOT capture time spent processing the asyncQueue! 138 | if (digestStart === 0) { 139 | // $log.debug('$rootScope.$watch: digestStart'); 140 | digestStart = performance.now(); 141 | digestCycles++; 142 | } 143 | 144 | // Schedules a one-shot callback after digest loop is clean 145 | $rootScope.$$postDigest(function () { 146 | if (digestStart !== 0) { 147 | var digestEnd = performance.now(); 148 | var digestMs = (digestEnd - digestStart); 149 | _setText('#digest-ms', digestMs.toFixed(1)); 150 | _setText('#digest-fps', (1000 / digestMs).toFixed(0)); 151 | 152 | maxDigestMs = Math.max(digestMs, maxDigestMs); 153 | _setText('#max-digest-ms', maxDigestMs.toFixed(1)); 154 | _setText('#max-digest-fps', (1000 / maxDigestMs).toFixed(0)); 155 | 156 | sumDigestMs += digestMs; 157 | if (digestCycles > 0) { 158 | var avgDigestMs = sumDigestMs / digestCycles; 159 | _setText('#avg-digest-ms', avgDigestMs.toFixed(1)); 160 | _setText('#avg-digest-fps', (1000 / avgDigestMs).toFixed(0)); 161 | } 162 | 163 | _setText('#dirty-checks', dirtyChecks); 164 | _setText('#digest-cycles', digestCycles); 165 | 166 | var count = _countScopesWatchers(); 167 | var scopes = count[0], 168 | watchers = count[1]; 169 | 170 | _setText('#scopes', scopes); 171 | _setText('#watchers', watchers); 172 | 173 | var log = 'NG-PERF: Digest Cycle #' + digestCycles + ': ' + digestMs.toFixed(1) + ' ms, ' + 174 | 'Scopes: ' + scopes + ', Watchers: ' + watchers + 175 | ' [Overhead: ' + (performance.now() - digestEnd).toPrecision(3) + ' ms]'; 176 | $log.debug(log); 177 | if ($window.console.timeStamp) { 178 | $window.console.timeStamp(log); 179 | } 180 | 181 | // Register an async function to run first. 182 | // 183 | // NOTE: This technique for timing the $digest cycles 184 | // DOES capture time spent processing the asyncQueue! 185 | // $rootScope.$$asyncQueue.unshift({ 186 | // scope: $rootScope, 187 | // expression: function (scope) { 188 | // // $log.debug('$rootScope.$evalAsync: digestStart'); 189 | // digestStart = performance.now(); 190 | // digestCycles++; 191 | // } 192 | // }); 193 | 194 | // Clear digestStart for next "dirty loop." 195 | digestStart = 0; 196 | } 197 | }); 198 | 199 | return null; 200 | } 201 | }); 202 | } 203 | 204 | } 205 | 206 | }; 207 | }]); 208 | 209 | }) 210 | 211 | (angular); 212 | --------------------------------------------------------------------------------