├── .gitignore ├── .scss └── app.scss ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── config.rb ├── extension ├── _locales │ └── en │ │ └── messages.json ├── css │ ├── app.css │ ├── dependencies.css │ ├── sb-admin-2.css │ ├── timeline.css │ └── vendors.css ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── images │ └── AngularJS-Shield-small.png ├── manifest.json └── src │ ├── background.js │ ├── devtools │ ├── devtools.html │ └── devtools.js │ ├── injected │ ├── content-script.js │ └── inspector.js │ ├── panel │ └── panel.html │ └── vendors │ └── sb-admin-2.js ├── inch.json ├── package.json ├── panelApp ├── main.js ├── models │ └── registry.js ├── panels │ ├── instantMetrics.js │ ├── plots.js │ ├── servicePanelController.js │ └── settingsPanelController.js └── tabHandler.js └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | extension/vendor 3 | *.lock 4 | .sass-cache 5 | 6 | node_modules 7 | npm-debug.log 8 | 9 | *~ 10 | .DS_Store 11 | .idea 12 | .vagrant 13 | target 14 | neo4J-cleanup-addon.iml 15 | log-test.txt 16 | 17 | extension/src/panel/panel.js 18 | extension/src/vendors/*.min.js 19 | *.zip 20 | -------------------------------------------------------------------------------- /.scss/app.scss: -------------------------------------------------------------------------------- 1 | #page-wrapper { 2 | padding-top: 20px; 3 | } 4 | 5 | #digest-time-range-slider{ 6 | margin-top: 10px; 7 | } 8 | 9 | #digest-rate-range-slider{ 10 | margin-top: 10px; 11 | } 12 | 13 | #digest-time-distribution-range-slider{ 14 | margin-top: 20px; 15 | } 16 | 17 | .rickshaw_annotation_timeline .annotation.active .content { 18 | display: inline-block; 19 | } 20 | 21 | .rickshaw_annotation_timeline .annotation:hover .content{ 22 | display: inline-block; 23 | } 24 | 25 | .rickshaw_annotation_timeline .annotation .content { 26 | width: auto; 27 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.1.0](https://github.com/Linkurious/angular-performance/tree/0.1.0) (2015-08-30) 4 | [Full Changelog](https://github.com/Linkurious/angular-performance/compare/0.0.4...0.1.0) 5 | 6 | **Fixed bugs:** 7 | 8 | - Uncaught TypeError: Cannot read property 'get' of undefined [\#19](https://github.com/Linkurious/angular-performance/issues/19) 9 | - Permission Error [\#18](https://github.com/Linkurious/angular-performance/issues/18) 10 | - Error when running angular 1.3 in production mode [\#17](https://github.com/Linkurious/angular-performance/issues/17) 11 | 12 | **Implemented enhancements:** 13 | 14 | - Lazy injection of inspector.js file [\#22](https://github.com/Linkurious/angular-performance/issues/22) 15 | - How to reset all metrics and charts? [\#15](https://github.com/Linkurious/angular-performance/issues/15) 16 | 17 | **Closed issues:** 18 | 19 | - Export charts [\#14](https://github.com/Linkurious/angular-performance/issues/14) 20 | 21 | **Merged pull requests:** 22 | 23 | - Update inspector.js [\#21](https://github.com/Linkurious/angular-performance/pull/21) ([Nilanno](https://github.com/Nilanno)) 24 | 25 | ## [0.0.4](https://github.com/Linkurious/angular-performance/tree/0.0.4) (2015-06-16) 26 | [Full Changelog](https://github.com/Linkurious/angular-performance/compare/0.0.3...0.0.4) 27 | 28 | **Merged pull requests:** 29 | 30 | - Added support for data-ng-app [\#12](https://github.com/Linkurious/angular-performance/pull/12) ([Russe11](https://github.com/Russe11)) 31 | 32 | ## [0.0.3](https://github.com/Linkurious/angular-performance/tree/0.0.3) (2015-05-21) 33 | [Full Changelog](https://github.com/Linkurious/angular-performance/compare/0.0.2...0.0.3) 34 | 35 | **Implemented enhancements:** 36 | 37 | - Remove console.log from the inspector.js [\#11](https://github.com/Linkurious/angular-performance/issues/11) 38 | 39 | **Fixed bugs:** 40 | 41 | - Inspector.js injected twice [\#10](https://github.com/Linkurious/angular-performance/issues/10) 42 | 43 | ## [0.0.2](https://github.com/Linkurious/angular-performance/tree/0.0.2) (2015-05-14) 44 | **Implemented enhancements:** 45 | 46 | - Clean-up inspector.js when the devtools panel is closed [\#7](https://github.com/Linkurious/angular-performance/issues/7) 47 | 48 | **Fixed bugs:** 49 | 50 | - Inspector.js injected twice [\#6](https://github.com/Linkurious/angular-performance/issues/6) 51 | - Fix the way the events are displayed [\#5](https://github.com/Linkurious/angular-performance/issues/5) 52 | - Chrome web store build - Module not detected on minified script [\#3](https://github.com/Linkurious/angular-performance/issues/3) 53 | - When a tab is refreshed after the devtools are opened, the extension stops recording [\#1](https://github.com/Linkurious/angular-performance/issues/1) 54 | 55 | 56 | 57 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | 4 | Thanks for wanting to contribute to this project ! 5 | 6 | ## Pull requests 7 | Please pull request on the `develop` branch, pull requests on `master` will not be merged. The `master` branch represents the state of the extension on the chrome webstore. 8 | 9 | ## Coding guidelines 10 | 11 | ### Indentation 12 | The Indent should be 2 spaces. 13 | 14 | ### Maximum line length 15 | The maximum line length of a line should be 120 characters. 16 | 17 | ### Variable declarations 18 | Several variable declarations should be indented of two spaces: 19 | 20 | ```js 21 | var 22 | un = 1, 23 | dos = 2, 24 | tres = 3; 25 | ``` 26 | 27 | ### Semicolons 28 | Always use semicolons and never rely in implicit insertion. 29 | 30 | ### Quotes 31 | Quote strings with `'`. Don't use `"`. 32 | 33 | ### Constants 34 | Should be declared in upper case. 35 | ```JS 36 | var CONTANT_1 = 10; 37 | ``` 38 | ### Equality 39 | Use the `===` and `!==` operators. The `==` and `!=` operators do type coercion and should not be used. 40 | 41 | ### Errors 42 | When you when to throw an error, NEVER throw something else than an error. 43 | 44 | ```JavaScript 45 | throw new Error('description') 46 | ``` 47 | 48 | ### Commenting your code 49 | Use `JSDoc` to format JavaScript comments. [Read the docs](http://usejsdoc.org/index.html). 50 | 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "sass", "~> 3.4.0" 4 | gem "compass", "~> 1.0" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Linkurious SAS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Angular-performance 2 | 3 | [![Codacy Badge](https://www.codacy.com/project/badge/1ae19e8ddd704a7bab46537588224099)](https://www.codacy.com/app/nikel092_2742/angular-performance) 4 | [![Dependency Status](https://david-dm.org/Linkurious/angular-performance.svg)](https://david-dm.org/Linkurious/angular-performance) 5 | [![Inline docs](http://inch-ci.org/github/Linkurious/angular-performance.svg?branch=master)](http://inch-ci.org/github/Linkurious/angular-performance) 6 | 7 | [![Screenshot](screenshot.png)](screenshot.png) 8 | 9 | This is a chrome extension aimed at monitoring angular application performance. 10 | Tested with: angular 1.2.28 and 1.3.15 11 | 12 | Because of how Angular 1.x is structured, some key elements needs to be monitored during developement to assess an application performance. This extension provides realtime monitoring charts of the number of watchers, digest timing and digest rate. You also get the digest timing distribution so that you can make out exceptionally long digest timing from more recursive paterns and all realtime data are linked to events so that you can determine which actions changed the application performances. Finally, you can time services method and count their execution to determine the ones that have the more impact on the running time of your app. 13 | 14 | ## Install 15 | ###From the Chrome Web Store 16 | [WebStore Link](https://chrome.google.com/webstore/detail/angular-performance/hejbpbhdhhchmmcgmccpnngfedalkmkm) 17 | 18 | ### Manual 19 | * Clone the repository 20 | * **(Optional)** Switch to the develop git branch (Latest version) 21 | * Build the extension (see below) 22 | * Go into the Chrome main menu -> more tools -> extension 23 | * Enable developer mode 24 | * Load unpacked extension 25 | * select the `extension` folder of this repository 26 | 27 | 28 | ## Build 29 | ### Requirements 30 | * Node 31 | * Npm 32 | * Chrome > v41 33 | 34 | ### Commands 35 | 36 | To build the extension you have to run a few commands 37 | 38 | ```shell 39 | $ npm install 40 | $ npm run build 41 | ``` 42 | 43 | ## Develop 44 | Please follow the [Contributing](./CONTRIBUTING.md) code guidelines, we will try to review and merge your changes as quickly as possible. 45 | 46 | If need be here is an [architecture guide](http://nicolasjoseph.com/angular/chrome-extension/performance/2015/08/20/angular-performance-development-guide.html) for angular performance. 47 | 48 | ## Features 49 | 50 | ### Implemented 51 | * Events Capture 52 | * Digest time monitoring 53 | * Digest rate monitoring 54 | * Digest times distribution 55 | * Watcher count monitoring 56 | * Services async and sync timing 57 | 58 | ### Roadmap 59 | * FPS rendering monitoring 60 | * Scopes inspection (respective value and digesting time) 61 | * Dependency analysis 62 | * Back up collected data on a remote server 63 | 64 | ## License 65 | See [LICENSE](LICENSE) file. 66 | 67 | ## Credits 68 | Many thanks to the contributors on these open source projects on which is inspired this extension 69 | * [ng-stats](https://github.com/kentcdodds/ng-stats) 70 | * [Angular Inspector](https://github.com/kkirsche/angularjs-inspector) 71 | * [ng-Inspector](https://github.com/rev087/ng-inspector) 72 | 73 | -------------------------------------------------------------------------------- /config.rb: -------------------------------------------------------------------------------- 1 | # Require any additional compass plugins here. 2 | add_import_path "node_modules" 3 | 4 | # Set this to the root of your project when deployed: 5 | http_path = "/" 6 | css_dir = "extension/css/" 7 | sass_dir = ".scss/" 8 | images_dir = "extension/images/" 9 | 10 | # You can select your preferred output style here (can be overridden via the command line): 11 | # output_style = :expanded or :nested or :compact or :compressed 12 | 13 | # To enable relative paths to assets via compass helper functions. Uncomment: 14 | # relative_assets = true 15 | 16 | # To disable debugging comments that display the original location of your selectors. Uncomment: 17 | # line_comments = false 18 | 19 | # If you prefer the indented syntax, you might want to regenerate this 20 | # project again passing --syntax sass, or you can uncomment this: 21 | # preferred_syntax = :sass 22 | # and then run: 23 | # sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass 24 | -------------------------------------------------------------------------------- /extension/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "Angular-Performance", 4 | "description": "Extension Name" 5 | }, 6 | "pageButtonTitle": { 7 | "message": "Angular Performance", 8 | "description": "Page Button title" 9 | } 10 | } -------------------------------------------------------------------------------- /extension/css/app.css: -------------------------------------------------------------------------------- 1 | /* line 1, ../../.scss/app.scss */ 2 | #page-wrapper { 3 | padding-top: 20px; 4 | } 5 | 6 | /* line 5, ../../.scss/app.scss */ 7 | #digest-time-range-slider { 8 | margin-top: 10px; 9 | } 10 | 11 | /* line 9, ../../.scss/app.scss */ 12 | #digest-rate-range-slider { 13 | margin-top: 10px; 14 | } 15 | 16 | /* line 13, ../../.scss/app.scss */ 17 | #digest-time-distribution-range-slider { 18 | margin-top: 20px; 19 | } 20 | 21 | /* line 17, ../../.scss/app.scss */ 22 | .rickshaw_annotation_timeline .annotation.active .content { 23 | display: inline-block; 24 | } 25 | 26 | /* line 21, ../../.scss/app.scss */ 27 | .rickshaw_annotation_timeline .annotation:hover .content { 28 | display: inline-block; 29 | } 30 | 31 | /* line 25, ../../.scss/app.scss */ 32 | .rickshaw_annotation_timeline .annotation .content { 33 | width: auto; 34 | } 35 | -------------------------------------------------------------------------------- /extension/css/dependencies.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is used to include in the extension all the css files required by the project 3 | * It will be compiled to the vendors.css file. 4 | */ 5 | 6 | @import "bootstrap"; 7 | @import "jquery-ui/themes/base/minified/jquery-ui.min.css"; 8 | @import "metismenu/dist/metisMenu.min.css"; 9 | @import "rickshaw/rickshaw.min.css"; 10 | @import "font-awesome/css/font-awesome.min.css"; -------------------------------------------------------------------------------- /extension/css/sb-admin-2.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - SB Admin 2 Bootstrap Admin Theme (http://startbootstrap.com) 3 | * Code licensed under the Apache License v2.0. 4 | * For details, see http://www.apache.org/licenses/LICENSE-2.0. 5 | */ 6 | 7 | body { 8 | background-color: #f8f8f8; 9 | } 10 | 11 | #wrapper { 12 | width: 100%; 13 | } 14 | 15 | #page-wrapper { 16 | padding: 0 15px; 17 | min-height: 568px; 18 | background-color: #fff; 19 | } 20 | 21 | @media(min-width:768px) { 22 | #page-wrapper { 23 | position: inherit; 24 | margin: 0 0 0 250px; 25 | padding: 0 30px; 26 | border-left: 1px solid #e7e7e7; 27 | } 28 | } 29 | 30 | .navbar-top-links { 31 | margin-right: 0; 32 | } 33 | 34 | .navbar-top-links li { 35 | display: inline-block; 36 | } 37 | 38 | .navbar-top-links li:last-child { 39 | margin-right: 15px; 40 | } 41 | 42 | .navbar-top-links li a { 43 | padding: 15px; 44 | min-height: 50px; 45 | } 46 | 47 | .navbar-top-links .dropdown-menu li { 48 | display: block; 49 | } 50 | 51 | .navbar-top-links .dropdown-menu li:last-child { 52 | margin-right: 0; 53 | } 54 | 55 | .navbar-top-links .dropdown-menu li a { 56 | padding: 3px 20px; 57 | min-height: 0; 58 | } 59 | 60 | .navbar-top-links .dropdown-menu li a div { 61 | white-space: normal; 62 | } 63 | 64 | .navbar-top-links .dropdown-messages, 65 | .navbar-top-links .dropdown-tasks, 66 | .navbar-top-links .dropdown-alerts { 67 | width: 310px; 68 | min-width: 0; 69 | } 70 | 71 | .navbar-top-links .dropdown-messages { 72 | margin-left: 5px; 73 | } 74 | 75 | .navbar-top-links .dropdown-tasks { 76 | margin-left: -59px; 77 | } 78 | 79 | .navbar-top-links .dropdown-alerts { 80 | margin-left: -123px; 81 | } 82 | 83 | .navbar-top-links .dropdown-user { 84 | right: 0; 85 | left: auto; 86 | } 87 | 88 | .sidebar .sidebar-nav.navbar-collapse { 89 | padding-right: 0; 90 | padding-left: 0; 91 | } 92 | 93 | .sidebar .sidebar-search { 94 | padding: 15px; 95 | } 96 | 97 | .sidebar ul li { 98 | border-bottom: 1px solid #e7e7e7; 99 | } 100 | 101 | .sidebar ul li a.active { 102 | background-color: #eee; 103 | } 104 | 105 | .sidebar .arrow { 106 | float: right; 107 | } 108 | 109 | .sidebar .fa.arrow:before { 110 | content: "\f104"; 111 | } 112 | 113 | .sidebar .active>a>.fa.arrow:before { 114 | content: "\f107"; 115 | } 116 | 117 | .sidebar .nav-second-level li, 118 | .sidebar .nav-third-level li { 119 | border-bottom: 0!important; 120 | } 121 | 122 | .sidebar .nav-second-level li a { 123 | padding-left: 37px; 124 | } 125 | 126 | .sidebar .nav-third-level li a { 127 | padding-left: 52px; 128 | } 129 | 130 | @media(min-width:768px) { 131 | .sidebar { 132 | z-index: 1; 133 | position: absolute; 134 | width: 250px; 135 | margin-top: 51px; 136 | } 137 | 138 | .navbar-top-links .dropdown-messages, 139 | .navbar-top-links .dropdown-tasks, 140 | .navbar-top-links .dropdown-alerts { 141 | margin-left: auto; 142 | } 143 | } 144 | 145 | .btn-outline { 146 | color: inherit; 147 | background-color: transparent; 148 | transition: all .5s; 149 | } 150 | 151 | .btn-primary.btn-outline { 152 | color: #428bca; 153 | } 154 | 155 | .btn-success.btn-outline { 156 | color: #5cb85c; 157 | } 158 | 159 | .btn-info.btn-outline { 160 | color: #5bc0de; 161 | } 162 | 163 | .btn-warning.btn-outline { 164 | color: #f0ad4e; 165 | } 166 | 167 | .btn-danger.btn-outline { 168 | color: #d9534f; 169 | } 170 | 171 | .btn-primary.btn-outline:hover, 172 | .btn-success.btn-outline:hover, 173 | .btn-info.btn-outline:hover, 174 | .btn-warning.btn-outline:hover, 175 | .btn-danger.btn-outline:hover { 176 | color: #fff; 177 | } 178 | 179 | .chat { 180 | margin: 0; 181 | padding: 0; 182 | list-style: none; 183 | } 184 | 185 | .chat li { 186 | margin-bottom: 10px; 187 | padding-bottom: 5px; 188 | border-bottom: 1px dotted #999; 189 | } 190 | 191 | .chat li.left .chat-body { 192 | margin-left: 60px; 193 | } 194 | 195 | .chat li.right .chat-body { 196 | margin-right: 60px; 197 | } 198 | 199 | .chat li .chat-body p { 200 | margin: 0; 201 | } 202 | 203 | .panel .slidedown .glyphicon, 204 | .chat .glyphicon { 205 | margin-right: 5px; 206 | } 207 | 208 | .chat-panel .panel-body { 209 | height: 350px; 210 | overflow-y: scroll; 211 | } 212 | 213 | .login-panel { 214 | margin-top: 25%; 215 | } 216 | 217 | .flot-chart { 218 | display: block; 219 | height: 400px; 220 | } 221 | 222 | .flot-chart-content { 223 | width: 100%; 224 | height: 100%; 225 | } 226 | 227 | .dataTables_wrapper { 228 | position: relative; 229 | clear: both; 230 | } 231 | 232 | table.dataTable thead .sorting, 233 | table.dataTable thead .sorting_asc, 234 | table.dataTable thead .sorting_desc, 235 | table.dataTable thead .sorting_asc_disabled, 236 | table.dataTable thead .sorting_desc_disabled { 237 | background: 0 0; 238 | } 239 | 240 | table.dataTable thead .sorting_asc:after { 241 | content: "\f0de"; 242 | float: right; 243 | font-family: fontawesome; 244 | } 245 | 246 | table.dataTable thead .sorting_desc:after { 247 | content: "\f0dd"; 248 | float: right; 249 | font-family: fontawesome; 250 | } 251 | 252 | table.dataTable thead .sorting:after { 253 | content: "\f0dc"; 254 | float: right; 255 | font-family: fontawesome; 256 | color: rgba(50,50,50,.5); 257 | } 258 | 259 | .btn-circle { 260 | width: 30px; 261 | height: 30px; 262 | padding: 6px 0; 263 | border-radius: 15px; 264 | text-align: center; 265 | font-size: 12px; 266 | line-height: 1.428571429; 267 | } 268 | 269 | .btn-circle.btn-lg { 270 | width: 50px; 271 | height: 50px; 272 | padding: 10px 16px; 273 | border-radius: 25px; 274 | font-size: 18px; 275 | line-height: 1.33; 276 | } 277 | 278 | .btn-circle.btn-xl { 279 | width: 70px; 280 | height: 70px; 281 | padding: 10px 16px; 282 | border-radius: 35px; 283 | font-size: 24px; 284 | line-height: 1.33; 285 | } 286 | 287 | .show-grid [class^=col-] { 288 | padding-top: 10px; 289 | padding-bottom: 10px; 290 | border: 1px solid #ddd; 291 | background-color: #eee!important; 292 | } 293 | 294 | .show-grid { 295 | margin: 15px 0; 296 | } 297 | 298 | .huge { 299 | font-size: 40px; 300 | } 301 | 302 | .panel-green { 303 | border-color: #5cb85c; 304 | } 305 | 306 | .panel-green .panel-heading { 307 | border-color: #5cb85c; 308 | color: #fff; 309 | background-color: #5cb85c; 310 | } 311 | 312 | .panel-green a { 313 | color: #5cb85c; 314 | } 315 | 316 | .panel-green a:hover { 317 | color: #3d8b3d; 318 | } 319 | 320 | .panel-red { 321 | border-color: #d9534f; 322 | } 323 | 324 | .panel-red .panel-heading { 325 | border-color: #d9534f; 326 | color: #fff; 327 | background-color: #d9534f; 328 | } 329 | 330 | .panel-red a { 331 | color: #d9534f; 332 | } 333 | 334 | .panel-red a:hover { 335 | color: #b52b27; 336 | } 337 | 338 | .panel-yellow { 339 | border-color: #f0ad4e; 340 | } 341 | 342 | .panel-yellow .panel-heading { 343 | border-color: #f0ad4e; 344 | color: #fff; 345 | background-color: #f0ad4e; 346 | } 347 | 348 | .panel-yellow a { 349 | color: #f0ad4e; 350 | } 351 | 352 | .panel-yellow a:hover { 353 | color: #df8a13; 354 | } -------------------------------------------------------------------------------- /extension/css/timeline.css: -------------------------------------------------------------------------------- 1 | .timeline { 2 | position: relative; 3 | padding: 20px 0 20px; 4 | list-style: none; 5 | } 6 | 7 | .timeline:before { 8 | content: " "; 9 | position: absolute; 10 | top: 0; 11 | bottom: 0; 12 | left: 50%; 13 | width: 3px; 14 | margin-left: -1.5px; 15 | background-color: #eeeeee; 16 | } 17 | 18 | .timeline > li { 19 | position: relative; 20 | margin-bottom: 20px; 21 | } 22 | 23 | .timeline > li:before, 24 | .timeline > li:after { 25 | content: " "; 26 | display: table; 27 | } 28 | 29 | .timeline > li:after { 30 | clear: both; 31 | } 32 | 33 | .timeline > li:before, 34 | .timeline > li:after { 35 | content: " "; 36 | display: table; 37 | } 38 | 39 | .timeline > li:after { 40 | clear: both; 41 | } 42 | 43 | .timeline > li > .timeline-panel { 44 | float: left; 45 | position: relative; 46 | width: 46%; 47 | padding: 20px; 48 | border: 1px solid #d4d4d4; 49 | border-radius: 2px; 50 | -webkit-box-shadow: 0 1px 6px rgba(0,0,0,0.175); 51 | box-shadow: 0 1px 6px rgba(0,0,0,0.175); 52 | } 53 | 54 | .timeline > li > .timeline-panel:before { 55 | content: " "; 56 | display: inline-block; 57 | position: absolute; 58 | top: 26px; 59 | right: -15px; 60 | border-top: 15px solid transparent; 61 | border-right: 0 solid #ccc; 62 | border-bottom: 15px solid transparent; 63 | border-left: 15px solid #ccc; 64 | } 65 | 66 | .timeline > li > .timeline-panel:after { 67 | content: " "; 68 | display: inline-block; 69 | position: absolute; 70 | top: 27px; 71 | right: -14px; 72 | border-top: 14px solid transparent; 73 | border-right: 0 solid #fff; 74 | border-bottom: 14px solid transparent; 75 | border-left: 14px solid #fff; 76 | } 77 | 78 | .timeline > li > .timeline-badge { 79 | z-index: 100; 80 | position: absolute; 81 | top: 16px; 82 | left: 50%; 83 | width: 50px; 84 | height: 50px; 85 | margin-left: -25px; 86 | border-radius: 50% 50% 50% 50%; 87 | text-align: center; 88 | font-size: 1.4em; 89 | line-height: 50px; 90 | color: #fff; 91 | background-color: #999999; 92 | } 93 | 94 | .timeline > li.timeline-inverted > .timeline-panel { 95 | float: right; 96 | } 97 | 98 | .timeline > li.timeline-inverted > .timeline-panel:before { 99 | right: auto; 100 | left: -15px; 101 | border-right-width: 15px; 102 | border-left-width: 0; 103 | } 104 | 105 | .timeline > li.timeline-inverted > .timeline-panel:after { 106 | right: auto; 107 | left: -14px; 108 | border-right-width: 14px; 109 | border-left-width: 0; 110 | } 111 | 112 | .timeline-badge.primary { 113 | background-color: #2e6da4 !important; 114 | } 115 | 116 | .timeline-badge.success { 117 | background-color: #3f903f !important; 118 | } 119 | 120 | .timeline-badge.warning { 121 | background-color: #f0ad4e !important; 122 | } 123 | 124 | .timeline-badge.danger { 125 | background-color: #d9534f !important; 126 | } 127 | 128 | .timeline-badge.info { 129 | background-color: #5bc0de !important; 130 | } 131 | 132 | .timeline-title { 133 | margin-top: 0; 134 | color: inherit; 135 | } 136 | 137 | .timeline-body > p, 138 | .timeline-body > ul { 139 | margin-bottom: 0; 140 | } 141 | 142 | .timeline-body > p + p { 143 | margin-top: 5px; 144 | } 145 | 146 | @media(max-width:767px) { 147 | ul.timeline:before { 148 | left: 40px; 149 | } 150 | 151 | ul.timeline > li > .timeline-panel { 152 | width: calc(100% - 90px); 153 | width: -moz-calc(100% - 90px); 154 | width: -webkit-calc(100% - 90px); 155 | } 156 | 157 | ul.timeline > li > .timeline-badge { 158 | top: 16px; 159 | left: 15px; 160 | margin-left: 0; 161 | } 162 | 163 | ul.timeline > li > .timeline-panel { 164 | float: right; 165 | } 166 | 167 | ul.timeline > li > .timeline-panel:before { 168 | right: auto; 169 | left: -15px; 170 | border-right-width: 15px; 171 | border-left-width: 0; 172 | } 173 | 174 | ul.timeline > li > .timeline-panel:after { 175 | right: auto; 176 | left: -14px; 177 | border-right-width: 14px; 178 | border-left-width: 0; 179 | } 180 | } -------------------------------------------------------------------------------- /extension/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /extension/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /extension/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /extension/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /extension/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /extension/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /extension/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 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 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /extension/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /extension/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /extension/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /extension/images/AngularJS-Shield-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/extension/images/AngularJS-Shield-small.png -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_extName__", 3 | "version": "0.1.0", 4 | "manifest_version": 2, 5 | "default_locale": "en", 6 | "author": "Nicolas Joseph", 7 | "icons": { 8 | "16": "images/AngularJS-Shield-small.png", 9 | "48": "images/AngularJS-Shield-small.png", 10 | "128": "images/AngularJS-Shield-small.png" 11 | }, 12 | "devtools_page": "src/devtools/devtools.html", 13 | "content_scripts": [{ 14 | "matches": [""], 15 | "js": ["src/injected/content-script.js"], 16 | "run_at": "document_end" 17 | }], 18 | "page_action": { 19 | "default_icon": { 20 | "19": "images/AngularJS-Shield-small.png", 21 | "38": "images/AngularJS-Shield-small.png" 22 | }, 23 | "default_title": "__MSG_pageButtonTitle__" 24 | }, 25 | "background": { 26 | "scripts": ["src/background.js"], 27 | "persistent": false 28 | }, 29 | "permissions": ["", "tabs", "webNavigation"], 30 | "web_accessible_resources": [ 31 | "src/injected/inspector.js" 32 | ] 33 | } -------------------------------------------------------------------------------- /extension/src/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Mapping of the connections between the devtools and the tabs 4 | var 5 | contentScriptConnections = {}, 6 | devToolsConnections = {}, 7 | panelConnections = {}; 8 | 9 | // Handles connections 10 | chrome.runtime.onConnect.addListener(function(port){ 11 | 12 | /** 13 | * Listener for the devtools panel messages. 14 | * 15 | * @param {Object} message message sent from the devtools 16 | * @param {Object} message.task task to execute 17 | */ 18 | var devToolsListener = function(message){ 19 | 20 | switch (message.task){ 21 | case 'init': 22 | console.log('Background - Dev Tools listener initialized ' + message.tabId); 23 | devToolsConnections[message.tabId] = port; 24 | if (contentScriptConnections[message.tabId]){ 25 | contentScriptConnections[message.tabId].postMessage({ 26 | task: 'addInspector' 27 | }); 28 | } 29 | break; 30 | case 'log': 31 | if (message.obj) { 32 | console.log('Devtools - ' + message.text, message.obj); 33 | } else { 34 | console.log('Devtools - ' + message.text); 35 | } 36 | break; 37 | default: 38 | console.log('Background - Devtools listener received an unknown task: ' + message.task); 39 | } 40 | return true; 41 | }; 42 | 43 | /** 44 | * Listener for the content Script. 45 | * 46 | * @param {Object} message message sent from the devtools 47 | * @param {Object} message.task task to execute 48 | * @param {Port} port port 49 | */ 50 | var contentScriptListener = function(message, port){ 51 | 52 | var sender = port.sender; 53 | 54 | if(message.task === 'log'){ 55 | if (message.obj) { 56 | console.log('Content-script - ' + message.text, message.obj); 57 | } else { 58 | console.log('Content-script - ' + message.text); 59 | } 60 | return; 61 | } 62 | 63 | if (sender.tab) { 64 | 65 | var tabId = sender.tab.id; 66 | 67 | if (message.task === 'initDevToolPanel' && devToolsConnections[tabId]) { 68 | devToolsConnections[tabId].postMessage(message); 69 | } else if (message.task === 'checkDevToolsStatus' && devToolsConnections[tabId]) { 70 | contentScriptConnections[tabId].postMessage({ 71 | task: 'addInspector' 72 | }); 73 | } else if (tabId in panelConnections) { 74 | panelConnections[tabId].postMessage(message); 75 | } else { 76 | //console.log('background.js - Tab not found in connection list.', sender.tab.id, panelConnections); 77 | } 78 | } else { 79 | console.log('Background - sender.tab not defined.'); 80 | } 81 | return true; 82 | }; 83 | 84 | /** 85 | * Listener for the panel inserted in the devtools (we cannot communicate directly between the 86 | * devtool page and the panel (weird) 87 | * 88 | * @param {Object} message 89 | */ 90 | var panelListener = function(message){ 91 | if (message.task === 'init'){ 92 | console.log('Background - Panel listener initialized ', message.tabId); 93 | panelConnections[message.tabId] = port 94 | 95 | } else if (message.task === 'sendTaskToInspector') { 96 | contentScriptConnections[message.tabId].postMessage(message.data) 97 | 98 | } else if (message.task === 'log'){ 99 | if (message.obj) { 100 | console.log('Panel - ' + message.text, message.obj); 101 | } else { 102 | console.log('Panel - ' + message.text); 103 | } 104 | } 105 | }; 106 | 107 | /* 108 | Bindings 109 | */ 110 | 111 | if (port.name === 'devtools-page'){ 112 | console.log('Background - Devtools listener setup'); 113 | 114 | // add the listener 115 | port.onMessage.addListener(devToolsListener); 116 | 117 | // Removes the listener on disconnection and cleans up connections 118 | port.onDisconnect.addListener(function() { 119 | port.onMessage.removeListener(devToolsListener); 120 | 121 | var tabs = Object.keys(devToolsConnections); 122 | for (var i=0, len=tabs.length; i < len; i++) { 123 | if (devToolsConnections[tabs[i]] == port) { 124 | delete devToolsConnections[tabs[i]]; 125 | break; 126 | } 127 | } 128 | }); 129 | 130 | 131 | } else if (port.name === 'content-script'){ 132 | 133 | console.log('Background - content script listener setup'); 134 | port.onMessage.addListener(contentScriptListener); 135 | 136 | // If the devtools are already opened for this tab, and this is the content-script has just been injected 137 | // we also want to add the inspector to the page 138 | if (port.sender){ 139 | contentScriptConnections[port.sender.tab.id] = port; 140 | } 141 | 142 | port.onDisconnect.addListener(function(){ 143 | port.onMessage.removeListener(contentScriptListener); 144 | 145 | var tabs = Object.keys(contentScriptConnections); 146 | for (var i=0, len=tabs.length; i < len; i++) { 147 | if (contentScriptConnections[tabs[i]] == port) { 148 | delete contentScriptConnections[tabs[i]]; 149 | break; 150 | } 151 | } 152 | }); 153 | 154 | 155 | } else if (port.name === 'angular-performance-panel'){ 156 | 157 | port.onMessage.addListener(panelListener); 158 | 159 | // Removes the listener on disconnection and cleans up connections 160 | port.onDisconnect.addListener(function() { 161 | port.onMessage.removeListener(panelListener); 162 | 163 | var tabs = Object.keys(panelConnections); 164 | for (var i = 0, len = tabs.length; i < len ; i++) { 165 | if (panelConnections[tabs[i]] == port) { 166 | delete panelConnections[tabs[i]]; 167 | // On panel closing, clean up the tab from all wrapped functions and removes the injector.js 168 | contentScriptConnections[tabs[i]].postMessage({ 169 | task: 'cleanUpInspectedApp' 170 | }); 171 | break; 172 | } 173 | } 174 | }); 175 | } 176 | }); 177 | -------------------------------------------------------------------------------- /extension/src/devtools/devtools.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /extension/src/devtools/devtools.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _panelBuilt = false; 4 | 5 | // Create a connection to the background page 6 | var backgroundPageConnection = chrome.runtime.connect({ 7 | name: "devtools-page" 8 | }); 9 | 10 | /** 11 | * Proxy to be used to send log messages to the background script fom where they can be read. 12 | * 13 | * @param {String} message - Message to be printed into the background.js console 14 | * @param {Object} [obj] - Object to log into the console. 15 | */ 16 | function log(message, obj){ 17 | var logMessage = { 18 | task: 'log', 19 | text: message 20 | }; 21 | 22 | if (!!obj){ 23 | logMessage.obj = obj; 24 | } 25 | 26 | backgroundPageConnection.postMessage(logMessage); 27 | } 28 | 29 | backgroundPageConnection.onMessage.addListener(function (message) { 30 | 31 | // We only want to build the panel if angular was detected in the page 32 | if (message.task === 'initDevToolPanel' && !_panelBuilt){ 33 | log('building panel'); 34 | chrome.devtools.panels.create( 35 | 'Angular', 36 | 'images/AngularJS-Shield-small.png', 37 | 'src/panel/panel.html' 38 | ); 39 | _panelBuilt = true; 40 | } 41 | }); 42 | 43 | // Tell the background script to include this into the dispatch 44 | backgroundPageConnection.postMessage({ 45 | task: 'init', 46 | tabId: chrome.devtools.inspectedWindow.tabId 47 | }); -------------------------------------------------------------------------------- /extension/src/injected/content-script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | This content script is injected only when the devtools are opened 5 | */ 6 | 7 | var 8 | USER_EVENTS = [ 9 | 'mousedown', 10 | 'mouseup', 11 | 'click', 12 | 'dblclick', 13 | //Mouse move event id too chatty 14 | //'mousemove', 15 | 'mouseover', 16 | 'mouseout', 17 | 'mousewheel', 18 | 19 | 'keydown', 20 | 'keyup', 21 | 'keypress', 22 | 'textInput', 23 | 24 | 'touchstart', 25 | 'touchmove', 26 | 'touchend', 27 | 'touchcancel', 28 | 29 | 'resize', 30 | 'scroll', 31 | 'zoom', 32 | 'focus', 33 | 'blur', 34 | 'select', 35 | 'change', 36 | 'submit', 37 | 'reset' 38 | ]; 39 | 40 | var backgroundPageConnection = chrome.runtime.connect({ 41 | name: 'content-script' 42 | }); 43 | 44 | /** 45 | * Proxy to be used to send log messages to the background script fom where they can be read. 46 | * 47 | * @param {String} message - Message to be printed into the background.js console 48 | * @param {Object} [obj] - Object to log into the console. 49 | */ 50 | function log(message, obj){ 51 | 52 | var logMessage = { 53 | task: 'log', 54 | text: message 55 | }; 56 | 57 | if (!!obj){ 58 | logMessage.obj = obj; 59 | } 60 | 61 | backgroundPageConnection.postMessage(logMessage); 62 | } 63 | 64 | /** 65 | * Gets the XPath of an element 66 | * 67 | * @param {Node} element - Dom element 68 | * @returns {string} - XPath 69 | */ 70 | function generateXPath(element) { 71 | if (element.id !== ''){ 72 | return 'id("' + element.id + '")'; 73 | } 74 | if (element === document.body || element.parentNode === null) { 75 | return element.tagName; 76 | } 77 | 78 | var ix = 0; 79 | var siblings = element.parentNode.childNodes; 80 | 81 | for (var i = 0 ; i < siblings.length ; i++) { 82 | var sibling = siblings[i]; 83 | if (sibling === element) { 84 | return generateXPath(element.parentNode) + '/' + element.tagName + '[' + (ix + 1) + ']'; 85 | } 86 | if (sibling.nodeType === 1 && sibling.tagName === element.tagName) { 87 | ix++; 88 | } 89 | } 90 | } 91 | 92 | log('Loaded'); 93 | 94 | // Listen for messages from the current page and send then to the background script for dispatch 95 | window.addEventListener('message', function(event) { 96 | 97 | // We only accept messages from ourselves 98 | if (event.source != window) { 99 | return; 100 | } 101 | 102 | var message = event.data; 103 | 104 | if (typeof message !== 'object' || message === null || message.source !== 'angular-performance-inspector') { 105 | return; 106 | } 107 | 108 | if (message.task === 'removeInspector'){ 109 | // removes the inspector from the DOM 110 | var inspector = document.getElementById("angular-performance-inspector"); 111 | inspector.parentNode.removeChild(inspector); 112 | return; 113 | } 114 | 115 | backgroundPageConnection.postMessage(message); 116 | }, false); 117 | 118 | backgroundPageConnection.onMessage.addListener(function(message){ 119 | 120 | if (message.source){ 121 | log('Source already defined'); 122 | } 123 | 124 | if (message.task === 'addInspector'){ 125 | log('Add Inspector'); 126 | 127 | // Add Listeners for all user events to that they can be captured 128 | USER_EVENTS.forEach(function(eventType){ 129 | document.addEventListener(eventType, function(event){ 130 | backgroundPageConnection.postMessage({ 131 | source: 'angular-performance', 132 | task: 'registerEvent', 133 | data: { 134 | timestamp: Date.now(), 135 | event: { 136 | type: event.type, 137 | targetDOMPath: generateXPath(event.target) 138 | } 139 | } 140 | }); 141 | }); 142 | }); 143 | 144 | 145 | // Add injected script to the page 146 | var script = document.createElement('script'); 147 | script.type = 'text/javascript'; 148 | script.id = 'angular-performance-inspector'; 149 | script.src = chrome.extension.getURL('src/injected/inspector.js'); 150 | document.head.appendChild(script); 151 | return; 152 | } 153 | 154 | message.source = 'angular-performance'; 155 | window.postMessage(message, '*'); 156 | }); 157 | 158 | backgroundPageConnection.postMessage({ 159 | task: 'checkDevToolsStatus' 160 | }); 161 | -------------------------------------------------------------------------------- /extension/src/injected/inspector.js: -------------------------------------------------------------------------------- 1 | /* 2 | File that will be injected in the page by the content script in order to retrieve angular 3 | performance metrics. 4 | 5 | Heavily inspired by: 6 | - ng stats 7 | - ng inspector 8 | - Angular Inspector 9 | - Batarang 10 | 11 | */ 12 | (function() { 13 | 'use strict'; 14 | 15 | var 16 | _angularInjector, 17 | _isMonitoringActive = true, 18 | _backUp = { 19 | digest: null, 20 | modules: {} 21 | }; 22 | 23 | if (document.readyState === 'complete'){ 24 | setTimeout(detectAngular, 0); 25 | } else { 26 | window.onload = function() { 27 | setTimeout(detectAngular, 0); 28 | }; 29 | } 30 | 31 | /** 32 | * If angular is detected, bootstraps the inspector 33 | */ 34 | function detectAngular(){ 35 | if (typeof angular !== 'undefined') { 36 | 37 | sendTask('initDevToolPanel'); 38 | 39 | // We listen for async instrumentation instructions 40 | window.addEventListener('message', function(event){ 41 | // We only accept messages from ourselves 42 | if (event.source != window || event.data.source !== 'angular-performance') { 43 | return; 44 | } 45 | 46 | var message = event.data; 47 | 48 | switch (message.task){ 49 | 50 | case 'checkModuleName': 51 | var moduleServices = getNgModuleServices(message.moduleName); 52 | // If there is no services the method will return an empty object, if the module name is 53 | // invalid, it will return undefined. 54 | sendTask('reportModuleExistence', { 55 | moduleName: message.moduleName, 56 | services: (moduleServices) ? Object.keys(moduleServices) : undefined 57 | }); 58 | break; 59 | 60 | case 'instrumentModuleServices': 61 | instrumentModuleServices(message.moduleName); 62 | break; 63 | 64 | case 'cleanUpInspectedApp': 65 | _isMonitoringActive = false; 66 | cleanUpInspectedApp(); 67 | // Once everything is cleaned up, we can remove this script from the DOM 68 | sendTask('removeInspector'); 69 | break; 70 | } 71 | }); 72 | 73 | 74 | if (document.querySelector('[ng-app],[data-ng-app]')){ 75 | try { 76 | // Check if angular is running in production mode. If it is, reload with correct debugging information 77 | _angularInjector = angular.element(document.querySelector('[ng-app],[data-ng-app]')).injector().get; 78 | Object.getPrototypeOf(getRootScope()); 79 | } catch(e){ 80 | if (e instanceof TypeError) { 81 | angular.reloadWithDebugInfo(); 82 | } else { 83 | console.error(e) 84 | } 85 | } finally { 86 | bootstrapInspector(); 87 | } 88 | } else { 89 | console.log('Angular performance, ng-app and data-ng-app are not defined. At least one need to be defined.') 90 | } 91 | 92 | } 93 | } 94 | 95 | /** 96 | * Function to set up all listeners and data mining tools 97 | */ 98 | function bootstrapInspector(){ 99 | instrumentDigest(); 100 | initWatcherCount(); 101 | } 102 | 103 | /** 104 | * This should clean up all that has been instrumented by the inspector and get them back 105 | * to their normal behaviour. (UnWrap everything) 106 | */ 107 | function cleanUpInspectedApp(){ 108 | restoreDigest(); 109 | restoreModuleServices(); 110 | } 111 | 112 | // ------------------------------------------------------------------------------------------ 113 | // Digest Monitoring 114 | // ------------------------------------------------------------------------------------------ 115 | 116 | /** 117 | * Wraps the angular digest so that we can measure how long it take for the digest to happen. 118 | */ 119 | function instrumentDigest(){ 120 | 121 | var scopePrototype = Object.getPrototypeOf(getRootScope()); 122 | _backUp.digest = scopePrototype.$digest; 123 | 124 | scopePrototype.$digest = function $digest() { 125 | var start = performance.now(); 126 | _backUp.digest.apply(this, arguments); 127 | var time = (performance.now() - start); 128 | register('DigestTiming', { 129 | timestamp: Date.now(), 130 | time: time 131 | }); 132 | }; 133 | } 134 | 135 | /** 136 | * Restores the classic angular digest. 137 | */ 138 | function restoreDigest(){ 139 | Object.getPrototypeOf(getRootScope()).$digest = _backUp.digest; 140 | } 141 | 142 | // ------------------------------------------------------------------------------------------ 143 | // Scope & Watcher Exploration 144 | // ------------------------------------------------------------------------------------------ 145 | 146 | /** 147 | * Function to be called once to init the watcher retrieval. 148 | */ 149 | function initWatcherCount(){ 150 | register('RootWatcherCount', { 151 | timestamp: Date.now(), 152 | watcher:{ 153 | watcherCount: getWatcherCountForScope(), 154 | location: window.location.href 155 | } 156 | }); 157 | if (_isMonitoringActive) { 158 | setTimeout(initWatcherCount, 300); 159 | } 160 | } 161 | 162 | /** 163 | * Retrieves the watcher count for a particular scope 164 | * 165 | * @param {angular.scope} [$scope] - angular scope instance 166 | * @returns {number} - angular scope count 167 | */ 168 | function getWatcherCountForScope($scope) { 169 | var count = 0; 170 | iterateScopes($scope, function($scope) { 171 | count += getWatchersFromScope($scope).length; 172 | }); 173 | return count; 174 | } 175 | 176 | /** 177 | * Apply a function down the angular scope 178 | * 179 | * @param {angular.scope} [current] - current angular $scope 180 | * @param {Function} fn - function to apply down the scope 181 | * @returns {*} 182 | */ 183 | function iterateScopes(current, fn) { 184 | if (typeof current === 'function') { 185 | fn = current; 186 | current = null; 187 | } 188 | current = current || getRootScope(); 189 | current = makeScopeReference(current); 190 | if (!current) { 191 | return; 192 | } 193 | var ret = fn(current); 194 | if (ret === false) { 195 | return ret; 196 | } 197 | return iterateChildren(current, fn); 198 | } 199 | 200 | /** 201 | * Apply a function on a scope siblings (same scope level) and down to their children 202 | * 203 | * @param {angular.scope} start - starting scope of the iteration 204 | * @param {Function} fn - function to be applied on the different scopes 205 | * @returns {*} 206 | */ 207 | function iterateSiblings(start, fn) { 208 | var ret; 209 | while (!!(start = start.$$nextSibling)) { 210 | ret = fn(start); 211 | if (ret === false) { 212 | break; 213 | } 214 | 215 | ret = iterateChildren(start, fn); 216 | if (ret === false) { 217 | break; 218 | } 219 | } 220 | return ret; 221 | } 222 | 223 | /** 224 | * Apply a function on all the children scopes and their respective siblings 225 | * 226 | * @param {angular.scope} start - start node of the 227 | * @param {Function} fn - function to apply 228 | * @returns {*} 229 | */ 230 | function iterateChildren(start, fn) { 231 | var ret; 232 | while (!!(start = start.$$childHead)) { 233 | ret = fn(start); 234 | if (ret === false) { 235 | break; 236 | } 237 | 238 | ret = iterateSiblings(start, fn); 239 | if (ret === false) { 240 | break; 241 | } 242 | } 243 | return ret; 244 | } 245 | 246 | 247 | /** 248 | * Gets the angular root scope 249 | * 250 | * @returns {angular.scope} 251 | */ 252 | function getRootScope(){ 253 | if (typeof $rootScope !== 'undefined') { 254 | return $rootScope; 255 | } 256 | var scopeEl = document.querySelector('.ng-scope'); 257 | if (!scopeEl) { 258 | return null; 259 | } 260 | return angular.element(scopeEl).scope().$root; 261 | } 262 | 263 | /** 264 | * Retrieve all watchers from a scope 265 | * 266 | * @param {angular.scope} scope - angular scope to get the watchers from 267 | * @returns {Array} 268 | */ 269 | function getWatchersFromScope(scope) { 270 | return scope && scope.$$watchers ? scope.$$watchers : []; 271 | } 272 | 273 | /** 274 | * Gets the id of a scope 275 | * 276 | * @param {angular.scope} scope - angular scope to get the id from 277 | * @returns {angular.scope} 278 | */ 279 | function makeScopeReference(scope) { 280 | if (isScopeId(scope)) { 281 | scope = getScopeById(scope); 282 | } 283 | return scope; 284 | } 285 | 286 | /** 287 | * Gets the scope from an ID 288 | * 289 | * @param {String|Number} id - scope id 290 | * @returns {angular.scope} 291 | */ 292 | function getScopeById(id) { 293 | var myScope = null; 294 | iterateScopes(function(scope) { 295 | if (scope.$id === id) { 296 | myScope = scope; 297 | return false; 298 | } 299 | }); 300 | return myScope; 301 | } 302 | 303 | /** 304 | * Check if the scope passed as an argument is an id or not. 305 | * 306 | * @param {angular.scope} scope 307 | * @returns {boolean} 308 | */ 309 | function isScopeId(scope) { 310 | return typeof scope === 'string' || typeof scope === 'number'; 311 | } 312 | 313 | // ------------------------------------------------------------------------------------------ 314 | // Module & Services 315 | // ------------------------------------------------------------------------------------------ 316 | 317 | /** 318 | * Gets all the services name from the specified module 319 | * 320 | * @param {String} moduleName - name of the module to get the services from 321 | * @param {Object} [services] - already found services (parent module) can be null 322 | * @returns {Object|undefined} map of all the angular services linked to the specified module. 323 | * returns undefined if the module isn't defined 324 | */ 325 | function getNgModuleServices(moduleName, services){ 326 | 327 | if (!services) { 328 | services = {}; 329 | } 330 | 331 | var module; 332 | 333 | try { 334 | module = angular.module(moduleName); 335 | } catch(e){ 336 | return; 337 | } 338 | 339 | angular.forEach(module._invokeQueue, function(service) { 340 | if (service[0] === '$provide' && service[1] === 'service') { 341 | services[service[2][0]] = _angularInjector(service[2][0]); 342 | } 343 | }); 344 | 345 | angular.forEach(module.requires, function(dependencyModule) { 346 | getNgModuleServices(dependencyModule, services) 347 | }); 348 | 349 | return services; 350 | } 351 | 352 | /** 353 | * This methods adds timers to all the functions of all the services of a given module 354 | * 355 | * @param {String} moduleName - module name to instrument. 356 | */ 357 | function instrumentModuleServices(moduleName){ 358 | 359 | _backUp.modules[moduleName] = {}; 360 | var services = getNgModuleServices(moduleName); 361 | 362 | angular.forEach(Object.keys(services), function(serviceName){ 363 | 364 | _backUp.modules[moduleName][serviceName] = {}; 365 | var service = services[serviceName]; 366 | 367 | angular.forEach(Object.getOwnPropertyNames(service), function(propertyName){ 368 | 369 | // Early return for all properties that are not functions 370 | // arguments is a reserved property name 371 | if (propertyName === 'arguments' || 372 | propertyName === 'caller' || 373 | propertyName === 'callee' || 374 | typeof service[propertyName] !== 'function' || 375 | propertyName === 'constructor') { 376 | return; 377 | } 378 | 379 | var functionToWrap = _backUp.modules[moduleName][serviceName][propertyName] = service[propertyName]; 380 | 381 | // We Wrap all the service functions to measure execution time. 382 | service[propertyName] = function(){ 383 | var 384 | start = performance.now(), 385 | // Execute the function as usual 386 | result = functionToWrap.apply(this, arguments); 387 | 388 | // Register execution time in the extension registry 389 | register('SyncServiceFunctionCall', { 390 | module: moduleName, 391 | service: serviceName, 392 | func: propertyName, 393 | time: performance.now() - start 394 | }); 395 | 396 | // We only consider promises since it is the default async handling in angular. 397 | if (result && typeof result.then === 'function') { 398 | // We reset the timer so that only async time gets taken. 399 | start = performance.now(); 400 | 401 | // We Append our timing promise to the actual promise 402 | result.then(function() { 403 | register('ASyncServiceFunctionCall', { 404 | module: moduleName, 405 | service: serviceName, 406 | func: propertyName, 407 | time: performance.now() - start 408 | }); 409 | }) 410 | } 411 | return result; 412 | } 413 | }) 414 | }); 415 | } 416 | 417 | /** 418 | * Restores the services functions as they were before being wrapped 419 | * 420 | * @param {String} [moduleName] - name of the module to be unwrapped. If not mentioned, everything 421 | * should be restored. 422 | */ 423 | function restoreModuleServices(moduleName){ 424 | 425 | var modules; 426 | 427 | if (moduleName){ 428 | 429 | if (_backUp.modules[moduleName]) { 430 | modules = [moduleName]; 431 | } else { 432 | throw new Error('angular performance - We tried to restore the module '+ moduleName + 'but ' + 433 | 'we could not find any back up :('); 434 | } 435 | 436 | } else { 437 | modules = Object.keys(_backUp.modules); 438 | } 439 | 440 | angular.forEach(modules, function(module){ 441 | var services = getNgModuleServices(module); 442 | 443 | angular.forEach(Object.keys(_backUp.modules[module]), function(service){ 444 | angular.forEach(Object.keys(_backUp.modules[module][service]), function(fnName){ 445 | services[service][fnName] = _backUp.modules[module][service][fnName] 446 | }); 447 | }); 448 | 449 | // Clean up back up 450 | delete _backUp.modules[module]; 451 | }); 452 | } 453 | 454 | // ------------------------------------------------------------------------------------------ 455 | // Utils 456 | // ------------------------------------------------------------------------------------------ 457 | 458 | /** 459 | * Reports a metric 460 | * 461 | * @param {String} task - task to do 462 | * @param {Object} [value] - data that can be sent along with the task 463 | */ 464 | function sendTask(task, value){ 465 | window.postMessage({ 466 | source: 'angular-performance-inspector', 467 | task: task, 468 | data: value 469 | }, '*'); 470 | } 471 | 472 | /** 473 | * Register a metric into the devtool instance registry 474 | * 475 | * @param {String} variable - can be 'digestTiming' 476 | * @param {Object} value - value to be registered with the variable 477 | */ 478 | function register(variable, value){ 479 | sendTask('register'+variable, value); 480 | } 481 | })(); 482 | -------------------------------------------------------------------------------- /extension/src/panel/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Angular Performance 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 66 | 67 |
68 | 69 |
70 | 71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 |
79 |
80 |
0
81 |
Watchers
82 |
83 |
84 |
85 | 86 | 91 | 92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | 100 |
101 |
102 |
0 ms
103 |
Last Digest Time
104 |
105 |
106 |
107 | 108 | 113 | 114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | 122 |
123 |
124 |
0 /s
125 |
Digest/s
126 |
127 |
128 |
129 | 130 | 135 | 136 |
137 |
138 | 162 |
163 | 164 |
165 |
166 |
167 |
168 | Digest time (ms) 169 |
170 | 171 | 172 |
173 |
174 | 175 |
176 |
177 |
178 |
179 |
180 | 181 |
182 | 183 |
184 |
185 |
186 |
187 | Digest time Distribution 188 |
189 | 190 |
191 |
192 |
193 |
194 | 195 |
196 |
197 |
198 | 199 | 200 |
201 |
202 |
203 |
204 | Digest Rate 205 |
206 | 207 | 208 |
209 |
210 | 211 |
212 |
213 |
214 |
215 |
216 | 217 |
218 | 219 | 220 |
221 |
222 | 223 | 224 |
225 |
226 |
227 |
228 | Watchers 229 |
230 | 231 | 232 |
233 |
234 | 235 |
236 |
237 |
238 |
239 |
240 | 241 |
242 | 243 | 244 |
245 |
246 |
247 |
248 |
249 |
250 | Watchers average count depending on location 251 |
252 | 253 |
254 |
255 |
256 |
257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 |
idLocation
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 | 275 |
276 | 277 |
278 | 279 |
280 |
281 | 282 | 300 | 301 |
302 | 303 | 304 | 330 | 338 |
339 | 340 |
341 | 342 | 343 | 344 | 377 | 378 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | -------------------------------------------------------------------------------- /extension/src/vendors/sb-admin-2.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | $('#side-menu').metisMenu(); 4 | 5 | }); 6 | 7 | //Loads the correct sidebar on window load, 8 | //collapses the sidebar on window resize. 9 | // Sets the min-height of #page-wrapper to window size 10 | $(function() { 11 | $(window).bind("load resize", function() { 12 | topOffset = 50; 13 | width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width; 14 | if (width < 768) { 15 | $('div.navbar-collapse').addClass('collapse'); 16 | topOffset = 100; // 2-row-menu 17 | } else { 18 | $('div.navbar-collapse').removeClass('collapse'); 19 | } 20 | 21 | height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1; 22 | height = height - topOffset; 23 | if (height < 1) height = 1; 24 | if (height > topOffset) { 25 | $("#page-wrapper").css("min-height", (height) + "px"); 26 | } 27 | }); 28 | 29 | var url = window.location; 30 | var element = $('ul.nav a').filter(function() { 31 | return this.href == url || url.href.indexOf(this.href) == 0; 32 | }).addClass('active').parent().parent().addClass('in').parent(); 33 | if (element.is('li')) { 34 | element.addClass('active'); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /inch.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "included": [ 4 | "extension/src/**/*.js", 5 | "panelApp/**/*.js" 6 | ], 7 | "excluded": [ 8 | "extension/vendors/sb-admin-2.js" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-performance", 3 | "version": "0.1.0", 4 | "description": "Chrome plugin for angular performance monitoring", 5 | "dependencies": { 6 | "bootstrap": "^3.3.4", 7 | "font-awesome": "^4.3.0", 8 | "jquery": "^2.1.3", 9 | "jquery-ui": "^1.10.5", 10 | "lodash": "^3.6.0", 11 | "metismenu": "^2.0.0", 12 | "rickshaw": "^1.5.1" 13 | }, 14 | "devDependencies": { 15 | "browserify": "11.0.1", 16 | "clean-css": "^3.1.9", 17 | "rework-npm-cli": "^0.1.1", 18 | "uglify-js": "^2.4.19" 19 | }, 20 | "scripts": { 21 | "copyFonts": "cp ./node_modules/font-awesome/fonts/* ./extension/fonts/ & cp ./node_modules/bootstrap/fonts/* ./extension/fonts/", 22 | "copyBootstrap": "cp ./node_modules/bootstrap/dist/js/bootstrap.min.js ./extension/src/vendors/", 23 | "copyMetisMenu": "cp ./node_modules/metismenu/dist/metisMenu.min.js ./extension/src/vendors/", 24 | "copyJs": "npm run copyBootstrap & npm run copyMetisMenu", 25 | "compileVendorCSS": "rework-npm extension/css/dependencies.css | cleancss -o extension/css/vendors.css", 26 | "compilePanelJs": "browserify panelApp/main.js -o extension/src/panel/panel.js", 27 | "build": "npm run copyFonts & npm run copyJs & npm run compileVendorCSS & npm run compilePanelJs", 28 | "watch": "watchify panelApp/main.js -o extension/src/panel/panel.js --debug --verbose" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/Linkurious/angular-performance.git" 33 | }, 34 | "author": "Nicolas Joseph", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/Linkurious/angular-performance/issues" 38 | }, 39 | "homepage": "https://github.com/Linkurious/angular-performance" 40 | } 41 | -------------------------------------------------------------------------------- /panelApp/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Browserify isolates loaded modules, dependencies need jquery as global 4 | window.jQuery = window.$ = require('jquery'); 5 | 6 | var _ = require('lodash'); 7 | 8 | var 9 | ServicePanelCtrl = require('./panels/servicePanelController'), 10 | Registry = require('./models/registry'), 11 | TabsHandler = require('./tabHandler'), 12 | Plots = require('./panels/plots'), 13 | InstantMetrics = require('./panels/instantMetrics'), 14 | SettingsPanelCtrl = require('./panels/settingsPanelController'), 15 | backgroundPageConnection = chrome.runtime.connect({ 16 | name: "angular-performance-panel" 17 | }); 18 | 19 | var 20 | registry = new Registry(), 21 | servicePanelCtrl = new ServicePanelCtrl(backgroundPageConnection, registry); 22 | 23 | // Initialize all services with the registry (should be a singleton) 24 | InstantMetrics.initRegistry(registry); 25 | Plots.initRegistry(registry); 26 | 27 | var 28 | tabs = new TabsHandler(Plots), 29 | // For now the reference of the settings is not used by the other services but it could in the future. 30 | settingsPanelCtrl = new SettingsPanelCtrl(registry, Plots, tabs); 31 | 32 | // Listen to the message sent by the injected script 33 | backgroundPageConnection.onMessage.addListener(function(message){ 34 | 35 | switch(message.task){ 36 | case 'registerDigestTiming': 37 | InstantMetrics.updateDigestTiming(message.data.time); 38 | registry.registerDigestTiming(message.data.timestamp, message.data.time); 39 | break; 40 | case 'registerEvent': 41 | registry.registerEvent(message.data.timestamp, message.data.event); 42 | break; 43 | case 'registerRootWatcherCount': 44 | InstantMetrics.updateWatcherCount(message.data.watcher.watcherCount); 45 | registry.registerWatcherCount(message.data.timestamp, message.data.watcher); 46 | break; 47 | case 'registerSyncServiceFunctionCall': 48 | registry.registerSyncExecution(message.data); 49 | servicePanelCtrl.refreshModulePanels(); 50 | break; 51 | case 'registerASyncServiceFunctionCall': 52 | registry.registerASyncExecution(message.data); 53 | servicePanelCtrl.refreshModulePanels(); 54 | break; 55 | case 'reportModuleExistence': 56 | servicePanelCtrl.printModuleNameCheck(message.data.moduleName, message.data.services); 57 | break; 58 | default: 59 | console.log('Unknown task ', message); 60 | break; 61 | } 62 | }); 63 | 64 | // The panel is initialized, send reference for message dispatch 65 | backgroundPageConnection.postMessage({ 66 | task: 'init', 67 | tabId: chrome.devtools.inspectedWindow.tabId 68 | }); 69 | 70 | // Set up the timed retrieval of digest count 71 | InstantMetrics.listenToDigestCount(registry); 72 | 73 | // Set up plots of the main tab 74 | Plots.setMainPlotsSettings([ 75 | { 76 | id: 'digest-time-chart', 77 | eventTimelineId: 'digest-time-event-timeline', 78 | rangeSliderId: 'digest-time-range-slider', 79 | plotName: 'Digest time length', 80 | dataFunction: registry.getDigestTimingPlotData, 81 | pauseButton: '#pauseDigestTime', 82 | liveButton: '#liveDigestTime', 83 | exportButton: '#exportDigestTime', 84 | live: true 85 | }, 86 | { 87 | id: 'digest-rate-chart', 88 | eventTimelineId: 'digest-rate-event-timeline', 89 | rangeSliderId: 'digest-rate-range-slider', 90 | plotName: 'Digest count', 91 | dataFunction: registry.getDigestRatePlotData, 92 | pauseButton: '#pauseDigestCount', 93 | liveButton: '#liveDigestCount', 94 | live: true 95 | }, 96 | { 97 | id: 'digest-time-distribution-chart', 98 | rangeSliderId: 'digest-time-distribution-range-slider', 99 | plotName: 'Digest time distribution', 100 | dataFunction: registry.getDigestTimeDistributionPlotData, 101 | renderer: 'bar', 102 | xAxis: 'numerical', 103 | live: true 104 | }, 105 | { 106 | id: 'watchers-count-chart', 107 | eventTimelineId: 'watchers-event-timeline', 108 | rangeSliderId: 'watchers-range-slider', 109 | plotName: 'Watchers count', 110 | dataFunction: registry.getWatchersCountPlotData, 111 | pauseButton: '#pauseWatchersCount', 112 | liveButton: '#liveWatchersCount', 113 | live: true 114 | }, 115 | { 116 | id: 'watcher-count-distribution-chart', 117 | rangeSliderId: 'watcher-count-distribution-range-slider', 118 | plotName: 'Watcher Count distribution', 119 | dataFunction: registry.getWatchersCountDistributionPlotData, 120 | renderer: 'bar', 121 | xAxis: 'numerical', 122 | live: true, 123 | callback: function(){ 124 | $('#id-location-table').empty(); 125 | var locationMap = registry.getLocationMap(); 126 | 127 | _.forEach(Object.keys(locationMap), function(location){ 128 | $('#id-location-table').append(''+ locationMap[location] +''+ location +''); 129 | }); 130 | } 131 | } 132 | ]); 133 | 134 | Plots.buildMainPlots(); 135 | tabs.bindTabs(); 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /panelApp/models/registry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | /** 6 | * Object holding all the values that will be displayed in the interface 7 | * 8 | * @constructor 9 | */ 10 | function Registry(){ 11 | 12 | var self = this; 13 | 14 | var 15 | BUFFER_MAX_ELEMENT = 300; 16 | 17 | var 18 | // Raw data 19 | _digestTiming = [], 20 | _digestTimingDistribution = {}, 21 | _events = [], 22 | _watcherCount = [], 23 | _watcherCountDistribution = {}, 24 | _functionTimings = {}, 25 | 26 | // Plot data 27 | _digestTimingDistributionPlotData = [], 28 | _digestTimingPlotData = [], 29 | _digestRatePlotData = [], 30 | _watcherCountPlotData = [], 31 | _watcherCountDistributionPlotData = [], 32 | 33 | _locationMap = {}, 34 | _locationCount = 0; 35 | 36 | // ------------------------------------------------------------------------------------------ 37 | // Registry helpers 38 | // ------------------------------------------------------------------------------------------ 39 | 40 | /** 41 | * Cleans up all data of the registry to start fresh. We clean the existing object to keep 42 | * the references so that we don't have to re instantiate Rickshaw plots. 43 | */ 44 | self.clearData = function(){ 45 | 46 | _.forEach([_digestTiming, _events, _watcherCount, _digestTimingDistributionPlotData, 47 | _digestTimingPlotData, _digestRatePlotData, _watcherCountPlotData, 48 | _watcherCountDistributionPlotData], function(array){ 49 | 50 | for (var i = 0, len = array.length; i -1 && now - ((number-1) * resolution) < _digestTiming[i].timestamp; i-- ){ 119 | 120 | for (var index = number - 1, bin = 0 ; index > -1 ; index--) { 121 | if ((now - (resolution * (bin + 1)) < _digestTiming[i].timestamp && 122 | _digestTiming[i].timestamp < now - (resolution * bin))) { 123 | 124 | _digestTimingPlotData[index] = { x: now - (resolution * bin), y: (_digestTimingPlotData[index].y + _digestTiming[i].time) / 2}; 125 | break; 126 | } 127 | bin++; 128 | } 129 | } 130 | 131 | return _digestTimingPlotData; 132 | }; 133 | 134 | /** 135 | * Gets the data to be fed into the plotting function for digest rate 136 | * 137 | * @param {Number} [number] - number of item to return, default to 100 138 | * @param {Number} [resolution] - time between the sampled points in ms 139 | * @returns {Array[]} - array of array containing each [x, y] 140 | */ 141 | self.getDigestRatePlotData = function(number, resolution){ 142 | 143 | _.forEach(_digestRatePlotData, function(){ 144 | _digestRatePlotData.shift(); 145 | }); 146 | 147 | if (!number){ 148 | number = 300; 149 | } 150 | if (!resolution){ 151 | resolution = 1000; 152 | } 153 | 154 | var 155 | now = Date.now(), 156 | i; 157 | 158 | for (i = 0; i < number ; i++){ 159 | _digestRatePlotData.push({x: now - ((number - i - 1) * resolution), y: 0}) 160 | } 161 | 162 | for (i = _digestTiming.length - 1; i > -1 && now - ((number-1) * resolution) < _digestTiming[i].timestamp; i-- ){ 163 | 164 | for (var index = number - 1, bin = 0 ; index > -1 ; index--) { 165 | if ((now - (resolution * (bin + 1)) < _digestTiming[i].timestamp && 166 | _digestTiming[i].timestamp < now - (resolution * bin))) { 167 | 168 | _digestRatePlotData[index] = {x: now - (resolution * bin), y: _digestRatePlotData[index].y + 1}; 169 | break; 170 | } 171 | bin++; 172 | } 173 | } 174 | 175 | return _digestRatePlotData; 176 | }; 177 | 178 | /** 179 | * Get the distribution of the timings in for the digest data 180 | * 181 | * @returns {Array} 182 | */ 183 | self.getDigestTimeDistributionPlotData = function(){ 184 | 185 | _.forEach(_digestTimingDistributionPlotData, function(){ 186 | _digestTimingDistributionPlotData.shift(); 187 | }); 188 | 189 | _.forEach(Object.keys(_digestTimingDistribution), function(key){ 190 | _digestTimingDistributionPlotData.push({x: parseInt(key, 10), y: _digestTimingDistribution[key]}); 191 | }); 192 | 193 | // This is needed for graph init at the beginning of the session 194 | if (_digestTimingDistributionPlotData.length === 0){ 195 | _digestTimingDistributionPlotData.push({x: 0, y: 0}); 196 | } 197 | 198 | return _digestTimingDistributionPlotData; 199 | }; 200 | 201 | /** 202 | * Gets the last registered timing according to the order of registration 203 | * 204 | * @returns {Object|null} timing - null if no element registered yet 205 | * @returns {Number} timing.timestamp - start time of the digest 206 | * @returns {Number} timing.time - length of the digest 207 | */ 208 | self.getLastDigestTiming = function(){ 209 | return (_digestTiming.length > 0) ? _digestTiming[_digestTiming.length-1] : null ; 210 | }; 211 | 212 | /** 213 | * Returns digest/second instant 214 | * 215 | * @returns {number} 216 | */ 217 | self.getLastSecondDigestCount = function(){ 218 | var 219 | now = Date.now(), 220 | count = 0; 221 | 222 | // We go through the digest timing array from the most recent to the least recent entry. We stop 223 | // when the time difference between now and the entry exceeds 500ms. 224 | for (var i = _digestTiming.length - 1 ; i > -1 && _digestTiming[i].timestamp > now - 1000 ; i--){ 225 | count++; 226 | } 227 | 228 | return count; 229 | }; 230 | 231 | 232 | // ------------------------------------------------------------------------------------------ 233 | // Events 234 | // ------------------------------------------------------------------------------------------ 235 | 236 | /** 237 | * Register a user event 238 | * 239 | * @param {Number} timestamp - js timestamp when the event was fired 240 | * @param {Object} event - event to be registered 241 | * @param {String} event.type - event type ex: 'onclick' 242 | * @param {String} event.targetDOMPath - DOM path of the targeted event. 243 | */ 244 | self.registerEvent = function(timestamp, event){ 245 | if (_events.length === BUFFER_MAX_ELEMENT){ 246 | _events.shift(); 247 | } 248 | _events.push({ 249 | timestamp: timestamp, 250 | event: event 251 | }); 252 | }; 253 | 254 | /** 255 | * Get event data corresponding to a dataSet. 256 | * 257 | * @param {String} chart - chart to get the event data for. Can be 'digest-time-chart' or 258 | * 'digest-rate-chart' 259 | * @returns {Array[]} 260 | */ 261 | self.getLastEventAnnotatorData = function(chart){ 262 | 263 | var 264 | data = [], 265 | start; 266 | 267 | if (chart === 'digest-time-chart'){ 268 | if (_digestTimingPlotData.length === 0){ 269 | return data; 270 | } 271 | 272 | start = _digestTimingPlotData[_digestTimingPlotData.length - 1].x; 273 | } else if (chart === 'digest-rate-chart'){ 274 | if (_digestRatePlotData.length === 0){ 275 | return data; 276 | } 277 | 278 | start = _digestRatePlotData[_digestRatePlotData.length - 1].x; 279 | } 280 | 281 | for (var i = _events.length - 1 ; i > -1 && _events[i].timestamp > start ; i--){ 282 | data.push({ 283 | timestamp: _events[i].timestamp, 284 | message: 'Event: '+_events[i].event.type+' on '+_events[i].event.targetDOMPath 285 | }); 286 | } 287 | 288 | return data; 289 | }; 290 | 291 | // ------------------------------------------------------------------------------------------ 292 | // Watchers 293 | // ------------------------------------------------------------------------------------------ 294 | 295 | /** 296 | * Registers instant data on watcher 297 | * 298 | * @param {Number} timestamp - corresponds to the timestamp of the take metric 299 | * @param {Object} watcher - saved watcher data 300 | * @param {Number} watcher.watcherCount - number of watchers counted 301 | * @param {String} watcher.location - current url of the taken metric 302 | */ 303 | self.registerWatcherCount = function(timestamp, watcher){ 304 | if (_watcherCount.length === BUFFER_MAX_ELEMENT){ 305 | _watcherCount.shift(); 306 | } 307 | _watcherCount.push({ 308 | timestamp: timestamp, 309 | watcher: watcher 310 | }); 311 | 312 | if (_watcherCountDistribution[watcher.location]){ 313 | _watcherCountDistribution[watcher.location] = Math.round((_watcherCountDistribution[watcher.location] + watcher.watcherCount) / 2); 314 | } else { 315 | _watcherCountDistribution[watcher.location] = watcher.watcherCount; 316 | _locationMap[watcher.location] = _locationCount; 317 | _locationCount++; 318 | } 319 | }; 320 | 321 | /** 322 | * Gets the watchers plotted data as for the watcher count history. 323 | * The plotted data is a progressively filled graph. Once the buffer is full, the time flies. 324 | * 325 | * @returns {Array[]} 326 | */ 327 | self.getWatchersCountPlotData = function(){ 328 | 329 | // Empty array 330 | _.forEach(_watcherCountPlotData, function(){ 331 | _watcherCountPlotData.shift(); 332 | }); 333 | 334 | var i; 335 | 336 | // Get the last watcher count registrations 337 | for (i = _watcherCount.length - 1; i > -1 ; i-- ){ 338 | _watcherCountPlotData[i] = {x: _watcherCount[i].timestamp, y: _watcherCount[i].watcher.watcherCount} 339 | } 340 | 341 | // This is needed for graph init at the beginning of the session 342 | if (_watcherCountPlotData.length === 0){ 343 | _watcherCountPlotData.push({x: 0, y: 0}); 344 | } 345 | 346 | return _watcherCountPlotData; 347 | }; 348 | 349 | /** 350 | * Gets the distribution of the watcher count according to the location of the page. 351 | * 352 | * @returns {Array[]} 353 | */ 354 | self.getWatchersCountDistributionPlotData = function(){ 355 | // Empty array 356 | _.forEach(_watcherCountDistributionPlotData, function(){ 357 | _watcherCountDistributionPlotData.shift(); 358 | }); 359 | 360 | _.forEach(Object.keys(_watcherCountDistribution), function(key){ 361 | _watcherCountDistributionPlotData.push({x: _locationMap[key], y: _watcherCountDistribution[key]}); 362 | }); 363 | 364 | // This is needed for graph init at the beginning of the session 365 | if (_watcherCountDistributionPlotData.length === 0){ 366 | _watcherCountDistributionPlotData.push({x: 0, y: 0}); 367 | } 368 | 369 | return _watcherCountDistributionPlotData; 370 | }; 371 | 372 | // ------------------------------------------------------------------------------------------ 373 | // Service Functions execution times 374 | // ------------------------------------------------------------------------------------------ 375 | 376 | /** 377 | * Registers a sync function execution into the registry. The data structure is stored as follow in 378 | * _functionTimings: 379 | * 380 | * The first key represents the angular module 381 | * The second key represents the service 382 | * The last key represents the function 383 | * 384 | * ex: 385 | * _functionTiming['ng-app']['MainService']['addSomething'] 386 | * 387 | * That objects stores the number of calls to the function, the average sync execution time and the 388 | * async execution time. 389 | * 390 | * @param {Object} executionMessage - Object representing a function execution 391 | * @param {String} executionMessage.module - module containing the service from which is executed 392 | * the function 393 | * @param {String} executionMessage.service - service from which is executed the function 394 | * @param {String} executionMessage.func - the executed function 395 | * @param {String} executionMessage.time - the sync execution time of the function 396 | */ 397 | self.registerSyncExecution = function(executionMessage){ 398 | 399 | if (!_functionTimings[executionMessage.module]){ 400 | _functionTimings[executionMessage.module] = {}; 401 | } 402 | 403 | if (!_functionTimings[executionMessage.module][executionMessage.service]){ 404 | _functionTimings[executionMessage.module][executionMessage.service] = {}; 405 | } 406 | 407 | var service = _functionTimings[executionMessage.module][executionMessage.service]; 408 | 409 | if (!service[executionMessage.func]){ 410 | service[executionMessage.func] = { 411 | count: 1, 412 | syncExecTime: Math.round(executionMessage.time), 413 | asyncExecTime: 0, 414 | impactScore: Math.round(executionMessage.time) /100 415 | } 416 | } else { 417 | service[executionMessage.func].count++; 418 | service[executionMessage.func].syncExecTime = 419 | Math.round((service[executionMessage.func].syncExecTime + executionMessage.time) / 2); 420 | service[executionMessage.func].impactScore = 421 | (service[executionMessage.func].syncExecTime + service[executionMessage.func].asyncExecTime) * service[executionMessage.func].count / 100; 422 | } 423 | }; 424 | 425 | /** 426 | * Registers an qsync function execution into the registry. 427 | * 428 | * @see registerSyncExecution for data structure 429 | * 430 | * @param {Object} executionMessage - Object representing a function execution 431 | * @param {String} executionMessage.module - module containing the service from which is executed 432 | * the function 433 | * @param {String} executionMessage.service - service from which is executed the function 434 | * @param {String} executionMessage.func - the executed function 435 | * @param {String} executionMessage.time - the async execution time of the function 436 | */ 437 | self.registerASyncExecution = function(executionMessage){ 438 | var service = _functionTimings[executionMessage.module][executionMessage.service]; 439 | 440 | service[executionMessage.func].asyncExecTime = 441 | Math.round((service[executionMessage.func].asyncExecTime + executionMessage.time) / 2); 442 | service[executionMessage.func].impactScore = 443 | (service[executionMessage.func].syncExecTime + service[executionMessage.func].asyncExecTime) * service[executionMessage.func].count / 100; 444 | }; 445 | 446 | /** 447 | * Gets the module data function execution in a sorted array by most impact first 448 | * 449 | * @param {String} module - module to get the data from 450 | * @param {Number} [limit] - only returns the most impacting function executions 451 | * @returns {Array} 452 | */ 453 | self.getModuleFunctionsExecutionData = function(module, limit){ 454 | 455 | var toReturn = []; 456 | 457 | _.forEach(_functionTimings[module], function(functions, service){ 458 | _.forEach(functions, function(execData, func){ 459 | toReturn.push({ 460 | service: service, 461 | func: func, 462 | syncExecTime: execData.syncExecTime, 463 | asyncExecTime: execData.asyncExecTime, 464 | count: execData.count, 465 | impactScore: execData.impactScore 466 | }); 467 | }); 468 | }); 469 | 470 | // We want to have result from the function that has most impact on the software to the one that 471 | // has the less impact. 472 | toReturn = _.sortBy(toReturn, function(execData){ 473 | return - execData.impactScore 474 | }); 475 | 476 | if(limit){ 477 | toReturn = _.take(toReturn, limit); 478 | } 479 | 480 | return toReturn; 481 | }; 482 | 483 | /** 484 | * Clears out all the registered data for a registered module 485 | * 486 | * @param {String} moduleName - name of the module to clear the data from 487 | */ 488 | self.clearModuleFunctionExecutionData = function(moduleName){ 489 | _functionTimings[moduleName] = {}; 490 | }; 491 | 492 | /** 493 | * Gets the location map that makes the correspondence between the Location and their id 494 | * 495 | * @returns {{}} 496 | */ 497 | self.getLocationMap = function(){ 498 | return _locationMap; 499 | }; 500 | } 501 | 502 | module.exports = Registry; 503 | -------------------------------------------------------------------------------- /panelApp/panels/instantMetrics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var 4 | $ = require('jquery'); 5 | 6 | var 7 | instantMetrics = {}, 8 | _registry = null, 9 | _REFRESH_RATE = 300, 10 | _digestCountTimeout = null; 11 | 12 | /** 13 | * Initializes the registry reference. 14 | * 15 | * @param {Object} registry - registry object of the app. This should be the current instance reference. 16 | */ 17 | instantMetrics.initRegistry = function(registry){ 18 | _registry = registry; 19 | }; 20 | 21 | /** 22 | * Updates the panel with the last digest length 23 | * 24 | * @param {Number} time - time of the digest in ms 25 | */ 26 | instantMetrics.updateDigestTiming = function(time){ 27 | $('#instantDigestTime').text(Math.ceil(time)); 28 | }; 29 | 30 | /** 31 | * Updates the panel with the last second 32 | */ 33 | instantMetrics.updateDigestCount = function(){ 34 | $('#instantDigestRate').text(_registry.getLastSecondDigestCount()); 35 | }; 36 | 37 | /** 38 | * Updates the panel with the last Watcher count 39 | * 40 | * @param {Number} count - watcher count to be displayed in the instant panel. 41 | */ 42 | instantMetrics.updateWatcherCount = function(count){ 43 | $('#instantWatcherCount').text(count); 44 | }; 45 | 46 | /** 47 | * Gets the digest count every _REFRESH_RATE seconds 48 | */ 49 | instantMetrics.listenToDigestCount = function(){ 50 | instantMetrics.updateDigestCount(); 51 | _digestCountTimeout = setTimeout(instantMetrics.listenToDigestCount, _REFRESH_RATE); 52 | }; 53 | 54 | module.exports = instantMetrics; -------------------------------------------------------------------------------- /panelApp/panels/plots.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Rickshaw = require('rickshaw'); 4 | var $ = require('jquery'); 5 | var _ = require('lodash'); 6 | 7 | require('jquery-ui'); 8 | 9 | var 10 | _UPDATE_INTERVAL = 1000, 11 | _COLOR_PALETTE = new Rickshaw.Color.Palette( { scheme: 'classic9' }), 12 | 13 | _registry = null, 14 | _mainPlotsSettings = [], 15 | 16 | Plots = {}; 17 | 18 | /** 19 | * Initializes the registry reference. 20 | * 21 | * @param {Object} registry - registry object of the app. This should be the current instance reference. 22 | */ 23 | Plots.initRegistry = function(registry){ 24 | _registry = registry; 25 | }; 26 | 27 | /** 28 | * Sets the different plots of the panel 29 | * 30 | * @param {Object[]} settingsArray - Array of settings 31 | */ 32 | Plots.setMainPlotsSettings = function(settingsArray){ 33 | _mainPlotsSettings = settingsArray; 34 | }; 35 | 36 | /** 37 | * Stops the main plots rendering to spare to computing resources 38 | */ 39 | Plots.stopMainPlotRendering = function(){ 40 | _.forEach(_mainPlotsSettings, function(plot){ 41 | plot.live = false; 42 | }); 43 | }; 44 | 45 | /** 46 | * Turns on auto-refresh on the main plots. 47 | */ 48 | Plots.startMainPlotRendering = function(){ 49 | _.forEach(_mainPlotsSettings, function(plot){ 50 | plot.live = true; 51 | plot.updateFunction(); 52 | }) 53 | }; 54 | 55 | /** 56 | * Builds the plots on the main page 57 | */ 58 | Plots.buildMainPlots = function(){ 59 | 60 | _.forEach(_mainPlotsSettings, function(plot){ 61 | 62 | $('#' + plot.id).empty(); 63 | 64 | if (plot.eventTimelineId) { 65 | $('#' + plot.eventTimelineId).empty(); 66 | } 67 | if (plot.rangeSliderId) { 68 | $('#' + plot.rangeSliderId).empty(); 69 | } 70 | 71 | 72 | // Controls of the start en end date 73 | if (plot.pauseButton && plot.liveButton) { 74 | $(plot.liveButton).unbind(); 75 | $(plot.pauseButton).unbind(); 76 | 77 | $(plot.liveButton).click(function () { 78 | plot.live = true; 79 | $(plot.pauseButton).removeClass('active'); 80 | $(plot.liveButton).addClass('active'); 81 | plot.updateFunction(); 82 | }); 83 | 84 | $(plot.pauseButton).click(function () { 85 | plot.live = false; 86 | $(plot.liveButton).removeClass('active'); 87 | $(plot.pauseButton).addClass('active'); 88 | }); 89 | } 90 | 91 | // Graph instantiation 92 | plot.instance = new Rickshaw.Graph({ 93 | element: document.getElementById(plot.id), 94 | renderer: (plot.renderer) ? plot.renderer : 'line', 95 | stroke: true, 96 | preserve: true, 97 | series: [ 98 | { 99 | color: _COLOR_PALETTE.color(), 100 | data: plot.dataFunction(), 101 | name: plot.plotName 102 | } 103 | ] 104 | }); 105 | 106 | plot.slider = new Rickshaw.Graph.RangeSlider.Preview({ 107 | graph: plot.instance, 108 | element: document.getElementById(plot.rangeSliderId) 109 | }); 110 | 111 | plot.instance.render(); 112 | 113 | if (plot.eventTimelineId) { 114 | plot.annotator = new Rickshaw.Graph.Annotate({ 115 | graph: plot.instance, 116 | element: document.getElementById(plot.eventTimelineId) 117 | }); 118 | } 119 | 120 | if (!plot.xAxis) { 121 | plot.xAxis = new Rickshaw.Graph.Axis.Time({ 122 | graph: plot.instance, 123 | timeFixture: new Rickshaw.Fixtures.Time.Local() 124 | }); 125 | } else { 126 | plot.xAxis = new Rickshaw.Graph.Axis.X( { 127 | graph: plot.instance 128 | }); 129 | } 130 | 131 | plot.xAxis.render(); 132 | 133 | plot.yAxis = new Rickshaw.Graph.Axis.Y( { 134 | graph: plot.instance, 135 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT 136 | }); 137 | 138 | plot.yAxis.render(); 139 | 140 | plot.updateFunction = function(){ 141 | 142 | if (plot.eventTimelineId) { 143 | _.forEach(_registry.getLastEventAnnotatorData(plot.id), function (event) { 144 | plot.annotator.add(event.timestamp, event.message); 145 | }); 146 | plot.annotator.update(); 147 | } 148 | plot.dataFunction(); 149 | plot.instance.update(); 150 | 151 | if (plot.callback){ 152 | plot.callback(); 153 | } 154 | 155 | if (plot.live) { 156 | setTimeout(plot.updateFunction, _UPDATE_INTERVAL); 157 | } 158 | }; 159 | 160 | plot.updateFunction(); 161 | }); 162 | }; 163 | 164 | /** 165 | * Cleans up annotations registered in the event bar . 166 | */ 167 | Plots.clearAnnotations = function(){ 168 | _.forEach(_mainPlotsSettings, function(plot){ 169 | if (plot.annotator) { 170 | _.forEach(Object.keys(plot.annotator.data), function(timestamp){ 171 | delete plot.annotator.data[timestamp]; 172 | }) 173 | } 174 | }); 175 | 176 | $('.annotation').remove(); 177 | }; 178 | module.exports = Plots; 179 | -------------------------------------------------------------------------------- /panelApp/panels/servicePanelController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var 4 | _ = require('lodash'), 5 | $ = require('jquery'); 6 | 7 | /** 8 | * Controller used for the Service panel. It directly modifies the interface 9 | * 10 | * @param {Port} pageConnection - direct connection to the background script 11 | * @param {Registry} registry - registry containing all the reported values 12 | * @constructor 13 | */ 14 | function ServicePanelController (pageConnection, registry){ 15 | 16 | var 17 | self = this, 18 | 19 | _newModuleNameInput = $('#module-name'), 20 | _newModuleServices = $('#module-services'), 21 | _addedModules = []; 22 | 23 | // We want to check if the module name is correct only when the user has finished typing. 24 | // We consider this to be when he has stopped for more than 300ms. 25 | _newModuleNameInput.keyup(_.debounce(function(){ 26 | sendTaskToInspector({ 27 | task: 'checkModuleName', 28 | moduleName: _newModuleNameInput.val() 29 | }); 30 | }, 300)); 31 | 32 | $('#addModuleModalApplyButton').click(function(){ 33 | self.addModulePanel(_newModuleNameInput.val()); 34 | sendTaskToInspector({ 35 | task: 'instrumentModuleServices', 36 | moduleName: _newModuleNameInput.val() 37 | }); 38 | $('#addModuleModal').modal('hide'); 39 | }); 40 | 41 | /** 42 | * Prints into the modal if the module name is correct or not. Along with that, it also prints 43 | * the list of services that are required by that module. 44 | * 45 | * @param {String} moduleName - Module name that was checked by the inspector 46 | * @param {String[]} [services] - list of services defined by the module. If undefined, it means 47 | * that the module does not exist. 48 | */ 49 | self.printModuleNameCheck = function(moduleName, services){ 50 | 51 | var parent = _newModuleNameInput.parent(); 52 | 53 | // We want to check that the user has not changed the name in the input since the check task 54 | // was sent to the inspector 55 | if (services && moduleName === _newModuleNameInput.val()){ 56 | parent.removeClass('has-error'); 57 | $('#addModuleModalApplyButton').removeAttr('disabled'); 58 | parent.addClass('has-success'); 59 | 60 | _newModuleServices.empty(); 61 | 62 | var ul = $('
    '); 63 | _.forEach(services.sort(), function(service){ 64 | ul.append('
  • ' + service + '
  • '); 65 | }); 66 | 67 | _newModuleServices.append(ul); 68 | } else { 69 | parent.removeClass('has-success'); 70 | parent.addClass('has-error'); 71 | $('#addModuleModalApplyButton').attr('disabled', true); 72 | _newModuleServices 73 | .empty() 74 | .append('

    The Module name is incorrect

    '); 75 | } 76 | }; 77 | 78 | /** 79 | * Adds a panel to the Module instrumentation panel 80 | * 81 | * @param {String} moduleName - name of the module to instrument 82 | */ 83 | self.addModulePanel = function(moduleName){ 84 | $('#detailTimingTab').append('' + 85 | '
    ' + 86 | '
    ' + 87 | '
    '+ 88 | '
    '+ 89 | ' Module: ' + moduleName + 90 | /* TODO code instrumentation cleanup in the inspector 91 | '
    ' + 92 | '
    '+ 93 | '' + 96 | '
    '+ 97 | '
    ' + 98 | */ 99 | '
    '+ 100 | 101 | '
    ' + 102 | '
    ' + 103 | '
    ' + 104 | '
    ' + 105 | '
    '+ 106 | '' + 107 | '
    '+ 108 | '' + 115 | '
    '+ 116 | '
    '+ 117 | '
    ' + 118 | '
    ' + 119 | '
    ' + 120 | '
    ' + 121 | '
    '+ 122 | '
    '+ 123 | '' + 124 | '
    '+ 125 | '
    '+ 126 | '
    ' + 127 | '
    ' + 128 | '
    ' + 129 | '
    '+ 130 | '
    ' + 131 | '
    ' + 132 | '
    '); 133 | 134 | _addedModules.push(moduleName); 135 | 136 | var panel = $('#panelFor' + moduleName); 137 | 138 | panel.find('input').bind('keyup mouseup', _.debounce(function(){ 139 | self.refreshModulePanels(); 140 | }, 200)); 141 | 142 | panel.find('.removeModuleInstrumentationButton').click(function(){ 143 | panel.remove(); 144 | _.remove(_addedModules, function(value){ 145 | return value === moduleName; 146 | }); 147 | }); 148 | 149 | panel.find('form').keypress(function(e){ 150 | var charCode = e.charCode || e.keyCode || e.which; 151 | if (charCode === 13) { 152 | return false; 153 | } 154 | }); 155 | 156 | $('#clearDataFor' + moduleName).click(function(){ 157 | registry.clearModuleFunctionExecutionData(moduleName); 158 | self.refreshModulePanels(); 159 | }); 160 | }; 161 | 162 | /** 163 | * Refreshes the tables for all the modules at most every 500 ms 164 | */ 165 | self.refreshModulePanels = _.throttle(function(){ 166 | 167 | var table, panel, panelBody; 168 | 169 | _.forEach(_addedModules, function(module){ 170 | 171 | panel = $('#panelFor' + module); 172 | panelBody = panel.find('.table-responsive'); 173 | panelBody.empty(); 174 | 175 | // TODO Can be included in the addModule Function for more efficiency 176 | table = $('' + 177 | '
    ' + 178 | '' + 179 | '' + 180 | '' + 181 | '' + 182 | '' + 183 | '' + 184 | '' + 185 | '' + 186 | '' + 187 | '' + 188 | '' + 189 | '' + 190 | '' + 191 | '' + 192 | '
    #ServiceFunctionSync Execution Time (ms)ASync Execution Time (ms) Execution Count Impact Score
    ' + 193 | '
    '); 194 | 195 | _.forEach(registry.getModuleFunctionsExecutionData(module, panel.find('input').val()), function(execData, index){ 196 | table.find('tbody').append('' + 197 | '' + 198 | '' + (index+1) + '' + 199 | '' + execData.service + '' + 200 | '' + execData.func + '' + 201 | '' + execData.syncExecTime + '' + 202 | '' + execData.asyncExecTime + '' + 203 | '' + execData.count + '' + 204 | '' + execData.impactScore + '' + 205 | ''); 206 | }); 207 | 208 | panel.find('.panel-body').append(table); 209 | }); 210 | }, 500); 211 | 212 | // ------------------------------------------------------------------------------------------ 213 | // Private Functions 214 | // ------------------------------------------------------------------------------------------ 215 | 216 | /** 217 | * @private 218 | * Sends a task to the inspector through the background page message dispatcher 219 | * 220 | * @param {Object} task - task to be executed by the Inspector. 221 | * @param {String} task.task - This is the string that reference the task to be done 222 | */ 223 | function sendTaskToInspector(task){ 224 | pageConnection.postMessage({ 225 | task: 'sendTaskToInspector', 226 | tabId: chrome.devtools.inspectedWindow.tabId, 227 | data: task 228 | }); 229 | } 230 | } 231 | 232 | module.exports = ServicePanelController; -------------------------------------------------------------------------------- /panelApp/panels/settingsPanelController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var 4 | $ = require('jquery'); 5 | 6 | function SettingsPanelCtrl(registry, plots, tabs){ 7 | 8 | $('#clearDataButton').click(function(){ 9 | registry.clearData(); 10 | plots.clearAnnotations(); 11 | tabs.goToTab(tabs.TABS.HOME); 12 | }); 13 | } 14 | 15 | module.exports = SettingsPanelCtrl; 16 | -------------------------------------------------------------------------------- /panelApp/tabHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | var _ = require('lodash'); 5 | 6 | /** 7 | * This module handles the change between the panel tabs. 8 | * It both handles showing/hiding elements and stopping rendering when not necessary. 9 | * 10 | * @param {Object} plots - Plot object of the panel app. 11 | */ 12 | function tabHandler(plots){ 13 | 14 | var self = this; 15 | 16 | 17 | self.TABS = { 18 | HOME: 'homeTab', 19 | SCOPE: 'scopeTab', 20 | TIMING: 'detailTimingTab', 21 | SETTINGS: 'settingsTab' 22 | }; 23 | 24 | var 25 | _plots = plots, 26 | _currentTab = self.TABS.HOME; 27 | 28 | /** 29 | * This method allows navigation through tabs in the panel. 30 | * 31 | * @param {String} tabName - name of the tab to go to. 32 | */ 33 | self.goToTab = function(tabName){ 34 | 35 | if (tabName === _currentTab ){ 36 | return; 37 | } 38 | if (!_.includes(_.values(self.TABS), tabName)) { 39 | throw new Error('tabHandler.js - Unrecognized tab name'); 40 | } 41 | 42 | if (_currentTab === self.TABS.HOME){ 43 | _plots.stopMainPlotRendering(); 44 | } 45 | 46 | $('#' + _currentTab).hide(); 47 | $('#' + _currentTab + 'Button').removeClass('active'); 48 | $('#' + tabName).show(); 49 | $('#' + tabName + 'Button').addClass('active'); 50 | 51 | if (tabName === self.TABS.HOME){ 52 | _plots.startMainPlotRendering(); 53 | } 54 | 55 | _currentTab = tabName; 56 | }; 57 | 58 | /** 59 | * Binds the tabs to the the click listeners. 60 | */ 61 | self.bindTabs = function(){ 62 | _.forEach(Object.keys(self.TABS), function(TAB){ 63 | $('#' + self.TABS[TAB] + 'Button').click(function(){ 64 | self.goToTab(self.TABS[TAB]); 65 | }) 66 | }); 67 | }; 68 | } 69 | 70 | module.exports = tabHandler; 71 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkurious/angular-performance/cd5dc89ab6ba8cd7d2489b451a00aa85c4fe0eaa/screenshot.png --------------------------------------------------------------------------------