├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── docs ├── ChromeTracingMetrics.json ├── NetworkResources.json ├── NetworkTimings.json ├── RafRenderingStats.json ├── RuntimePerfMetrics.json ├── TimelineMetrics.json └── index.js ├── lib ├── actions │ ├── index.js │ ├── loginAction.js │ ├── page_scripts │ │ ├── chrome_scroll.js │ │ ├── gesture_common.js │ │ └── raf_scroll.js │ ├── scrollAction.js │ └── wait.js ├── cli.js ├── helpers.js ├── index.js ├── metrics │ ├── BaseMetrics.js │ ├── ChromeTracingMetrics.js │ ├── NetworkResources.js │ ├── NetworkTimings.js │ ├── RafRenderingStats.js │ ├── SampleMetric.js │ ├── TimelineMetrics.js │ ├── index.js │ └── util │ │ ├── RenderingStats.js │ │ ├── RuntimePerfMetrics.js │ │ ├── StatData.js │ │ └── statistics.js ├── options.js ├── probes │ ├── AndroidTracingProbe.js │ ├── NavTimingProbe.js │ ├── NetworkResourcesProbe.js │ ├── PerfLogProbe.js │ ├── RafBenchmarkingProbe.js │ ├── SampleProbe.js │ └── index.js └── runner.js ├── license.txt ├── package.json └── test ├── e2e ├── e2e.spec.js └── runner.spec.js ├── res ├── android-hybrid.config.json ├── android.config.json ├── browserstack.config.json ├── chrome_canary.config.json ├── ios-hybrid-appium.config.json ├── ios-safari-appium.config.json ├── saucelabs.config.json ├── selenium_debug.config.json └── selenium_local.config.json ├── test.helper.js └── unit ├── actions.spec.js ├── docs.spec.js ├── helpers.spec.js ├── metrics.spec.js ├── options ├── actions.spec.js ├── browsers.spec.js ├── configFile.spec.js ├── defaults.spec.js └── selenium.spec.js └── util.spec.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | lib/cli.js text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | *.pem 4 | *.njsproj 5 | *.v12.suo 6 | .vscode/ 7 | 8 | # Created by http://www.gitignore.io 9 | 10 | ### Node ### 11 | lib-cov 12 | lcov.info 13 | *.seed 14 | *.log 15 | *.csv 16 | *.dat 17 | *.out 18 | *.pid 19 | *.gz 20 | 21 | pids 22 | logs 23 | results 24 | build 25 | .grunt 26 | 27 | node_modules 28 | 29 | 30 | ### OSX ### 31 | .DS_Store 32 | .AppleDouble 33 | .LSOverride 34 | 35 | # Icon must ends with two \r. 36 | Thumbnails 37 | 38 | 39 | # Icon 40 | ._* 41 | 42 | # Files that might appear on external disk 43 | .Spotlight-V100 44 | .Trashes 45 | 46 | ### SublimeText ### 47 | # workspace files are user-specific 48 | *.sublime-workspace 49 | *.sublime-project 50 | 51 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | *.pem 4 | Gruntfile.js 5 | test/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | install: 3 | - npm install 4 | language: node_js 5 | node_js: 6 | - "0.10" 7 | env: 8 | global: 9 | - SELENIUM: http://ondemand.saucelabs.com/wd/hub 10 | - USERNAME: browserperf 11 | - secure: |- 12 | IG4vXAJoiHY4lrKHWrueny7xMfcrvoLJcygjJakP+Wk+GkgBHUu3ihKNRwtG 13 | ms+BaruNS+yHA9CTF3MxHKLJCTgKOMI4awV2UzqatpHX3AJjW2d2ri4G1yfJ 14 | JStSM3t2RVKMU8HeHo1FXZvDOX0f/gQYE0hIBjR9ZFl2ni5ImA0= 15 | notifications: 16 | email: 17 | on_success: never 18 | branches: 19 | except: 20 | - /-dev|dev-/ 21 | sudo: false 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # browser-perf 2 | 3 | - Is a NodeJS based tool 4 | - For measuring browser performance metrics (like frame rates, expensive layouts, paints, styles, etc.) 5 | - For Web pages, Cordova/Phonegap and other Hybrid applications. 6 | - Metrics are measured while mimicking real user interactions - clicking buttons, typing content, etc. 7 | - Tool collects the metrics from sources like `about:tracing`, Chrome Devtools timeline, IE UI Responsiveness tab, Xperf, etc. 8 | - Monitor this information regularly by integrating the tool with continuous integration systems. 9 | 10 | ## Documentation 11 | Read more on [why browser-perf here](https://github.com/axemclion/browser-perf/wiki#why-browser-perf-). 12 | 13 | Please see the [wiki pages](https://github.com/axemclion/browser-perf/wiki/_pages) for more information. 14 | You can find information about supported browsers, [getting started](https://github.com/axemclion/browser-perf/wiki/Setup-Instructions), [command line usage](https://github.com/axemclion/browser-perf/wiki/Command-Line-Usage), reference for the [Node API](https://github.com/axemclion/browser-perf/wiki/Node-Module---API) etc. 15 | 16 | ## Usage 17 | 18 | ### Command line 19 | 20 | Install the tool using `npm install -g browser-perf` and then run 21 | 22 | ``` 23 | $ browser-perf http://yourwebsite.com --browsers=chrome,firefox --selenium=ondemand.saucelabs.com --username=username --accesskey=accesskey 24 | ``` 25 | 26 | - Replace username and access key with the [saucelabs.com](http://saucelabs.com) username and accesskey 27 | - If you have [Selenium](http://www.seleniumhq.org/download/) set up, you could substitute `ondemand.saucelabs.com` with `localhost:4444/wd/hub` 28 | - You can also use [BrowserStack](http://browserstack.com) credentials and substitute `ondemand.saucelabs.com` with `hub.browserstack.com` 29 | 30 | See the [wiki page](https://github.com/axemclion/browser-perf/wiki/Command-Line-Usage) for an extensive list of command line options and more usage scenarios. 31 | 32 | Here is a video of the command line usage 33 | [![Demo of browser-perf](https://img.youtube.com/vi/0HmAFrUCIUI/0.jpg "Demo of browser-perf")](https://www.youtube.com/watch?v=0HmAFrUCIUI) 34 | 35 | ## Node Module 36 | 37 | browser-perf is also a node module and has the following API 38 | 39 | ```javascript 40 | 41 | var browserPerf = require('browser-perf'); 42 | browserPerf('/*URL of the page to be tested*/', function(err, res) { 43 | // res - array of objects. Metrics for this URL 44 | if (err) { 45 | console.log('ERROR: ' + err); 46 | } else { 47 | console.log(res); 48 | } 49 | }, { 50 | selenium: 'http://localhost:4444/wd/hub', 51 | browsers: ['chrome', 'firefox'] 52 | username: SAUCE_USERNAME // if running tests on the cloud 53 | }); 54 | 55 | ``` 56 | See the [API wiki page](https://github.com/axemclion/browser-perf/wiki/Node-Module---API) for more details on configuring. 57 | Instructions on using it for Cordova apps is also on the [wiki](https://github.com/axemclion/browser-perf/wiki/Setup-Instructions#wiki-cordova-applications) 58 | 59 | ## Scenario 60 | - Websites can become slow 61 | - over time as more CSS and Javascript is added 62 | - due to a single commit that adds expensive CSS (like gradients) 63 | - We use tools in [Chrome](https://developers.google.com/chrome-developer-tools/docs/timeline) or [Internet Explorer](http://msdn.microsoft.com/en-us/library/ie/dn255009%28v=vs.85%29.asp) only when the site is too slow. 64 | - Tools like YSlow and Page Speed are great, but will it not be better if the are a part of continuous integration? 65 | - Tools like this(http://npmjs.org/package/browser-perf) and [Phantomas](https://github.com/macbre/phantomas) can fill the gap to monitor site performance every time a checkin is performed. 66 | 67 | ## License 68 | Licensed under BSD-2 Clause. See License.txt for more details 69 | 70 | ## Contact 71 | Please ping [me](http://twitter.com/nparashuram) if you would need help setting this up. 72 | -------------------------------------------------------------------------------- /docs/ChromeTracingMetrics.json: -------------------------------------------------------------------------------- 1 | { 2 | "percentage_smooth": { 3 | "type": "total", 4 | "tags": ["Paint"], 5 | "unit": "percentage", 6 | "source": "ChromeTracingMetrics", 7 | "summary": "Percentage of frames that were hitting 60 fps.", 8 | "details": "", 9 | "browsers": ["chrome", "android"], 10 | "importance": 50 11 | }, 12 | "frame_time_discrepancy": { 13 | "type": "total", 14 | "tags": ["Paint"], 15 | "unit": "ms", 16 | "source": "ChromeTracingMetrics", 17 | "summary": "Absolute discrepancy of frame time stamps, where discrepancy is a measure of irregularity.", 18 | "details": "It quantifies the worst jank. For a single pause, discrepancy corresponds to the length of this pause in milliseconds. Consecutive pauses increase the discrepancy. This metric is important because even if the mean and 95th percentile are good, one long pause in the middle of an interaction is still bad.", 19 | "browsers": ["chrome", "android"], 20 | "importance": 70 21 | }, 22 | "frames_per_sec": { 23 | "type": "total", 24 | "tags": ["Paint"], 25 | "unit": "fps", 26 | "source": "ChromeTracingMetrics", 27 | "summary": "Average number of frames drawn per second", 28 | "details": "Frame times are calculated using benchmark events in the browser's tracing information", 29 | "browsers": ["chrome", "android"], 30 | "importance": 100 31 | }, 32 | "mean_frame_time": { 33 | "type": "total", 34 | "tags": ["Paint"], 35 | "unit": "ms", 36 | "source": "ChromeTracingMetrics", 37 | "summary": "Average time taken to render each frame", 38 | "details": "Arithmetic mean of frame times. Frame times are calculated using tracing information, looking for events of the Benchmark category", 39 | "browsers": ["chrome", "android"], 40 | "importance": 99 41 | } 42 | } -------------------------------------------------------------------------------- /docs/NetworkResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "NetworkImage": { 3 | "type": "total", 4 | "tags": ["Network"], 5 | "unit": "ms", 6 | "source": "NetworkResources", 7 | "importance" : 60, 8 | "summary": "The time spent to load all images from page and CSS", 9 | "details": "", 10 | "browsers": ["chrome", "firefox", "android"] 11 | }, 12 | "NetworkImage_avg": { 13 | "type": "average", 14 | "tags": ["Network"], 15 | "unit": "ms", 16 | "source": "NetworkResources", 17 | "importance" : 60, 18 | "summary": "Average time spent to load one images", 19 | "details": "", 20 | "browsers": ["chrome", "firefox", "android"] 21 | }, 22 | "NetworkImage_count": { 23 | "type": "count", 24 | "tags": ["Network"], 25 | "unit": "count", 26 | "source": "NetworkResources", 27 | "importance" : 60, 28 | "summary": "Total number of images loaded", 29 | "details": "", 30 | "browsers": ["chrome", "firefox", "android"] 31 | }, 32 | "NetworkImage_max": { 33 | "type": "max", 34 | "tags": ["Network"], 35 | "unit": "ms", 36 | "source": "NetworkResources", 37 | "importance" : 60, 38 | "summary": "Maximum image loading duration", 39 | "details": "", 40 | "browsers": ["chrome", "firefox", "android"] 41 | }, 42 | 43 | "NetworkIframe": { 44 | "type": "total", 45 | "tags": ["Network"], 46 | "unit": "ms", 47 | "source": "NetworkResources", 48 | "importance" : 60, 49 | "summary": "The time spent to load all iframes on page", 50 | "details": "", 51 | "browsers": ["chrome", "firefox", "android"] 52 | }, 53 | "NetworkIframe_avg": { 54 | "type": "average", 55 | "tags": ["Network"], 56 | "unit": "ms", 57 | "source": "NetworkResources", 58 | "importance" : 60, 59 | "summary": "Average time spent to load one iframe", 60 | "details": "", 61 | "browsers": ["chrome", "firefox", "android"] 62 | }, 63 | "NetworkIframe_count": { 64 | "type": "count", 65 | "tags": ["Network"], 66 | "unit": "count", 67 | "source": "NetworkResources", 68 | "importance" : 60, 69 | "summary": "Total number of iframes loaded", 70 | "details": "", 71 | "browsers": ["chrome", "firefox", "android"] 72 | }, 73 | "NetworkIframe_max": { 74 | "type": "max", 75 | "tags": ["Network"], 76 | "unit": "ms", 77 | "source": "NetworkResources", 78 | "importance" : 60, 79 | "summary": "Maximum iframe loading duration", 80 | "details": "", 81 | "browsers": ["chrome", "firefox", "android"] 82 | }, 83 | 84 | "NetworkCss": { 85 | "type": "total", 86 | "tags": ["Network"], 87 | "unit": "ms", 88 | "source": "NetworkResources", 89 | "importance" : 60, 90 | "summary": "The time spent to load all CSS resources on page", 91 | "details": "", 92 | "browsers": ["chrome", "firefox", "android"] 93 | }, 94 | "NetworkCss_avg": { 95 | "type": "average", 96 | "tags": ["Network"], 97 | "unit": "ms", 98 | "source": "NetworkResources", 99 | "importance" : 60, 100 | "summary": "Average time spent to load one CSS file", 101 | "details": "", 102 | "browsers": ["chrome", "firefox", "android"] 103 | }, 104 | "NetworkCss_count": { 105 | "type": "count", 106 | "tags": ["Network"], 107 | "unit": "count", 108 | "source": "NetworkResources", 109 | "importance" : 60, 110 | "summary": "Total number of CSS files loaded", 111 | "details": "", 112 | "browsers": ["chrome", "firefox", "android"] 113 | }, 114 | "NetworkCss_max": { 115 | "type": "max", 116 | "tags": ["Network"], 117 | "unit": "ms", 118 | "source": "NetworkResources", 119 | "importance" : 60, 120 | "summary": "Maximum CSS file loading duration", 121 | "details": "", 122 | "browsers": ["chrome", "firefox", "android"] 123 | }, 124 | 125 | "NetworkJs": { 126 | "type": "total", 127 | "tags": ["Network"], 128 | "unit": "ms", 129 | "source": "NetworkResources", 130 | "importance" : 60, 131 | "summary": "The time spent to load all JS resources on page", 132 | "details": "", 133 | "browsers": ["chrome", "firefox", "android"] 134 | }, 135 | "NetworkJs_avg": { 136 | "type": "average", 137 | "tags": ["Network"], 138 | "unit": "ms", 139 | "source": "NetworkResources", 140 | "importance" : 60, 141 | "summary": "Average time spent to load one JS file", 142 | "details": "", 143 | "browsers": ["chrome", "firefox", "android"] 144 | }, 145 | "NetworkJs_count": { 146 | "type": "count", 147 | "tags": ["Network"], 148 | "unit": "count", 149 | "source": "NetworkResources", 150 | "importance" : 60, 151 | "summary": "Total number of JS files loaded", 152 | "details": "", 153 | "browsers": ["chrome", "firefox", "android"] 154 | }, 155 | "NetworkJs_max": { 156 | "type": "max", 157 | "tags": ["Network"], 158 | "unit": "ms", 159 | "source": "NetworkResources", 160 | "importance" : 60, 161 | "summary": "Maximum JS file loading duration", 162 | "details": "", 163 | "browsers": ["chrome", "firefox", "android"] 164 | }, 165 | 166 | "NetworkXhrrequest": { 167 | "type": "total", 168 | "tags": ["Network"], 169 | "unit": "ms", 170 | "source": "NetworkResources", 171 | "importance" : 60, 172 | "summary": "The time spent to load all XHR Requests on page", 173 | "details": "", 174 | "browsers": ["chrome", "firefox", "android"] 175 | }, 176 | "NetworkXhrrequest_avg": { 177 | "type": "average", 178 | "tags": ["Network"], 179 | "unit": "ms", 180 | "source": "NetworkResources", 181 | "importance" : 60, 182 | "summary": "Average time spent to load one XHR Request", 183 | "details": "", 184 | "browsers": ["chrome", "firefox", "android"] 185 | }, 186 | "NetworkXhrrequest_count": { 187 | "type": "count", 188 | "tags": ["Network"], 189 | "unit": "count", 190 | "source": "NetworkResources", 191 | "importance" : 60, 192 | "summary": "Total number of XHR Request loaded", 193 | "details": "", 194 | "browsers": ["chrome", "firefox", "android"] 195 | }, 196 | "NetworkXhrrequest_max": { 197 | "type": "max", 198 | "tags": ["Network"], 199 | "unit": "ms", 200 | "source": "NetworkResources", 201 | "importance" : 60, 202 | "summary": "Maximum XHR Request loading duration", 203 | "details": "", 204 | "browsers": ["chrome", "firefox", "android"] 205 | }, 206 | 207 | "NetworkOther": { 208 | "type": "total", 209 | "tags": ["Network"], 210 | "unit": "ms", 211 | "source": "NetworkResources", 212 | "importance" : 60, 213 | "summary": "The time spent to load all non-categorised page resources, f.e. icons", 214 | "details": "", 215 | "browsers": ["chrome", "firefox", "android"] 216 | }, 217 | "NetworkOther_avg": { 218 | "type": "average", 219 | "tags": ["Network"], 220 | "unit": "ms", 221 | "source": "NetworkResources", 222 | "importance" : 60, 223 | "summary": "Average time spent to load one non-categorised resource", 224 | "details": "", 225 | "browsers": ["chrome", "firefox", "android"] 226 | }, 227 | "NetworkOther_count": { 228 | "type": "count", 229 | "tags": ["Network"], 230 | "unit": "count", 231 | "source": "NetworkResources", 232 | "importance" : 60, 233 | "summary": "Total number of non-categorised resources loaded", 234 | "details": "", 235 | "browsers": ["chrome", "firefox", "android"] 236 | }, 237 | "NetworkOther_max": { 238 | "type": "max", 239 | "tags": ["Network"], 240 | "unit": "ms", 241 | "source": "NetworkResources", 242 | "importance" : 60, 243 | "summary": "Maximum non-categorised resource loading duration", 244 | "details": "", 245 | "browsers": ["chrome", "firefox", "android"] 246 | }, 247 | 248 | "NetworkRawData" : { 249 | "type": "tottal", 250 | "tags": ["Network"], 251 | "unit": "", 252 | "source": "NetworkResources", 253 | "importance" : 60, 254 | "summary": "Raw response from window.performance", 255 | "details": "", 256 | "browsers": ["chrome", "firefox", "android"] 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /docs/NetworkTimings.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstPaint": { 3 | "type": "total", 4 | "tags": ["Paint"], 5 | "unit": "ms", 6 | "source": "NetworkTimings", 7 | "importance": 70, 8 | "summary": "The time immediately before the first paint occurred", 9 | "details": "", 10 | "browsers": ["chrome", "ie"] 11 | }, 12 | 13 | "loadTime": { 14 | "type": "total", 15 | "tags": ["Network"], 16 | "unit": "ms", 17 | "source": "NetworkTimings", 18 | "importance": 50, 19 | "summary": "Total time from start to load", 20 | "details": "", 21 | "browsers": ["chrome", "ie", "safari", "android"] 22 | }, 23 | "domReadyTime": { 24 | "type": "total", 25 | "tags": ["Network"], 26 | "unit": "ms", 27 | "source": "NetworkTimings", 28 | "importance": 60, 29 | "summary": "Time spent constructing the DOM tree", 30 | "details": "", 31 | "browsers": ["chrome", "ie", "safari", "android"] 32 | }, 33 | "readyStart": { 34 | "type": "total", 35 | "tags": ["Network"], 36 | "unit": "ms", 37 | "source": "NetworkTimings", 38 | "importance": 60, 39 | "summary": "Time consumed preparing the new page", 40 | "details": "", 41 | "browsers": ["chrome", "ie", "safari", "android"] 42 | }, 43 | "redirectTime": { 44 | "type": "total", 45 | "tags": ["Network"], 46 | "unit": "ms", 47 | "source": "NetworkTimings", 48 | "importance": 40, 49 | "summary": "Time spent during redirection", 50 | "details": "", 51 | "browsers": ["chrome", "ie", "safari", "android"] 52 | }, 53 | "appcacheTime": { 54 | "type": "total", 55 | "tags": ["Network"], 56 | "unit": "ms", 57 | "source": "NetworkTimings", 58 | "importance": 40, 59 | "summary": "Time spent in AppCache", 60 | "details": "", 61 | "browsers": ["chrome", "ie", "safari", "android"] 62 | }, 63 | "unloadEventTime": { 64 | "type": "total", 65 | "tags": ["Network"], 66 | "unit": "ms", 67 | "source": "NetworkTimings", 68 | "importance": 50, 69 | "summary": "Time spent unloading documents", 70 | "details": "", 71 | "browsers": ["chrome", "ie", "safari", "android"] 72 | }, 73 | "domainLookupTime": { 74 | "type": "total", 75 | "tags": ["Network"], 76 | "unit": "ms", 77 | "source": "NetworkTimings", 78 | "importance": 40, 79 | "summary": "DNS query time", 80 | "details": "", 81 | "browsers": ["chrome", "ie", "safari", "android"] 82 | }, 83 | "connectTime": { 84 | "type": "total", 85 | "tags": ["Network"], 86 | "unit": "ms", 87 | "source": "NetworkTimings", 88 | "importance": 60, 89 | "summary": "TCP connection time", 90 | "details": "", 91 | "browsers": ["chrome", "ie", "safari", "android"] 92 | }, 93 | "requestTime": { 94 | "type": "total", 95 | "tags": ["Network"], 96 | "unit": "ms", 97 | "source": "NetworkTimings", 98 | "importance": 60, 99 | "summary": "Time spent during the request", 100 | "details": "", 101 | "browsers": ["chrome", "ie", "safari", "android"] 102 | }, 103 | "initDomTreeTime": { 104 | "type": "total", 105 | "tags": ["Network"], 106 | "unit": "ms", 107 | "source": "NetworkTimings", 108 | "importance": 60, 109 | "summary": "Request to completion of the DOM loading", 110 | "details": "", 111 | "browsers": ["chrome", "ie", "safari", "android"] 112 | }, 113 | "loadEventTime": { 114 | "type": "total", 115 | "tags": ["Network"], 116 | "unit": "ms", 117 | "source": "NetworkTimings", 118 | "importance": 60, 119 | "summary": "Load event time", 120 | "details": "", 121 | "browsers": ["chrome", "ie", "safari", "android"] 122 | }, 123 | "connectEnd": { 124 | "type": "total", 125 | "tags": ["Paint"], 126 | "unit": "ms", 127 | "source": "NetworkTimings", 128 | "importance": 20, 129 | "summary": "The time immediately after the user agent finished establishing the connection to the server to retrieve the document", 130 | "details": "", 131 | "browsers": ["chrome", "firefox", "ie"] 132 | }, 133 | "connectStart": { 134 | "type": "total", 135 | "tags": ["Paint"], 136 | "unit": "ms", 137 | "source": "NetworkTimings", 138 | "importance": 20, 139 | "summary": "The time immediately before the user agent started establishing the connection to the server to retrieve the document", 140 | "details": "", 141 | "browsers": ["chrome", "firefox", "ie"] 142 | }, 143 | "domainLookupEnd": { 144 | "type": "total", 145 | "tags": ["Paint"], 146 | "unit": "ms", 147 | "source": "NetworkTimings", 148 | "importance": 20, 149 | "summary": "The time immidiately after the user agent finishes the domain name lookup for the document", 150 | "details": "", 151 | "browsers": ["chrome", "firefox", "ie"] 152 | }, 153 | "domainLookupStart": { 154 | "type": "total", 155 | "tags": ["Paint"], 156 | "unit": "ms", 157 | "source": "NetworkTimings", 158 | "importance": 20, 159 | "summary": "The time immediately before the user agent started the domain name lookup for the document", 160 | "details": "", 161 | "browsers": ["chrome", "firefox", "ie"] 162 | }, 163 | "domComplete": { 164 | "type": "total", 165 | "tags": ["Paint"], 166 | "unit": "ms", 167 | "source": "NetworkTimings", 168 | "importance": 20, 169 | "summary": "The time immediately before the user agent set the document's readiness to complete", 170 | "details": "", 171 | "browsers": ["chrome", "firefox", "ie"] 172 | }, 173 | "domContentLoadedEventEnd": { 174 | "type": "total", 175 | "tags": ["Paint"], 176 | "unit": "ms", 177 | "source": "NetworkTimings", 178 | "importance": 20, 179 | "summary": "The time immediately after the document's DOMContentLoaded event completed", 180 | "details": "", 181 | "browsers": ["chrome", "firefox", "ie"] 182 | }, 183 | "domContentLoadedEventStart": { 184 | "type": "total", 185 | "tags": ["Paint"], 186 | "unit": "ms", 187 | "source": "NetworkTimings", 188 | "importance": 20, 189 | "summary": "The time immediately before the user agent fired the DOMContentLoaded event", 190 | "details": "", 191 | "browsers": ["chrome", "firefox", "ie"] 192 | }, 193 | "domInteractive": { 194 | "type": "total", 195 | "tags": ["Paint"], 196 | "unit": "ms", 197 | "source": "NetworkTimings", 198 | "importance": 20, 199 | "summary": "The time immidiately before the user agent set the document's readiness to interactive", 200 | "details": "", 201 | "browsers": ["chrome", "firefox", "ie"] 202 | }, 203 | "domLoading": { 204 | "type": "total", 205 | "tags": ["Paint"], 206 | "unit": "ms", 207 | "source": "NetworkTimings", 208 | "importance": 20, 209 | "summary": "The time immidiately before the user agent set the document's readiness to loading", 210 | "details": "", 211 | "browsers": ["chrome", "firefox", "ie"] 212 | }, 213 | "fetchStart": { 214 | "type": "total", 215 | "tags": ["Paint"], 216 | "unit": "ms", 217 | "source": "NetworkTimings", 218 | "importance": 20, 219 | "summary": "The time immediately before the user agent checks caches for resources OR starts fetching resources", 220 | "details": "", 221 | "browsers": ["chrome", "firefox", "ie"] 222 | }, 223 | "loadEventEnd": { 224 | "type": "total", 225 | "tags": ["Paint"], 226 | "unit": "ms", 227 | "source": "NetworkTimings", 228 | "importance": 20, 229 | "summary": "The time the load event for the document completed", 230 | "details": "", 231 | "browsers": ["chrome", "firefox", "ie"] 232 | }, 233 | "loadEventStart": { 234 | "type": "total", 235 | "tags": ["Paint"], 236 | "unit": "ms", 237 | "source": "NetworkTimings", 238 | "importance": 20, 239 | "summary": "The time immediately before the load event was fired for the document", 240 | "details": "", 241 | "browsers": ["chrome", "firefox", "ie"] 242 | }, 243 | "requestStart": { 244 | "type": "total", 245 | "tags": ["Paint"], 246 | "unit": "ms", 247 | "source": "NetworkTimings", 248 | "importance": 20, 249 | "summary": "The time immediately before the user agent started requesting the document from the server", 250 | "details": "", 251 | "browsers": ["chrome", "firefox", "ie"] 252 | }, 253 | "responseStart": { 254 | "type": "total", 255 | "tags": ["Paint"], 256 | "unit": "ms", 257 | "source": "NetworkTimings", 258 | "importance": 20, 259 | "summary": "The time immediately after the user agent received the first byte of the server's response to the document request", 260 | "details": "", 261 | "browsers": ["chrome", "firefox", "ie"] 262 | }, 263 | "responseEnd": { 264 | "type": "total", 265 | "tags": ["Paint"], 266 | "unit": "ms", 267 | "source": "NetworkTimings", 268 | "importance": 20, 269 | "summary": "", 270 | "details": "", 271 | "browsers": ["chrome", "firefox", "ie"] 272 | }, 273 | "navigationStart": { 274 | "type": "total", 275 | "tags": ["Paint"], 276 | "unit": "ms", 277 | "source": "NetworkTimings", 278 | "importance": 20, 279 | "summary": "", 280 | "details": "", 281 | "browsers": ["chrome", "firefox", "ie"] 282 | }, 283 | "redirectEnd": { 284 | "type": "total", 285 | "tags": ["Paint"], 286 | "unit": "ms", 287 | "source": "NetworkTimings", 288 | "importance": 20, 289 | "summary": "", 290 | "details": "", 291 | "browsers": ["chrome", "firefox", "ie"] 292 | }, 293 | "redirectStart": { 294 | "type": "total", 295 | "tags": ["Paint"], 296 | "unit": "ms", 297 | "source": "NetworkTimings", 298 | "importance": 20, 299 | "summary": "", 300 | "details": "", 301 | "browsers": ["chrome", "firefox", "ie"] 302 | }, 303 | "secureConnectionStart": { 304 | "type": "total", 305 | "tags": ["Paint"], 306 | "unit": "ms", 307 | "source": "NetworkTimings", 308 | "importance": 20, 309 | "summary": "", 310 | "details": "", 311 | "browsers": ["chrome", "ie"] 312 | }, 313 | "unloadEventEnd": { 314 | "type": "total", 315 | "tags": ["Paint"], 316 | "unit": "ms", 317 | "source": "NetworkTimings", 318 | "importance": 20, 319 | "summary": "", 320 | "details": "", 321 | "browsers": ["chrome", "firefox", "ie"] 322 | }, 323 | "unloadEventStart": { 324 | "type": "total", 325 | "tags": ["Paint"], 326 | "unit": "ms", 327 | "source": "NetworkTimings", 328 | "importance": 20, 329 | "summary": "", 330 | "details": "", 331 | "browsers": ["chrome", "firefox", "ie"] 332 | } 333 | } -------------------------------------------------------------------------------- /docs/RafRenderingStats.json: -------------------------------------------------------------------------------- 1 | { 2 | "meanFrameTime_raf": { 3 | "type": "total", 4 | "tags": ["Paint"], 5 | "unit": "ms", 6 | "source": "RafRenderingStats", 7 | "importance": 99, 8 | "summary": "Average time for each frame, calculated using requestAnimationFrame", 9 | "details": "May not be very accurate since heavy UI threads see one event for multiple frames drawn on the screen.", 10 | "browsers": ["chrome", "firefox", "safari", "ie", "android"] 11 | }, 12 | "framesPerSec_raf": { 13 | "type": "total", 14 | "tags": ["Frames"], 15 | "unit": "fps", 16 | "source": "RafRenderingStats", 17 | "importance": 100, 18 | "summary": "Number of Frames per second (fps) drawn on the screen, calculated using requestAnimationFrame.", 19 | "details": " 60 fps is a good benchmark for a smooth experience. May not be very accurate since heavy UI threads see one event for multiple frames drawn on the screen.", 20 | "browsers": ["chrome", "firefox", "safari", "ie", "android"] 21 | }, 22 | "droppedFrameCount": { 23 | "type": "total", 24 | "tags": ["Paint"], 25 | "unit": "count", 26 | "source": "RafRenderingStats", 27 | "importance": 40, 28 | "summary": "The number of frames produced by the GPU were over 16.6ms apart", 29 | "details": "", 30 | "browsers": ["chrome", "firefox", "safari", "ie", "android"] 31 | }, 32 | "numAnimationFrames": { 33 | "type": "total", 34 | "tags": ["Paint"], 35 | "unit": "count", 36 | "source": "RafRenderingStats", 37 | "importance": 50, 38 | "summary": "Total number of times requestAnimationFrame was called", 39 | "details": "", 40 | "browsers": ["chrome", "firefox", "safari", "ie", "android"] 41 | }, 42 | "numFramesSentToScreen": { 43 | "type": "total", 44 | "tags": ["Paint"], 45 | "unit": "count", 46 | "source": "RafRenderingStats", 47 | "importance": 50, 48 | "summary": "The number of frames the GPU sent to the screen", 49 | "details": "", 50 | "browsers": ["chrome", "firefox", "safari", "ie", "android"] 51 | } 52 | } -------------------------------------------------------------------------------- /docs/RuntimePerfMetrics.json: -------------------------------------------------------------------------------- 1 | { 2 | "framesPerSec (devtools)": { 3 | "type": "total", 4 | "tags": ["Paint"], 5 | "unit": "fps", 6 | "source": "DevTools", 7 | "summary": "Average number of frames drawn per second", 8 | "details": "Frame times are calculated using benchmark events in the browser's tracing information. This is a trimmed mean - i.e it ignores top and bottom 10% of the outliers and calculate the mean over the rest", 9 | "browsers": ["chrome", "android"], 10 | "importance": 100 11 | }, 12 | "ExpensiveEventHandlers": { 13 | "type": "total", 14 | "tags": ["Paint"], 15 | "unit": "count", 16 | "source": "RuntimePerfMetrics", 17 | "importance": 75, 18 | "summary": "The number of event handlers that took more than 16ms", 19 | "details": "", 20 | "browsers": ["chrome", "safari", "android"] 21 | }, 22 | "ExpensivePaints": { 23 | "type": "total", 24 | "tags": ["Paint"], 25 | "unit": "count", 26 | "source": "RuntimePerfMetrics", 27 | "importance": 80, 28 | "summary": "The number of paints that took more than 16ms", 29 | "details": "", 30 | "browsers": ["chrome", "safari", "android"] 31 | }, 32 | "Layers": { 33 | "type": "total", 34 | "tags": ["Paint"], 35 | "unit": "count", 36 | "source": "RuntimePerfMetrics", 37 | "importance": 85, 38 | "summary": "The number of layers used for rendering", 39 | "details": "", 40 | "browsers": ["chrome", "safari", "android"] 41 | }, 42 | 43 | "NodePerLayout_avg": { 44 | "type": "average", 45 | "tags": ["Paint"], 46 | "unit": "count", 47 | "source": "RuntimePerfMetrics", 48 | "importance": 85, 49 | "summary": "The number of nodes that need to be updated during layout", 50 | "details": "", 51 | "browsers": ["chrome", "safari", "android"] 52 | }, 53 | "PaintedArea_avg": { 54 | "type": "average", 55 | "tags": ["Paint"], 56 | "unit": "sq.pixels", 57 | "source": "RuntimePerfMetrics", 58 | "importance": 85, 59 | "summary": "The amount of space painted during paints", 60 | "details": "", 61 | "browsers": ["chrome", "safari", "android"] 62 | }, 63 | "PaintedArea_total": { 64 | "type": "total", 65 | "tags": ["Paint"], 66 | "unit": "sq.pixels", 67 | "source": "RuntimePerfMetrics", 68 | "importance": 80, 69 | "summary": "The amount of space painted during paints", 70 | "details": "", 71 | "browsers": ["chrome", "safari", "android"] 72 | } 73 | } -------------------------------------------------------------------------------- /docs/TimelineMetrics.json: -------------------------------------------------------------------------------- 1 | { 2 | "Styles": { 3 | "type": "total", 4 | "tags": ["Paint"], 5 | "unit": "ms", 6 | "source": "TimelineMetrics", 7 | "summary": "The time spent in styling operations of the page", 8 | "browsers": ["chrome", "safari", "android"], 9 | "importance": 69 10 | }, 11 | "Javascript": { 12 | "type": "total", 13 | "tags": ["Script"], 14 | "unit": "ms", 15 | "source": "TimelineMetrics", 16 | "summary": "The time spent in executing Javascript on the page", 17 | "browsers": ["chrome", "safari", "android"], 18 | "importance": 69 19 | }, 20 | 21 | "CompositeLayers": { 22 | "type": "total", 23 | "tags": ["Paint"], 24 | "unit": "ms", 25 | "source": "TimelineMetrics", 26 | "summary": "The time spent compositing rendering layers into a screen image", 27 | "browsers": ["chrome", "safari", "android"], 28 | "importance": 30 29 | }, 30 | "CompositeLayers_avg": { 31 | "type": "average", 32 | "tags": ["Paint"], 33 | "unit": "ms", 34 | "source": "TimelineMetrics", 35 | "summary": "The time spent compositing rendering layers into a screen image", 36 | "browsers": ["chrome", "safari", "android"], 37 | "importance": 60 38 | }, 39 | "CompositeLayers_count": { 40 | "type": "count", 41 | "tags": ["Paint"], 42 | "unit": "count", 43 | "source": "TimelineMetrics", 44 | "summary": "The time spent compositing rendering layers into a screen image", 45 | "browsers": ["chrome", "safari", "android"], 46 | "importance": 60 47 | }, 48 | "CompositeLayers_max": { 49 | "type": "max", 50 | "tags": ["Paint"], 51 | "unit": "ms", 52 | "source": "TimelineMetrics", 53 | "summary": "The time spent compositing rendering layers into a screen image", 54 | "browsers": ["chrome", "safari", "android"], 55 | "importance": 30 56 | }, 57 | "ParseHTML": { 58 | "type": "total", 59 | "tags": ["Paint"], 60 | "unit": "ms", 61 | "source": "TimelineMetrics", 62 | "summary": "The time spent executing the engine's HTML parsing algorithm", 63 | "browsers": ["chrome", "safari", "android"], 64 | "importance": 30 65 | }, 66 | "ParseHTML_avg": { 67 | "type": "average", 68 | "tags": ["Paint"], 69 | "unit": "ms", 70 | "source": "TimelineMetrics", 71 | "summary": "The time spent executing the engine's HTML parsing algorithm", 72 | "browsers": ["chrome", "safari", "android"], 73 | "importance": 50 74 | }, 75 | "ParseHTML_count": { 76 | "type": "count", 77 | "tags": ["Paint"], 78 | "unit": "count", 79 | "source": "TimelineMetrics", 80 | "summary": "The time spent executing the engine's HTML parsing algorithm", 81 | "browsers": ["chrome", "safari", "android"], 82 | "importance": 45 83 | }, 84 | "ParseHTML_max": { 85 | "type": "max", 86 | "tags": ["Paint"], 87 | "unit": "ms", 88 | "source": "TimelineMetrics", 89 | "summary": "The time spent executing the engine's HTML parsing algorithm", 90 | "browsers": ["chrome", "safari", "android"], 91 | "importance": 30 92 | }, 93 | "Layout": { 94 | "type": "total", 95 | "tags": ["Paint"], 96 | "unit": "ms", 97 | "source": "TimelineMetrics", 98 | "summary": "The time spent on (re)flow, or constructing the rendering tree from the DOM tree", 99 | "browsers": ["chrome", "safari", "android"], 100 | "importance": 69 101 | }, 102 | "Layout_avg": { 103 | "type": "average", 104 | "tags": ["Paint"], 105 | "unit": "ms", 106 | "source": "TimelineMetrics", 107 | "summary": "The time spent on (re)flow, or constructing the rendering tree from the DOM tree", 108 | "browsers": ["chrome", "safari", "android"], 109 | "importance": 65 110 | }, 111 | "Layout_count": { 112 | "type": "count", 113 | "tags": ["Paint"], 114 | "unit": "count", 115 | "source": "TimelineMetrics", 116 | "summary": "The time spent on (re)flow, or constructing the rendering tree from the DOM tree", 117 | "browsers": ["chrome", "safari", "android"], 118 | "importance": 65 119 | }, 120 | "Layout_max": { 121 | "type": "max", 122 | "tags": ["Paint"], 123 | "unit": "ms", 124 | "source": "TimelineMetrics", 125 | "summary": "The time spent on (re)flow, or constructing the rendering tree from the DOM tree", 126 | "browsers": ["chrome", "safari", "android"], 127 | "importance": 30 128 | }, 129 | "Paint": { 130 | "type": "total", 131 | "tags": ["Paint"], 132 | "unit": "ms", 133 | "source": "TimelineMetrics", 134 | "summary": "The time spent rasterizing the page into bitmaps", 135 | "browsers": ["chrome", "safari", "android"], 136 | "importance": 69 137 | }, 138 | "Paint_avg": { 139 | "type": "average", 140 | "tags": ["Paint"], 141 | "unit": "ms", 142 | "source": "TimelineMetrics", 143 | "summary": "The time spent rasterizing the page into bitmaps", 144 | "browsers": ["chrome", "safari", "android"], 145 | "importance": 65 146 | }, 147 | "Paint_count": { 148 | "type": "count", 149 | "tags": ["Paint"], 150 | "unit": "count", 151 | "source": "TimelineMetrics", 152 | "summary": "The time spent rasterizing the page into bitmaps", 153 | "browsers": ["chrome", "safari", "android"], 154 | "importance": 65 155 | }, 156 | "Paint_max": { 157 | "type": "max", 158 | "tags": ["Paint"], 159 | "unit": "ms", 160 | "source": "TimelineMetrics", 161 | "summary": "The time spent rasterizing the page into bitmaps", 162 | "browsers": ["chrome", "safari", "android"], 163 | "importance": 30 164 | }, 165 | "PaintSetup": { 166 | "type": "total", 167 | "tags": ["Paint"], 168 | "unit": "ms", 169 | "source": "TimelineMetrics", 170 | "summary": "The time spent converting DOM elements into a display list of drawing commands to paint", 171 | "browsers": ["chrome", "safari", "android"], 172 | "deprecated": { 173 | "chrome": 39 174 | }, 175 | "importance": 30 176 | }, 177 | "PaintSetup_avg": { 178 | "type": "average", 179 | "tags": ["Paint"], 180 | "unit": "ms", 181 | "source": "TimelineMetrics", 182 | "summary": "The time spent converting DOM elements into a display list of drawing commands to paint", 183 | "browsers": ["chrome", "safari", "android"], 184 | "deprecated": { 185 | "chrome": 39 186 | }, 187 | "importance": 50 188 | }, 189 | "PaintSetup_count": { 190 | "type": "count", 191 | "tags": ["Paint"], 192 | "unit": "count", 193 | "source": "TimelineMetrics", 194 | "summary": "The time spent converting DOM elements into a display list of drawing commands to paint", 195 | "browsers": ["chrome", "safari", "android"], 196 | "deprecated": { 197 | "chrome": 39 198 | }, 199 | "importance": 40 200 | }, 201 | "PaintSetup_max": { 202 | "type": "max", 203 | "tags": ["Paint"], 204 | "unit": "ms", 205 | "source": "TimelineMetrics", 206 | "summary": "The time spent converting DOM elements into a display list of drawing commands to paint", 207 | "browsers": ["chrome", "safari", "android"], 208 | "deprecated": { 209 | "chrome": 39 210 | }, 211 | "importance": 30 212 | }, 213 | "RecalculateStyles": { 214 | "type": "total", 215 | "tags": ["Paint"], 216 | "unit": "ms", 217 | "source": "TimelineMetrics", 218 | "summary": "The time spent recalculating element styling, when a reflow occurs", 219 | "browsers": ["chrome", "safari", "android"], 220 | "importance": 30 221 | }, 222 | "RecalculateStyles_avg": { 223 | "type": "average", 224 | "tags": ["Paint"], 225 | "unit": "ms", 226 | "source": "TimelineMetrics", 227 | "summary": "The time spent recalculating element styling, when a reflow occurs", 228 | "browsers": ["chrome", "safari", "android"], 229 | "importance": 65 230 | }, 231 | "RecalculateStyles_count": { 232 | "type": "count", 233 | "tags": ["Paint"], 234 | "unit": "count", 235 | "source": "TimelineMetrics", 236 | "summary": "The time spent recalculating element styling, when a reflow occurs", 237 | "browsers": ["chrome", "safari", "android"], 238 | "importance": 65 239 | }, 240 | "RecalculateStyles_max": { 241 | "type": "max", 242 | "tags": ["Paint"], 243 | "unit": "ms", 244 | "source": "TimelineMetrics", 245 | "summary": "The time spent recalculating element styling, when a reflow occurs", 246 | "browsers": ["chrome", "safari", "android"], 247 | "importance": 30 248 | }, 249 | "Program": { 250 | "type": "total", 251 | "tags": ["Paint"], 252 | "unit": "ms", 253 | "source": "TimelineMetrics", 254 | "summary": "The total time for all actions to render the page", 255 | "browsers": ["chrome", "safari", "android"], 256 | "importance": 20 257 | }, 258 | "Program_avg": { 259 | "type": "average", 260 | "tags": ["Paint"], 261 | "unit": "ms", 262 | "source": "TimelineMetrics", 263 | "summary": "The total time for all actions to render the page", 264 | "browsers": ["chrome", "safari", "android"], 265 | "importance": 20 266 | }, 267 | "Program_count": { 268 | "type": "count", 269 | "tags": ["Paint"], 270 | "unit": "count", 271 | "source": "TimelineMetrics", 272 | "summary": "The total time for all actions to render the page", 273 | "browsers": ["chrome", "safari", "android"], 274 | "importance": 20 275 | }, 276 | "Program_max": { 277 | "type": "max", 278 | "tags": ["Paint"], 279 | "unit": "ms", 280 | "source": "TimelineMetrics", 281 | "summary": "The total time for all actions to render the page", 282 | "browsers": ["chrome", "safari", "android"], 283 | "importance": 20 284 | }, 285 | "TimerFire": { 286 | "type": "total", 287 | "tags": ["Paint"], 288 | "unit": "ms", 289 | "source": "TimelineMetrics", 290 | "summary": "The time spent executing timer-triggered handlers", 291 | "browsers": ["chrome", "safari", "android"], 292 | "importance": 35 293 | }, 294 | "TimerFire_avg": { 295 | "type": "average", 296 | "tags": ["Paint"], 297 | "unit": "ms", 298 | "source": "TimelineMetrics", 299 | "summary": "The time spent executing timer-triggered handlers", 300 | "browsers": ["chrome", "safari", "android"], 301 | "importance": 53 302 | }, 303 | "TimerFire_count": { 304 | "type": "count", 305 | "tags": ["Paint"], 306 | "unit": "count", 307 | "source": "TimelineMetrics", 308 | "summary": "The time spent executing timer-triggered handlers", 309 | "browsers": ["chrome", "safari", "android"], 310 | "importance": 43 311 | }, 312 | "TimerFire_max": { 313 | "type": "max", 314 | "tags": ["Paint"], 315 | "unit": "ms", 316 | "source": "TimelineMetrics", 317 | "summary": "The time spent executing timer-triggered handlers", 318 | "browsers": ["chrome", "safari", "android"], 319 | "importance": 30 320 | }, 321 | "EvaluateScript": { 322 | "type": "total", 323 | "tags": ["Paint"], 324 | "unit": "ms", 325 | "source": "TimelineMetrics", 326 | "summary": "The time spent evaluating the page's scripts", 327 | "browsers": ["chrome", "safari", "android"], 328 | "importance": 30 329 | }, 330 | "EvaluateScript_avg": { 331 | "type": "average", 332 | "tags": ["Paint"], 333 | "unit": "ms", 334 | "source": "TimelineMetrics", 335 | "summary": "The time spent evaluating the page's scripts", 336 | "browsers": ["chrome", "safari", "android"], 337 | "importance": 50 338 | }, 339 | "EvaluateScript_count": { 340 | "type": "count", 341 | "tags": ["Paint"], 342 | "unit": "count", 343 | "source": "TimelineMetrics", 344 | "summary": "The time spent evaluating the page's scripts", 345 | "browsers": ["chrome", "safari", "android"], 346 | "importance": 40 347 | }, 348 | "EvaluateScript_max": { 349 | "type": "max", 350 | "tags": ["Paint"], 351 | "unit": "ms", 352 | "source": "TimelineMetrics", 353 | "summary": "The time spent evaluating the page's scripts", 354 | "browsers": ["chrome", "safari", "android"], 355 | "importance": 30 356 | }, 357 | "EventDispatch": { 358 | "type": "total", 359 | "tags": ["Paint"], 360 | "unit": "ms", 361 | "source": "TimelineMetrics", 362 | "summary": "The time spent executing event handlers", 363 | "browsers": ["chrome", "safari", "android"], 364 | "importance": 30 365 | }, 366 | "EventDispatch_avg": { 367 | "type": "average", 368 | "tags": ["Paint"], 369 | "unit": "ms", 370 | "source": "TimelineMetrics", 371 | "summary": "The time spent executing event handlers", 372 | "browsers": ["chrome", "safari", "android"], 373 | "importance": 50 374 | }, 375 | "EventDispatch_count": { 376 | "type": "count", 377 | "tags": ["Paint"], 378 | "unit": "count", 379 | "source": "TimelineMetrics", 380 | "summary": "The time spent executing event handlers", 381 | "browsers": ["chrome", "safari", "android"], 382 | "importance": 40 383 | }, 384 | "EventDispatch_max": { 385 | "type": "max", 386 | "tags": ["Paint"], 387 | "unit": "ms", 388 | "source": "TimelineMetrics", 389 | "summary": "The time spent executing event handlers", 390 | "browsers": ["chrome", "safari", "android"], 391 | "importance": 30 392 | }, 393 | "FireAnimationFrame": { 394 | "type": "total", 395 | "tags": ["Paint"], 396 | "unit": "ms", 397 | "source": "TimelineMetrics", 398 | "summary": "The time spent handling scheduled animation frame events", 399 | "browsers": ["chrome", "safari", "android"], 400 | "importance": 30 401 | }, 402 | "FireAnimationFrame_avg": { 403 | "type": "average", 404 | "tags": ["Paint"], 405 | "unit": "ms", 406 | "source": "TimelineMetrics", 407 | "summary": "The time spent handling scheduled animation frame events", 408 | "browsers": ["chrome", "safari", "android"], 409 | "importance": 50 410 | }, 411 | "FireAnimationFrame_count": { 412 | "type": "count", 413 | "tags": ["Paint"], 414 | "unit": "count", 415 | "source": "TimelineMetrics", 416 | "summary": "The time spent handling scheduled animation frame events", 417 | "browsers": ["chrome", "safari", "android"], 418 | "importance": 40 419 | }, 420 | "FireAnimationFrame_max": { 421 | "type": "max", 422 | "tags": ["Paint"], 423 | "unit": "ms", 424 | "source": "TimelineMetrics", 425 | "summary": "The time spent handling scheduled animation frame events", 426 | "browsers": ["chrome", "safari", "android"], 427 | "importance": 30 428 | }, 429 | "FunctionCall": { 430 | "type": "total", 431 | "tags": ["Paint"], 432 | "unit": "ms", 433 | "source": "TimelineMetrics", 434 | "summary": "The time spent executing calls to top-level Javascript functions", 435 | "browsers": ["chrome", "safari", "android"], 436 | "importance": 30 437 | }, 438 | "FunctionCall_avg": { 439 | "type": "average", 440 | "tags": ["Paint"], 441 | "unit": "ms", 442 | "source": "TimelineMetrics", 443 | "summary": "The time spent executing calls to top-level Javascript functions", 444 | "browsers": ["chrome", "safari", "android"], 445 | "importance": 50 446 | }, 447 | "FunctionCall_count": { 448 | "type": "count", 449 | "tags": ["Paint"], 450 | "unit": "count", 451 | "source": "TimelineMetrics", 452 | "summary": "The time spent executing calls to top-level Javascript functions", 453 | "browsers": ["chrome", "safari", "android"], 454 | "importance": 40 455 | }, 456 | "FunctionCall_max": { 457 | "type": "max", 458 | "tags": ["Paint"], 459 | "unit": "ms", 460 | "source": "TimelineMetrics", 461 | "summary": "The time spent executing calls to top-level Javascript functions", 462 | "browsers": ["chrome", "safari", "android"], 463 | "importance": 30 464 | }, 465 | "GCEvent": { 466 | "type": "total", 467 | "tags": ["Paint"], 468 | "unit": "ms", 469 | "source": "TimelineMetrics", 470 | "summary": "The time spent doing garbage collection during script execution", 471 | "browsers": ["chrome", "safari", "android"], 472 | "importance": 20 473 | }, 474 | "GCEvent_avg": { 475 | "type": "average", 476 | "tags": ["Paint"], 477 | "unit": "ms", 478 | "source": "TimelineMetrics", 479 | "summary": "The time spent doing garbage collection during script execution", 480 | "browsers": ["chrome", "safari", "android"], 481 | "importance": 30 482 | }, 483 | "GCEvent_count": { 484 | "type": "count", 485 | "tags": ["Paint"], 486 | "unit": "count", 487 | "source": "TimelineMetrics", 488 | "summary": "The time spent doing garbage collection during script execution", 489 | "browsers": ["chrome", "safari", "android"], 490 | "importance": 20 491 | }, 492 | "GCEvent_max": { 493 | "type": "max", 494 | "tags": ["Paint"], 495 | "unit": "ms", 496 | "source": "TimelineMetrics", 497 | "summary": "The time spent doing garbage collection during script execution", 498 | "browsers": ["chrome", "safari", "android"], 499 | "importance": 20 500 | }, 501 | "XHRReadyStateChange": { 502 | "type": "total", 503 | "tags": ["Paint"], 504 | "unit": "ms", 505 | "source": "TimelineMetrics", 506 | "summary": "The time spent handling handling changes to the ready state of XMLHTTPRequest's", 507 | "browsers": ["chrome", "safari", "android"], 508 | "importance": 10 509 | }, 510 | "XHRReadyStateChange_avg": { 511 | "type": "average", 512 | "tags": ["Paint"], 513 | "unit": "ms", 514 | "source": "TimelineMetrics", 515 | "summary": "The time spent handling changes to the ready state of XMLHTTPRequest's", 516 | "browsers": ["chrome", "safari", "android"], 517 | "importance": 10 518 | }, 519 | "XHRReadyStateChange_count": { 520 | "type": "count", 521 | "tags": ["Paint"], 522 | "unit": "count", 523 | "source": "TimelineMetrics", 524 | "summary": "The time spent handling changes to the ready state of XMLHTTPRequest's", 525 | "browsers": ["chrome", "safari", "android"], 526 | "importance": 10 527 | }, 528 | "XHRReadyStateChange_max": { 529 | "type": "max", 530 | "tags": ["Paint"], 531 | "unit": "ms", 532 | "source": "TimelineMetrics", 533 | "summary": "The time spent handling changes to the ready state of XMLHTTPRequest's", 534 | "browsers": ["chrome", "safari", "android"], 535 | "importance": 10 536 | }, 537 | "UpdateLayerTree": { 538 | "type": "total", 539 | "tags": ["Paint"], 540 | "unit": "ms", 541 | "source": "TimelineMetrics", 542 | "summary": "The time spent updating layout tree", 543 | "browsers": ["chrome", "safari", "android"], 544 | "importance": 10 545 | }, 546 | "UpdateLayerTree_count": { 547 | "type": "count", 548 | "tags": ["Paint"], 549 | "unit": "count", 550 | "source": "TimelineMetrics", 551 | "summary": "Number of updating layout tree", 552 | "browsers": ["chrome", "safari", "android"], 553 | "importance": 10 554 | }, 555 | "UpdateLayerTree_max": { 556 | "type": "max", 557 | "tags": ["Paint"], 558 | "unit": "ms", 559 | "source": "TimelineMetrics", 560 | "summary": "Maximum time spent updating layout tree", 561 | "browsers": ["chrome", "safari", "android"], 562 | "importance": 10 563 | }, 564 | "UpdateLayerTree_avg": { 565 | "type": "average", 566 | "tags": ["Paint"], 567 | "unit": "ms", 568 | "source": "TimelineMetrics", 569 | "summary": "Average time updating layout tree", 570 | "browsers": ["chrome", "safari", "android"], 571 | "importance": 10 572 | }, 573 | "Rasterize": { 574 | "type": "total", 575 | "tags": ["Paint"], 576 | "unit": "ms", 577 | "source": "TimelineMetrics", 578 | "summary": "Time spent to rasterization (process of stepping through draw calls and filling out actual pixels into buffers)", 579 | "browsers": ["chrome", "safari", "android"], 580 | "importance": 10 581 | }, 582 | "Rasterize_count": { 583 | "type": "count", 584 | "tags": ["Paint"], 585 | "unit": "count", 586 | "source": "TimelineMetrics", 587 | "summary": "Count of rasterization process", 588 | "browsers": ["chrome", "safari", "android"], 589 | "importance": 10 590 | }, 591 | "Rasterize_max": { 592 | "type": "max", 593 | "tags": ["Paint"], 594 | "unit": "ms", 595 | "source": "TimelineMetrics", 596 | "summary": "Maximum time spent to rasterization process", 597 | "browsers": ["chrome", "safari", "android"], 598 | "importance": 10 599 | }, 600 | "Rasterize_avg": { 601 | "type": "average", 602 | "tags": ["Paint"], 603 | "unit": "ms", 604 | "source": "TimelineMetrics", 605 | "summary": "Average time spent to rasterization process", 606 | "browsers": ["chrome", "safari", "android"], 607 | "importance": 10 608 | }, 609 | "RequestAnimationFrame": { 610 | "tags": ["Paint"], 611 | "source": "TimelineMetrics", 612 | "browsers": ["chrome", "safari", "android"], 613 | "unit": "ms", 614 | "importance": 30, 615 | "type": "total" 616 | }, 617 | "UpdateCounters": { 618 | "tags": ["Paint"], 619 | "source": "TimelineMetrics", 620 | "browsers": ["chrome", "safari", "android"], 621 | "unit": "ms", 622 | "importance": 30, 623 | "type": "total" 624 | }, 625 | "PaintImage": { 626 | "tags": ["Paint"], 627 | "source": "TimelineMetrics", 628 | "browsers": ["chrome", "safari", "android"], 629 | "unit": "ms", 630 | "summary": "Time spent to paint an image", 631 | "importance": 30, 632 | "type": "total" 633 | }, 634 | "PaintImage_avg": { 635 | "tags": ["Paint"], 636 | "source": "TimelineMetrics", 637 | "browsers": ["chrome", "safari", "android"], 638 | "unit": "ms", 639 | "summary": "Average time spent to paint images", 640 | "importance": 50, 641 | "type": "average" 642 | }, 643 | "PaintImage_max": { 644 | "tags": ["Paint"], 645 | "source": "TimelineMetrics", 646 | "browsers": ["chrome", "safari", "android"], 647 | "unit": "ms", 648 | "summary": "Maximum time spent to paint images", 649 | "importance": 30, 650 | "type": "max" 651 | }, 652 | "PaintImage_count": { 653 | "tags": ["Paint"], 654 | "source": "TimelineMetrics", 655 | "browsers": ["chrome", "safari", "android"], 656 | "unit": "count", 657 | "summary": "Count of painting images operations", 658 | "importance": 40, 659 | "type": "count" 660 | }, 661 | "Draw LazyPixelRef": { 662 | "tags": ["Paint"], 663 | "source": "TimelineMetrics", 664 | "browsers": ["chrome", "safari", "android"], 665 | "unit": "ms", 666 | "importance": 10, 667 | "type": "total" 668 | }, 669 | "UpdateLayer": { 670 | "tags": ["Paint"], 671 | "source": "TimelineMetrics", 672 | "browsers": ["chrome", "safari", "android"], 673 | "unit": "ms", 674 | "importance": 10, 675 | "type": "total" 676 | }, 677 | "UpdateLayer_avg": { 678 | "tags": ["Paint"], 679 | "source": "TimelineMetrics", 680 | "browsers": ["chrome", "safari", "android"], 681 | "unit": "ms", 682 | "importance": 10, 683 | "type": "average" 684 | }, 685 | "UpdateLayer_max": { 686 | "tags": ["Paint"], 687 | "source": "TimelineMetrics", 688 | "browsers": ["chrome", "safari", "android"], 689 | "unit": "ms", 690 | "importance": 10, 691 | "type": "max" 692 | }, 693 | "UpdateLayer_count": { 694 | "tags": ["Paint"], 695 | "source": "TimelineMetrics", 696 | "browsers": ["chrome", "safari", "android"], 697 | "unit": "count", 698 | "importance": 10, 699 | "type": "count" 700 | }, 701 | "AnalyzeTask": { 702 | "tags": ["Paint"], 703 | "source": "TimelineMetrics", 704 | "browsers": ["chrome", "safari", "android"], 705 | "unit": "ms", 706 | "importance": 10, 707 | "type": "total" 708 | }, 709 | "AnalyzeTask_avg": { 710 | "tags": ["Paint"], 711 | "source": "TimelineMetrics", 712 | "browsers": ["chrome", "safari", "android"], 713 | "unit": "ms", 714 | "importance": 10, 715 | "type": "average" 716 | }, 717 | "AnalyzeTask_max": { 718 | "tags": ["Paint"], 719 | "source": "TimelineMetrics", 720 | "browsers": ["chrome", "safari", "android"], 721 | "unit": "ms", 722 | "importance": 10, 723 | "type": "max" 724 | }, 725 | "AnalyzeTask_count": { 726 | "tags": ["Paint"], 727 | "source": "TimelineMetrics", 728 | "browsers": ["chrome", "safari", "android"], 729 | "unit": "count", 730 | "importance": 10, 731 | "type": "count" 732 | }, 733 | "Decode Image": { 734 | "tags": ["Paint"], 735 | "source": "TimelineMetrics", 736 | "browsers": ["chrome", "safari", "android"], 737 | "unit": "ms", 738 | "summary": "Time spent to decoding an image", 739 | "importance": 20, 740 | "type": "total" 741 | }, 742 | "Decode Image_avg": { 743 | "tags": ["Paint"], 744 | "source": "TimelineMetrics", 745 | "browsers": ["chrome", "safari", "android"], 746 | "unit": "ms", 747 | "summary": "Average time spent to decoding an image", 748 | "importance": 50, 749 | "type": "average" 750 | }, 751 | "Decode Image_max": { 752 | "tags": ["Paint"], 753 | "source": "TimelineMetrics", 754 | "browsers": ["chrome", "safari", "android"], 755 | "unit": "ms", 756 | "summary": "Maximum time spent to decoding an image", 757 | "importance": 30, 758 | "type": "max" 759 | }, 760 | "Decode Image_count": { 761 | "tags": ["Paint"], 762 | "source": "TimelineMetrics", 763 | "browsers": ["chrome", "safari", "android"], 764 | "unit": "count", 765 | "summary": "Count of images decoding", 766 | "importance": 40, 767 | "type": "count" 768 | }, 769 | "Decode LazyPixelRef": { 770 | "tags": ["Paint"], 771 | "source": "TimelineMetrics", 772 | "browsers": ["chrome", "safari", "android"], 773 | "unit": "ms", 774 | "importance": 10, 775 | "type": "total" 776 | }, 777 | "Decode LazyPixelRef_avg": { 778 | "tags": ["Paint"], 779 | "source": "TimelineMetrics", 780 | "browsers": ["chrome", "safari", "android"], 781 | "unit": "ms", 782 | "importance": 10, 783 | "type": "average" 784 | }, 785 | "Decode LazyPixelRef_max": { 786 | "tags": ["Paint"], 787 | "source": "TimelineMetrics", 788 | "browsers": ["chrome", "safari", "android"], 789 | "unit": "ms", 790 | "importance": 10, 791 | "type": "max" 792 | }, 793 | "Decode LazyPixelRef_count": { 794 | "tags": ["Paint"], 795 | "source": "TimelineMetrics", 796 | "browsers": ["chrome", "safari", "android"], 797 | "unit": "count", 798 | "importance": 10, 799 | "type": "count" 800 | }, 801 | "ImageDecodeTask": { 802 | "tags": ["Paint"], 803 | "source": "TimelineMetrics", 804 | "browsers": ["chrome", "safari", "android"], 805 | "unit": "ms", 806 | "importance": 30, 807 | "type": "total" 808 | }, 809 | "ImageDecodeTask_avg": { 810 | "tags": ["Paint"], 811 | "source": "TimelineMetrics", 812 | "browsers": ["chrome", "safari", "android"], 813 | "unit": "ms", 814 | "importance": 50, 815 | "type": "average" 816 | }, 817 | "ImageDecodeTask_max": { 818 | "tags": ["Paint"], 819 | "source": "TimelineMetrics", 820 | "browsers": ["chrome", "safari", "android"], 821 | "unit": "ms", 822 | "importance": 30, 823 | "type": "max" 824 | }, 825 | "ImageDecodeTask_count": { 826 | "tags": ["Paint"], 827 | "source": "TimelineMetrics", 828 | "browsers": ["chrome", "safari", "android"], 829 | "unit": "count", 830 | "importance": 40, 831 | "type": "count" 832 | }, 833 | "RasterTask": { 834 | "tags": ["Paint"], 835 | "source": "TimelineMetrics", 836 | "browsers": ["chrome", "safari", "android"], 837 | "unit": "ms", 838 | "importance": 10, 839 | "type": "total" 840 | }, 841 | "RasterTask_avg": { 842 | "tags": ["Paint"], 843 | "source": "TimelineMetrics", 844 | "browsers": ["chrome", "safari", "android"], 845 | "unit": "ms", 846 | "importance": 10, 847 | "type": "average" 848 | }, 849 | "RasterTask_max": { 850 | "tags": ["Paint"], 851 | "source": "TimelineMetrics", 852 | "browsers": ["chrome", "safari", "android"], 853 | "unit": "ms", 854 | "importance": 10, 855 | "type": "max" 856 | }, 857 | "RasterTask_count": { 858 | "tags": ["Paint"], 859 | "source": "TimelineMetrics", 860 | "browsers": ["chrome", "safari", "android"], 861 | "unit": "count", 862 | "importance": 10, 863 | "type": "count" 864 | }, 865 | "TimerInstall": { 866 | "tags": ["Paint"], 867 | "source": "TimelineMetrics", 868 | "browsers": ["chrome", "safari", "android"], 869 | "unit": "ms", 870 | "importance": 35, 871 | "type": "total" 872 | }, 873 | "InvalidateLayout": { 874 | "tags": ["Paint"], 875 | "source": "TimelineMetrics", 876 | "browsers": ["chrome", "safari", "android"], 877 | "unit": "ms", 878 | "importance": 68, 879 | "type": "total" 880 | }, 881 | "GPUTask": { 882 | "tags": ["Paint"], 883 | "source": "TimelineMetrics", 884 | "browsers": ["chrome", "safari", "android"], 885 | "unit": "ms", 886 | "importance": 10, 887 | "type": "total" 888 | }, 889 | "GPUTask_avg": { 890 | "tags": ["Paint"], 891 | "source": "TimelineMetrics", 892 | "browsers": ["chrome", "safari", "android"], 893 | "unit": "ms", 894 | "importance": 10, 895 | "type": "average" 896 | }, 897 | "GPUTask_max": { 898 | "tags": ["Paint"], 899 | "source": "TimelineMetrics", 900 | "browsers": ["chrome", "safari", "android"], 901 | "unit": "ms", 902 | "importance": 10, 903 | "type": "max" 904 | }, 905 | "GPUTask_count": { 906 | "tags": ["Paint"], 907 | "source": "TimelineMetrics", 908 | "browsers": ["chrome", "safari", "android"], 909 | "unit": "count", 910 | "importance": 10, 911 | "type": "count" 912 | }, 913 | "TimerRemove": { 914 | "tags": ["Paint"], 915 | "source": "TimelineMetrics", 916 | "browsers": ["chrome", "safari", "android"], 917 | "unit": "ms", 918 | "importance": 30, 919 | "type": "total" 920 | }, 921 | "ScheduleStyleRecalculation": { 922 | "tags": ["Paint"], 923 | "source": "TimelineMetrics", 924 | "browsers": ["chrome", "safari", "android"], 925 | "unit": "ms", 926 | "importance": 30, 927 | "type": "total" 928 | }, 929 | "MarkLoad": { 930 | "tags": ["Paint"], 931 | "source": "TimelineMetrics", 932 | "browsers": ["chrome", "safari", "android"], 933 | "unit": "ms", 934 | "importance": 10, 935 | "type": "total" 936 | }, 937 | "XHRLoad": { 938 | "tags": ["Paint"], 939 | "source": "TimelineMetrics", 940 | "browsers": ["chrome", "safari", "android"], 941 | "unit": "ms", 942 | "importance": 10, 943 | "type": "total" 944 | }, 945 | "XHRLoad_avg": { 946 | "tags": ["Paint"], 947 | "source": "TimelineMetrics", 948 | "browsers": ["chrome", "safari", "android"], 949 | "unit": "ms", 950 | "importance": 10, 951 | "type": "average" 952 | }, 953 | "XHRLoad_max": { 954 | "tags": ["Paint"], 955 | "source": "TimelineMetrics", 956 | "browsers": ["chrome", "safari", "android"], 957 | "unit": "ms", 958 | "importance": 10, 959 | "type": "max" 960 | }, 961 | "XHRLoad_count": { 962 | "tags": ["Paint"], 963 | "source": "TimelineMetrics", 964 | "browsers": ["chrome", "safari", "android"], 965 | "unit": "count", 966 | "importance": 10, 967 | "type": "count" 968 | }, 969 | "MarkDOMContent": { 970 | "tags": ["Paint"], 971 | "source": "TimelineMetrics", 972 | "browsers": ["chrome", "safari", "android"], 973 | "unit": "ms", 974 | "importance": 10, 975 | "type": "total" 976 | }, 977 | "CommitLoad": { 978 | "tags": ["Paint"], 979 | "source": "TimelineMetrics", 980 | "browsers": ["chrome", "safari", "android"], 981 | "unit": "ms", 982 | "importance": 10, 983 | "type": "total" 984 | }, 985 | "CommitLoad_avg": { 986 | "tags": ["Paint"], 987 | "source": "TimelineMetrics", 988 | "browsers": ["chrome", "safari", "android"], 989 | "unit": "ms", 990 | "importance": 10, 991 | "type": "average" 992 | }, 993 | "CommitLoad_max": { 994 | "tags": ["Paint"], 995 | "source": "TimelineMetrics", 996 | "browsers": ["chrome", "safari", "android"], 997 | "unit": "ms", 998 | "importance": 10, 999 | "type": "max" 1000 | }, 1001 | "CommitLoad_count": { 1002 | "tags": ["Paint"], 1003 | "source": "TimelineMetrics", 1004 | "browsers": ["chrome", "safari", "android"], 1005 | "unit": "count", 1006 | "importance": 10, 1007 | "type": "count" 1008 | } 1009 | } 1010 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | var Docs = function(source) { 2 | var glob = require('glob'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | var files; 7 | if (typeof source !== 'undefined' && typeof source === 'string') { 8 | files = [source + '.json']; 9 | } else if (Array.isArray(source)) { 10 | files = source.map(function(file) { 11 | return file + (path.extname(file) !== '.json' ? '.json' : ''); 12 | }); 13 | } else { 14 | files = glob.sync('*.json', { 15 | cwd: __dirname 16 | }); 17 | } 18 | 19 | this.metrics = {}; 20 | for (var i = 0; i < files.length; i++) { 21 | var fileContent = JSON.parse(fs.readFileSync(path.join(__dirname, files[i]), 'utf-8')); 22 | for (var key in fileContent) { 23 | this.metrics[key] = fileContent[key]; 24 | } 25 | } 26 | }; 27 | 28 | Docs.prototype.get = function(metric) { 29 | return this.metrics[metric] || {}; 30 | }; 31 | 32 | Docs.prototype.getProp = function(metric, prop) { 33 | return (typeof this.metrics[metric] !== 'undefined' ? this.metrics[metric][prop] : undefined); 34 | }; 35 | 36 | module.exports = Docs; -------------------------------------------------------------------------------- /lib/actions/index.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | 3 | var builtInActions = { 4 | scroll: require('./scrollAction.js'), 5 | login: require('./loginAction.js'), 6 | wait: require('./wait.js') 7 | }; 8 | 9 | var Actions = function(actionList) { 10 | var emptyAction = function() { 11 | return Q(); 12 | }; 13 | this.actionList = actionList.map(function(action) { 14 | if (typeof action === 'string' && builtInActions[action] !== 'undefined') { 15 | return builtInActions[action](); 16 | } else if (typeof action === 'function') { 17 | return action; 18 | } else { 19 | throw 'Could not find action ' + action; 20 | } 21 | }); 22 | }; 23 | 24 | Actions.prototype.forEachAction_ = function(cb) { 25 | return this.actionList.map(function(action) { 26 | return function() { 27 | return cb(action); 28 | } 29 | }).reduce(Q.when, Q()); 30 | }; 31 | 32 | Actions.prototype.setup = function(cfg) { 33 | return this.forEachAction_(function(action) { 34 | if (typeof action.setup === 'function') { 35 | return action.setup(cfg); 36 | } else { 37 | return Q(); 38 | } 39 | }); 40 | }; 41 | 42 | Actions.prototype.perform = function(browser) { 43 | return this.forEachAction_(function(action) { 44 | return action(browser); 45 | }); 46 | }; 47 | 48 | module.exports = Actions; 49 | module.exports.actions = builtInActions; -------------------------------------------------------------------------------- /lib/actions/loginAction.js: -------------------------------------------------------------------------------- 1 | module.exports = function(cfg) { 2 | cfg = cfg || {}; 3 | debug = require('debug')('bp:actions:login'); 4 | return function(browser) { 5 | debug('Loading login form'); 6 | return browser.get(cfg.page).then(function() { 7 | debug('Filling in Username'); 8 | return browser.elementByCssSelector(cfg.username.field) 9 | }).then(function(el) { 10 | return el.type(cfg.username.val || cfg.username.value); 11 | }).then(function() { 12 | debug('Filling in Password'); 13 | return browser.elementByCssSelector(cfg.password.field); 14 | }).then(function(el) { 15 | return el.type(cfg.password.val || cfg.password.value); 16 | }).then(function() { 17 | return browser.elementByCssSelector(cfg.submit.field); 18 | }).then(function(el) { 19 | debug('Clicking submit'); 20 | return el.click(); 21 | }).then(function() { 22 | return browser.sleep(5000); 23 | }); 24 | } 25 | }; -------------------------------------------------------------------------------- /lib/actions/page_scripts/chrome_scroll.js: -------------------------------------------------------------------------------- 1 | /* File From chromium/src/tools/telemetry/telemetry/page/actions/scroll.js */ 2 | 3 | // Copyright (c) 2012 The Chromium Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style license that can be 5 | // found in the LICENSE file. 6 | 7 | // This file provides the ScrollAction object, which scrolls a page 8 | // to the bottom or for a specified distance: 9 | // 1. var action = new __ScrollAction(callback, opt_distance_func) 10 | // 2. action.start(scroll_options) 11 | 'use strict'; 12 | 13 | (function() { 14 | var MAX_SCROLL_LENGTH_PIXELS = 5000; 15 | 16 | function ScrollGestureOptions(opt_options) { 17 | if (opt_options) { 18 | this.element_ = opt_options.element; 19 | this.left_start_percentage_ = opt_options.left_start_percentage; 20 | this.top_start_percentage_ = opt_options.top_start_percentage; 21 | this.direction_ = opt_options.direction; 22 | this.speed_ = opt_options.speed; 23 | this.gesture_source_type_ = opt_options.gesture_source_type; 24 | } else { 25 | this.element_ = document.body; 26 | this.left_start_percentage_ = 0.5; 27 | this.top_start_percentage_ = 0.5; 28 | this.direction_ = 'down'; 29 | this.speed_ = 800; 30 | this.gesture_source_type_ = chrome.gpuBenchmarking.DEFAULT_INPUT; 31 | } 32 | } 33 | 34 | function supportedByBrowser() { 35 | return !!(window.chrome && 36 | chrome.gpuBenchmarking && 37 | chrome.gpuBenchmarking.smoothScrollBy); 38 | } 39 | 40 | // This class scrolls a page from the top to the bottom once. 41 | // 42 | // The page is scrolled down by a single scroll gesture. 43 | function ScrollAction(opt_callback, opt_distance_func) { 44 | var self = this; 45 | 46 | this.beginMeasuringHook = function() {} 47 | this.endMeasuringHook = function() {} 48 | 49 | this.callback_ = opt_callback; 50 | this.distance_func_ = opt_distance_func; 51 | } 52 | 53 | ScrollAction.prototype.getScrollDistance_ = function() { 54 | if (this.distance_func_) 55 | return this.distance_func_(); 56 | 57 | if (this.options_.direction_ == 'down') { 58 | var clientHeight; 59 | // clientHeight is "special" for the body element. 60 | if (this.element_ == document.body) 61 | clientHeight = window.innerHeight; 62 | else 63 | clientHeight = this.element_.clientHeight; 64 | 65 | return this.element_.scrollHeight - 66 | this.element_.scrollTop - 67 | clientHeight; 68 | } else if (this.options_.direction_ == 'up') { 69 | return this.element_.scrollTop; 70 | } else if (this.options_.direction_ == 'right') { 71 | var clientWidth; 72 | // clientWidth is "special" for the body element. 73 | if (this.element_ == document.body) 74 | clientWidth = window.innerWidth; 75 | else 76 | clientWidth = this.element_.clientWidth; 77 | 78 | return this.element_.scrollWidth - this.element_.scrollLeft - clientWidth; 79 | } else if (this.options_.direction_ == 'left') { 80 | return this.element_.scrollLeft; 81 | } 82 | } 83 | 84 | ScrollAction.prototype.start = function(opt_options) { 85 | this.options_ = new ScrollGestureOptions(opt_options); 86 | // Assign this.element_ here instead of constructor, because the constructor 87 | // ensures this method will be called after the document is loaded. 88 | this.element_ = this.options_.element_; 89 | requestAnimationFrame(this.startGesture_.bind(this)); 90 | }; 91 | 92 | ScrollAction.prototype.startGesture_ = function() { 93 | this.beginMeasuringHook(); 94 | 95 | var distance = Math.min(MAX_SCROLL_LENGTH_PIXELS, 96 | this.getScrollDistance_()); 97 | 98 | var rect = __GestureCommon_GetBoundingVisibleRect(this.options_.element_); 99 | var start_left = 100 | rect.left + rect.width * this.options_.left_start_percentage_; 101 | var start_top = 102 | rect.top + rect.height * this.options_.top_start_percentage_; 103 | chrome.gpuBenchmarking.smoothScrollBy( 104 | distance, this.onGestureComplete_.bind(this), start_left, start_top, 105 | this.options_.gesture_source_type_, this.options_.direction_, 106 | this.options_.speed_); 107 | }; 108 | 109 | ScrollAction.prototype.onGestureComplete_ = function() { 110 | this.endMeasuringHook(); 111 | 112 | // We're done. 113 | if (this.callback_) 114 | this.callback_(); 115 | }; 116 | 117 | window.__ScrollAction = ScrollAction; 118 | window.__ScrollAction_SupportedByBrowser = supportedByBrowser; 119 | })(); -------------------------------------------------------------------------------- /lib/actions/page_scripts/gesture_common.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // This file provides common functionality for synthetic gesture actions. 6 | 'use strict'; 7 | 8 | (function() { 9 | 10 | function getBoundingVisibleRect(el) { 11 | var bound = el.getBoundingClientRect(); 12 | var rect = { top: bound.top, 13 | left: bound.left, 14 | width: bound.width, 15 | height: bound.height }; 16 | if (rect.top < 0) { 17 | rect.height += rect.top; 18 | rect.top = 0; 19 | } 20 | if (rect.left < 0) { 21 | rect.width += rect.left; 22 | rect.left = 0; 23 | } 24 | 25 | var outsideHeight = (rect.top + rect.height) - window.innerHeight; 26 | var outsideWidth = (rect.left + rect.width) - window.innerWidth; 27 | 28 | if (outsideHeight > 0) { 29 | rect.height -= outsideHeight; 30 | } 31 | if (outsideWidth > 0) { 32 | rect.width -= outsideWidth; 33 | } 34 | return rect; 35 | }; 36 | 37 | window.__GestureCommon_GetBoundingVisibleRect = getBoundingVisibleRect; 38 | })(); -------------------------------------------------------------------------------- /lib/actions/page_scripts/raf_scroll.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // This file provides the ScrollAction object, which scrolls a page 6 | // from top to bottom: 7 | // 1. var action = new __ScrollAction(callback) 8 | // 2. action.start(element_to_scroll) 9 | 'use strict'; 10 | 11 | (function() { 12 | var MAX_SCROLL_LENGTH_PIXELS = 5000; 13 | 14 | var getTimeMs = (function() { 15 | if (window.performance) 16 | return (performance.now || 17 | performance.mozNow || 18 | performance.msNow || 19 | performance.oNow || 20 | performance.webkitNow).bind(window.performance); 21 | else 22 | return function() { 23 | return new Date().getTime(); 24 | }; 25 | })(); 26 | 27 | var requestAnimationFrame = (function() { 28 | return window.requestAnimationFrame || 29 | window.webkitRequestAnimationFrame || 30 | window.mozRequestAnimationFrame || 31 | window.oRequestAnimationFrame || 32 | window.msRequestAnimationFrame || 33 | function(callback) { 34 | window.setTimeout(callback, 1000 / 60); 35 | }; 36 | })().bind(window); 37 | 38 | /** 39 | * Scrolls a given element down a certain amount to emulate user scroll. 40 | * Uses smooth scroll capabilities provided by the platform, if available. 41 | * @constructor 42 | */ 43 | function SmoothScrollDownGesture(opt_element) { 44 | this.element_ = opt_element || document.body; 45 | }; 46 | 47 | function min(a, b) { 48 | if (a > b) { 49 | return b; 50 | } 51 | return a; 52 | }; 53 | 54 | function getBoundingVisibleRect(el) { 55 | var bound = el.getBoundingClientRect(); 56 | var rect = { 57 | top: bound.top, 58 | left: bound.left, 59 | width: bound.width, 60 | height: bound.height 61 | }; 62 | var outsideHeight = (rect.top + rect.height) - window.innerHeight; 63 | var outsideWidth = (rect.left + rect.width) - window.innerWidth; 64 | 65 | if (outsideHeight > 0) { 66 | rect.height -= outsideHeight; 67 | } 68 | if (outsideWidth > 0) { 69 | rect.width -= outsideWidth; 70 | } 71 | return rect; 72 | }; 73 | 74 | SmoothScrollDownGesture.prototype.start = function(distance, callback) { 75 | this.callback_ = callback; 76 | var SCROLL_DELTA = 100; 77 | this.element_.scrollTop += SCROLL_DELTA; 78 | requestAnimationFrame(callback); 79 | }; 80 | 81 | // This class scrolls a page from the top to the bottom once. 82 | // 83 | // The page is scrolled down by a set of scroll gestures. These gestures 84 | // correspond to a reading gesture on that platform. 85 | // 86 | // start -> startPass_ -> ...scroll... -> onGestureComplete_ -> 87 | // -> startPass_ -> .. scroll... -> onGestureComplete_ -> callback_ 88 | function ScrollAction(opt_callback) { 89 | var self = this; 90 | 91 | this.beginMeasuringHook = function() {} 92 | this.endMeasuringHook = function() {} 93 | 94 | this.callback_ = opt_callback; 95 | } 96 | 97 | ScrollAction.prototype.getRemainingScrollDistance_ = function() { 98 | var clientHeight; 99 | // clientHeight is "special" for the body element. 100 | if (this.element_ == document.body) 101 | clientHeight = window.innerHeight; 102 | else 103 | clientHeight = this.element_.clientHeight; 104 | 105 | return this.scrollHeight_ - this.element_.scrollTop - clientHeight; 106 | } 107 | 108 | ScrollAction.prototype.start = function(opts) { 109 | opts = opts || { 110 | element: document.body 111 | }; 112 | var opt_element = opts.element; 113 | if (opt_element === document.body) { 114 | opt_element.scrollTop = 1; 115 | if (opt_element.scrollTop !== 1) { 116 | opt_element = document.documentElement; 117 | } 118 | } 119 | // Assign this.element_ here instead of constructor, because the constructor 120 | // ensures this method will be called after the document is loaded. 121 | this.element_ = opt_element || document.body; 122 | // Some pages load more content when you scroll to the bottom. Record 123 | // the original element height here and only scroll to that point. 124 | // -1 to allow for rounding errors on scaled viewports (like mobile). 125 | this.scrollHeight_ = Math.min(MAX_SCROLL_LENGTH_PIXELS, 126 | this.element_.scrollHeight - 1); 127 | requestAnimationFrame(this.startPass_.bind(this)); 128 | }; 129 | 130 | ScrollAction.prototype.startPass_ = function() { 131 | this.element_.scrollTop = 0; 132 | 133 | this.beginMeasuringHook(); 134 | 135 | this.gesture_ = new SmoothScrollDownGesture(this.element_); 136 | this.gesture_.start(this.getRemainingScrollDistance_(), 137 | this.onGestureComplete_.bind(this)); 138 | }; 139 | 140 | ScrollAction.prototype.getResults = function() { 141 | return this.renderingStats_; 142 | } 143 | 144 | ScrollAction.prototype.onGestureComplete_ = function() { 145 | // If the scrollHeight went down, only scroll to the new scrollHeight. 146 | // -1 to allow for rounding errors on scaled viewports (like mobile). 147 | this.scrollHeight_ = Math.min(this.scrollHeight_, 148 | this.element_.scrollHeight - 1); 149 | 150 | if (this.getRemainingScrollDistance_() > 0) { 151 | this.gesture_.start(this.getRemainingScrollDistance_(), 152 | this.onGestureComplete_.bind(this)); 153 | return; 154 | } 155 | 156 | this.endMeasuringHook(); 157 | 158 | // We're done. 159 | if (this.callback_) 160 | this.callback_(); 161 | }; 162 | 163 | if (!window.chrome) { 164 | console.log('Chrome Scroll replaced with regular scroll'); 165 | window.__ScrollAction = ScrollAction; 166 | window.__ScrollAction_GetBoundingVisibleRect = getBoundingVisibleRect; 167 | } 168 | })(); -------------------------------------------------------------------------------- /lib/actions/scrollAction.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | cfg - options for scrolling. Properties of cfg can be 4 | 5 | direction : up|down|left|right (default: down) 6 | scrollElement : Javascript code evaluated in browser to pick element to scroll (default: document.body) 7 | speed : scroll speed (default: 800) 8 | pollFreq : frequency at which polling should be done to check if scroll is complete (default: 200) 9 | 10 | To scroll a custom element, look at the wiki 11 | 12 | */ 13 | var fs = require('fs'), 14 | wd = require('wd'), 15 | Q = require('q'), 16 | debug = require('debug')('bp:actions:scroll'), 17 | jsmin = require('jsmin').jsmin, 18 | helpers = require('../helpers'); 19 | 20 | module.exports = function(cfg) { 21 | cfg = cfg || {}; 22 | 23 | function scroll(browser) { 24 | 25 | var raf_scroll = jsmin(fs.readFileSync(__dirname + '/page_scripts/raf_scroll.js', 'utf-8')), 26 | chrome_scroll = jsmin(fs.readFileSync(__dirname + '/page_scripts/chrome_scroll.js', 'utf-8')), 27 | gesture_common = jsmin(fs.readFileSync(__dirname + '/page_scripts/gesture_common.js', 'utf-8')); 28 | 29 | var runner = function(opts) { 30 | console.log("Scrolling with ", opts); 31 | window.__scrollActionDone = false; 32 | window.__scrollAction = new __ScrollAction(function() { 33 | window.__scrollActionDone = true; 34 | }); 35 | window.__scrollAction.start({ 36 | element: eval(opts.el), 37 | left_start_percentage: opts.left_start_percentage, 38 | top_start_percentage: opts.top_start_percentage, 39 | direction: opts.direction, 40 | speed: opts.speed, 41 | gesture_source_type: opts.gesture_source_type 42 | }); 43 | }; 44 | 45 | debug('Initializing Scroll function'); 46 | return browser.execute(gesture_common + chrome_scroll + raf_scroll + helpers.fnCall(runner, { 47 | left_start_percentage: 0.5, 48 | top_start_percentage: 0.5, 49 | direction: cfg.direction || 'down', 50 | speed: cfg.speed || 800, 51 | gesture_source_type: 0, 52 | el: cfg.scrollElement || 'document.body' 53 | })).then(function() { 54 | debug('Waiting for Scrolling to finish'); 55 | return browser.waitFor({ 56 | asserter: wd.asserters.jsCondition('(window.__scrollActionDone === true)', false), 57 | timeout: 1000 * 60 * 10, 58 | pollFreq: cfg.pollFreq || 2000 59 | }); 60 | }) 61 | }; 62 | 63 | scroll.setup = function(cfg) { 64 | cfg.browsers = cfg.browsers.map(function(browser) { 65 | if (browser.browserName && (browser.browserName.match(/android/i) || browser.browserName.match(/chrome/i))) { 66 | helpers.extend(browser, { 67 | chromeOptions: { 68 | args: ['--enable-gpu-benchmarking', '--enable-thread-composting'] 69 | } 70 | }); 71 | } 72 | return browser; 73 | }); 74 | return Q(cfg); 75 | }; 76 | 77 | return scroll; 78 | }; 79 | -------------------------------------------------------------------------------- /lib/actions/wait.js: -------------------------------------------------------------------------------- 1 | module.exports = function(cfg) { 2 | cfg = cfg || {}; 3 | var debug = require('debug')('bp:actions:wait'); 4 | return function(browser) { 5 | var duration = cfg || 5000; 6 | debug('Waiting for some time - %d', duration); 7 | return browser.sleep(duration); 8 | } 9 | }; -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var version = require('../package.json').version; 4 | var program = require('commander'); 5 | var debug = require('debug'); 6 | 7 | program 8 | .version(version) 9 | .option('-s, --selenium ', 'Specify Selenium Server, like localhost:4444 or ondemand.saucelabs.com:80') 10 | .option('-v --verbose') 11 | .option('-u --username ', 'Sauce, BrowserStack or Selenium User Name') 12 | .option('-a --accesskey ', 'Sauce, BrowserStack or Selenium Access Key') 13 | .option('-d --debug', 'Enable debug mode, keep the browser alive even after the tests', false) 14 | .option('-c --config-file ', 'Specify a configuration file. If other options are specified, they have precedence over options in config file') 15 | .option('-o --output ', 'Format of output [table|json], defaults to table') 16 | .option('--prescript-file ', 'Prescript node module to run (for login, etc) before the tests. Module should be module.exports = function(browser, callback){}') 17 | .option('--all', 'Display all metrics') 18 | .option('-b --browsers ', 'Browsers to run the test on', function(val) { 19 | return val.split(','); 20 | }) 21 | .parse(process.argv); 22 | 23 | var url = program.args[0]; 24 | program.output = program.output || 'table'; 25 | 26 | var MAX_IMPORTANCE = 30; 27 | if (program.all){ 28 | MAX_IMPORTANCE = 0; 29 | } 30 | 31 | if (program.verbose && !process.env.DEBUG) { 32 | process.env.DEBUG = 'bp:*'; 33 | } 34 | 35 | require('./index.js')(url, function(err, data) { 36 | if (err) { 37 | err.forEach(function(e){ 38 | if (e.errno === 'GRID_CONFIG_ERROR'){ 39 | console.log('Please check your Selenium config. Cannot reach grid at '+ e.address + ':' + e.port); 40 | } else { 41 | console.log(e.stack ? e.stack : e); 42 | } 43 | }); 44 | } else { 45 | if (program.output === 'table') { 46 | console.log(generateTable(data)); 47 | } else { 48 | console.log(data); 49 | } 50 | } 51 | }, { 52 | browsers: program.browsers, 53 | selenium: program.selenium, 54 | username: program.username, 55 | accesskey: program.accesskey, 56 | debugBrowser: program.debug, 57 | configFile: program.configFile, 58 | preScriptFile: program.prescriptFile 59 | }); 60 | 61 | function generateTable(data) { 62 | var cliTable = require('cli-table'); 63 | var Docs = require('../docs'); 64 | 65 | var apiDocs = new Docs(); 66 | var decimalPoints = { 67 | ms: 3, 68 | count: 0, 69 | fps: 3, 70 | percentage: 2, 71 | } 72 | 73 | var res = []; 74 | for (var i = 0; i < data.length; i++) { 75 | res.push('\n\nBrowser: ', data[i]._browserName + '\n'); 76 | var table = new cliTable({ 77 | head: ['Metrics', 'Value', 'Unit', 'Source'], 78 | colAligns: ['right', 'right', 'left', 'right'], 79 | colWidths: [35, 20, 10, 15] 80 | }); 81 | for (var key in data[i]) { 82 | if (key.indexOf('_') === 0) 83 | continue; 84 | if ((apiDocs.getProp(key, 'importance') || 0) < MAX_IMPORTANCE) 85 | continue; 86 | 87 | var val = data[i][key]; 88 | var unit = '' + (apiDocs.getProp(key, 'unit') || ''); 89 | if (typeof val === 'number') { 90 | if (typeof decimalPoints[unit] !== 'undefined') { 91 | val = val.toFixed(decimalPoints[unit]); 92 | } else { 93 | val = val + ''; 94 | } 95 | } 96 | 97 | table.push([key, val + '', unit, '' + (apiDocs.getProp(key, 'source') || '')]); 98 | } 99 | table = table.sort(function(a, b) { 100 | var rankA = apiDocs.getProp(a[0], 'importance') || -1; 101 | var rankB = apiDocs.getProp(b[0], 'importance') || -1; 102 | 103 | if (rankA === rankB) { 104 | if (a[3] === b[3]) { 105 | return a[0] > b[0] ? 1 : -1; 106 | } else { 107 | return a[3] > b[3] ? 1 : -1; 108 | } 109 | } else { 110 | return rankA > rankB ? 1 : (rankA < rankB ? -1 : 0); 111 | } 112 | }) 113 | res.push(table.toString()); 114 | } 115 | return res.join(''); 116 | } 117 | 118 | // END of File 119 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | var extend = function(obj1, obj2) { 2 | if (typeof obj1 !== 'object' && !obj1) { 3 | obj1 = {}; 4 | } 5 | if (typeof obj2 !== 'object' && !obj2) { 6 | obj2 = {}; 7 | } 8 | for (var key in obj2) { 9 | if (Array.isArray(obj1[key]) && Array.isArray(obj2[key])) { 10 | obj1[key] = obj1[key].concat(obj2[key]); 11 | } else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object' && !Array.isArray(obj1[key]) && !Array.isArray(obj2[key])) { 12 | obj1[key] = extend(obj1[key], obj2[key]); 13 | } else { 14 | obj1[key] = obj2[key]; 15 | } 16 | } 17 | return obj1; 18 | } 19 | 20 | var deepEquals = function(global, prop, val) { 21 | var props = prop.split('.'); 22 | for (var i = 0; i < props.length; i++) { 23 | if (typeof global !== 'object' || global === null){ 24 | return false; 25 | } 26 | 27 | if (typeof global[props[i]] === 'undefined') { 28 | return false; 29 | } 30 | global = global[props[i]]; 31 | } 32 | return global === val; 33 | }; 34 | 35 | var metrics = function(value, category, source, unit, importance, tags) { 36 | return { 37 | value: value, 38 | unit: unit || 'ms', 39 | category: category, 40 | source: source, 41 | tags: tags || [], 42 | importance: importance || 0 43 | } 44 | } 45 | 46 | metrics.format = function(metric) { 47 | var res = [metric.value, metric.unit]; 48 | return res.join(' '); 49 | }; 50 | 51 | metrics.log = function(m) { 52 | var res = {}; 53 | for (var key in m) { 54 | res[key] = metrics.format(m[key]); 55 | } 56 | return res; 57 | } 58 | 59 | var jsmin = require('jsmin').jsmin; 60 | 61 | module.exports = { 62 | fnCall: function(fn, args) { 63 | args = args || ''; 64 | if (typeof args === 'object') { 65 | args = JSON.stringify(args); 66 | } 67 | return '(' + jsmin(fn.toString()) + '(' + args + '));'; 68 | }, 69 | metrics: metrics, 70 | extend: extend, 71 | deepEquals: deepEquals 72 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | wd = require('wd'), 3 | Actions = require('./actions'), 4 | Metrics = require('./metrics'), 5 | helpers = require('./helpers'); 6 | 7 | var debug = require('debug'), 8 | log = debug('bp:index'), 9 | seleniumLog = debug('bp:selenium'); 10 | 11 | function main(url, cb, cfg) { 12 | var opts = require('./options').scrub(cfg), 13 | errors = [], 14 | results = []; 15 | var res = [], 16 | err = []; 17 | opts.browsers.map(function(browserConfig) { 18 | return function() { 19 | return runOnBrowser(url, browserConfig, opts).then(function(data) { 20 | data._browserName = browserConfig.browserName; 21 | data._url = url; 22 | res.push(data); 23 | }, function(error) { 24 | if (typeof error === "object" && error.code === 'ECONNREFUSED') { 25 | error.errno = 'GRID_CONFIG_ERROR'; 26 | } 27 | err.push(error); 28 | }); 29 | } 30 | }).reduce(Q.when, Q()).then(function() { 31 | cb(err.length === 0 ? undefined : err, res); 32 | }, function(err) { 33 | cb(err); 34 | }).done(); 35 | } 36 | 37 | 38 | function runOnBrowser(url, browserConfig, opts) { 39 | browser = wd.promiseRemote(opts.selenium); 40 | log('Selenium is on %s', browser.noAuthConfigUrl.hostname); 41 | browser.on('status', function(data) { 42 | //seleniumLog(data); 43 | }); 44 | browser.on('command', function(meth, path, data) { 45 | if (data && typeof data === 'object') { 46 | var data = JSON.stringify(data); 47 | } 48 | seleniumLog(meth, (path || '').substr(0, 70), (data || '').substr(0, 70)); 49 | }); 50 | 51 | var metrics = new Metrics(opts.metrics); 52 | var actions = new Actions(opts.actions); 53 | 54 | return metrics.setup(opts).then(function() { 55 | return actions.setup(opts); 56 | }).then(function() { 57 | log('Stating browser with', JSON.stringify(browserConfig)); 58 | return browser.init(browserConfig); 59 | }).then(function() { 60 | log('Session is ' + browser.sessionID); 61 | log('Running Prescript'); 62 | return opts.preScript(browser); 63 | }).then(function() { 64 | if (url) { 65 | return browser.get(url); 66 | } 67 | }).then(function() { 68 | return metrics.start(browser, browserConfig); 69 | }).then(function() { 70 | return actions.perform(browser, browserConfig); 71 | }).then(function() { 72 | return metrics.teardown(browser, browserConfig); 73 | }).then(function() { 74 | return metrics.getResults(); 75 | }).fin(function() { 76 | if (!opts.debugBrowser) { 77 | return browser.quit().catch(function() { 78 | return Q(); 79 | }); 80 | } 81 | }); 82 | } 83 | 84 | module.exports = main; 85 | module.exports.actions = Actions.actions; 86 | module.exports.runner = require('./runner'); 87 | module.exports.docs = require('../docs'); -------------------------------------------------------------------------------- /lib/metrics/BaseMetrics.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | 3 | function BaseMetric(cfg) { 4 | cfg = cfg || {}; 5 | this.probes = cfg.probes || this.probes; 6 | if (!this.probes || !Array.isArray(this.probes)) { 7 | this.probes = []; 8 | } 9 | this.hrtime = process.hrtime(); 10 | this.__data = []; 11 | }; 12 | 13 | BaseMetric.prototype.getResults = function() { 14 | throw 'getResults not implemented for ' + this.id; 15 | }; 16 | 17 | BaseMetric.prototype.onError = function(err) { 18 | 19 | }; 20 | 21 | BaseMetric.prototype.onData = function(data) { 22 | this.hrtime = process.hrtime(this.hrtime); 23 | if (data) { 24 | data.__time = this.hrtime[0]; 25 | this.__data.push(data); 26 | } 27 | }; 28 | 29 | var difference = function(a, b) { 30 | if (typeof a !== typeof b) { 31 | return NaN; 32 | } 33 | if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) { 34 | return a.map(function(el, i) { 35 | return el - b[i]; 36 | }); 37 | } else if (typeof a === 'object') { 38 | var diff = {}; 39 | for (var key in a) { 40 | diff[key] = difference(a[key], b[key]); 41 | } 42 | return diff; 43 | } else { 44 | return a - b; 45 | } 46 | } 47 | 48 | BaseMetric.prototype.__getDeltas = function() { 49 | var deltas = []; 50 | if (this.__data.length === 1) { 51 | return this.__data[0]; 52 | } 53 | for (var i = 1; i < this.__data.length; i++) { 54 | var x = difference(this.__data[i], this.__data[i - 1]); 55 | deltas.push(x); 56 | } 57 | return (deltas.length === 1 ? deltas[0] : deltas); 58 | }; 59 | 60 | module.exports = BaseMetric; -------------------------------------------------------------------------------- /lib/metrics/ChromeTracingMetrics.js: -------------------------------------------------------------------------------- 1 | /* 2 | Based on 3 | https://chromium.googlesource.com/chromium/src/+/e2f820e34c43102785cfc9f38bde8b5a052938b8/tools/telemetry/telemetry/web_perf/metrics/smoothness.py 4 | */ 5 | 6 | var Q = require('q'), 7 | BaseMetrics = require('./BaseMetrics'), 8 | helpers = require('../helpers'), 9 | statistics = require('./util/statistics'), 10 | RenderingStats = require('./util/RenderingStats'), 11 | debug = require('debug')('bp:metrics:ChromeTracingMetrics'); 12 | 13 | function ChromeTracingMetrics() { 14 | BaseMetrics.apply(this, arguments); 15 | this.renderingStats = new RenderingStats(); 16 | } 17 | 18 | require('util').inherits(ChromeTracingMetrics, BaseMetrics); 19 | 20 | ChromeTracingMetrics.prototype.id = 'ChromeTracingMetrics'; 21 | ChromeTracingMetrics.prototype.probes = ['PerfLogProbe', 'AndroidTracingProbe']; 22 | 23 | var TRACE_CATEGORIES = ['benchmark']; 24 | var TraceCategoryRegEx = new RegExp('\\b(' + TRACE_CATEGORIES.join('|') + '|__metadata)\\b'); 25 | 26 | ChromeTracingMetrics.prototype.setup = function(cfg) { 27 | cfg.browsers = cfg.browsers.map(function(browser) { 28 | if (browser.browserName && browser.browserName === 'chrome') { 29 | helpers.extend(browser, { 30 | chromeOptions: { 31 | perfLoggingPrefs: {} 32 | } 33 | }); 34 | 35 | browser.chromeOptions.perfLoggingPrefs.traceCategories = [ 36 | browser.chromeOptions.perfLoggingPrefs.traceCategories || '', 37 | TRACE_CATEGORIES 38 | ].join(); 39 | } 40 | return browser; 41 | }); 42 | return Q(cfg); 43 | }; 44 | 45 | ChromeTracingMetrics.prototype.onData = function(data) { 46 | if (data.type === 'perfLog') { 47 | var msg = data.value; 48 | if (msg.method === 'Tracing.dataCollected' && TraceCategoryRegEx.test(msg.params.cat)) { 49 | this.renderingStats.addData(msg.params); 50 | } 51 | } else if (data.type === 'androidTracing') { 52 | this.renderingStats.addData(data.value); 53 | } 54 | }; 55 | 56 | ChromeTracingMetrics.prototype.getResults = function() { 57 | var results = {}; 58 | var frames = this.renderingStats.getFrames(); 59 | this._ComputeFrameTimeMetric(frames, results); 60 | this._ComputeFrameTimeDiscrepancy(frames, results); 61 | return results; 62 | }; 63 | 64 | ChromeTracingMetrics.prototype._HasEnoughFrames = function(timestamps) { 65 | if (timestamps.length < 2) { 66 | debug('Does not have enough frames for computing tracing'); 67 | } 68 | return timestamps.length >= 2; 69 | }; 70 | 71 | /* 72 | Returns Values for the frame time metrics. 73 | This includes the raw and mean frame times, as well as the percentage of frames that were hitting 60 fps. 74 | */ 75 | ChromeTracingMetrics.prototype._ComputeFrameTimeMetric = function(stats, res) { 76 | if (this._HasEnoughFrames(stats.frame_timestamps)) { 77 | 78 | var smooth_threshold = 17.0; 79 | var smooth_count = stats.frame_times.filter(function(t) { 80 | return t < smooth_threshold; 81 | }).length; 82 | 83 | res.mean_frame_time = statistics.ArithmeticMean(stats.frame_times); 84 | res.percentage_smooth = smooth_count / stats.frame_times.length * 100.0; 85 | res.frames_per_sec = 1000 / res.mean_frame_time; 86 | } 87 | }; 88 | 89 | // Returns a Value for the absolute discrepancy of frame time stamps 90 | ChromeTracingMetrics.prototype._ComputeFrameTimeDiscrepancy = function(stats, res) { 91 | if (this._HasEnoughFrames(stats.frame_timestamps)) { 92 | res.frame_time_discrepancy = statistics.TimestampsDiscrepancy(stats.frame_timestamps); 93 | } 94 | }; 95 | 96 | module.exports = ChromeTracingMetrics; -------------------------------------------------------------------------------- /lib/metrics/NetworkResources.js: -------------------------------------------------------------------------------- 1 | var BaseMetrics = require('./BaseMetrics'), 2 | helpers = require('../helpers'), 3 | StatData = require('./util/StatData'), 4 | Q = require('q'); 5 | 6 | function NetworkResources() { 7 | 8 | /** 9 | * "outputRawData" config option to decide whether or not Metric should return raw data 10 | * 11 | * "resultsBeforeStart" window.performance.getEntries() track all resources, even before browser-perf is started. 12 | * With this option, metric could return all ("true") results or only between Probe "start" and "teardown" ("false") 13 | * 14 | * @type {{outputRawData: boolean, resultsBeforeStart: boolean}} 15 | */ 16 | this.defaultOptions = { 17 | 'outputRawData' : false, 18 | 'resultsBeforeStart' : false 19 | }; 20 | 21 | // Initialize result statistics object 22 | this.stats = {}; 23 | 24 | // Check http://www.w3.org/TR/resource-timing/ for available types 25 | this.typesMapping = { 26 | 'subdocument' : 'iframe', 27 | 'iframe' : 'iframe', 28 | 'img' : 'image', 29 | 'link' : 'css', 30 | 'script' : 'js', 31 | 'css' : 'image', //mean resource is loaded from CSS file 32 | 'xmlhttprequest' : 'xhrrequest' 33 | }; 34 | 35 | BaseMetrics.apply(this, arguments); 36 | } 37 | require('util').inherits(NetworkResources, BaseMetrics); 38 | 39 | NetworkResources.prototype.id = 'NetworkResources'; 40 | NetworkResources.prototype.probes = ['NetworkResourcesProbe']; 41 | 42 | /** 43 | * Metrics setup 44 | * Extend default configuration options 45 | * 46 | * @param config 47 | * @returns {*} 48 | */ 49 | NetworkResources.prototype.setup = function (config) { 50 | var options = config.metricOptions[this.id] || {}; 51 | this.options = helpers.extend(this.defaultOptions, options); 52 | 53 | return Q(config); 54 | }; 55 | 56 | NetworkResources.prototype.onData = function(data) { 57 | this.resources = data; 58 | this.resources.forEach(function(element) { 59 | // Process only "resource" type results, so we do not add "marks" and "measures" into StatData 60 | if (element['entryType'] && element['entryType'] === 'resource') { 61 | this.addData(element); 62 | } 63 | }, this); 64 | }; 65 | 66 | /** 67 | * Calculate type based on resource properties and add it to stats 68 | * 69 | * @param row 70 | */ 71 | NetworkResources.prototype.addData = function(row) { 72 | 73 | var type = this.typesMapping[row['initiatorType']] || 'other'; 74 | var statsKey = 'Network' + type[0].toUpperCase() + type.slice(1); 75 | 76 | if (typeof this.stats[statsKey] === 'undefined') { 77 | this.stats[statsKey] = new StatData(); 78 | } 79 | 80 | this.stats[statsKey].add(row['duration']); 81 | }; 82 | 83 | /** 84 | * Calculate results for Networking Resources usage 85 | * 86 | * @returns {object} 87 | */ 88 | NetworkResources.prototype.getResults = function() { 89 | 90 | var result = {}; 91 | 92 | for (var key in this.stats) { 93 | var stats = this.stats[key].getStats(); 94 | if (stats.sum === 0) { 95 | result[key] = stats.count; 96 | } else { 97 | result[key] = stats.sum; 98 | result[key + '_avg'] = stats.mean; 99 | result[key + '_max'] = stats.max; 100 | result[key + '_count'] = stats.count; 101 | } 102 | } 103 | 104 | if (this.options.outputRawData) { 105 | result['NetworkRawData'] = this.resources || []; 106 | } 107 | 108 | return result; 109 | }; 110 | 111 | module.exports = NetworkResources; 112 | -------------------------------------------------------------------------------- /lib/metrics/NetworkTimings.js: -------------------------------------------------------------------------------- 1 | var BaseMetrics = require('./BaseMetrics'), 2 | helpers = require('../helpers'); 3 | 4 | function NetworkTimings() { 5 | BaseMetrics.apply(this, arguments); 6 | } 7 | require('util').inherits(NetworkTimings, BaseMetrics); 8 | 9 | NetworkTimings.prototype.id = 'NetworkTimings'; 10 | NetworkTimings.prototype.probes = ['NavTimingProbe']; 11 | 12 | NetworkTimings.prototype.onData = function(data) { 13 | this.timing = data; 14 | } 15 | 16 | NetworkTimings.prototype.getResults = function(cfg, browser) { 17 | 18 | // More useful representation of timing information 19 | // Credit : https://github.com/addyosmani/timing.js 20 | this.addMetric('loadTime', 'loadEventEnd', 'fetchStart'); 21 | this.addMetric('domReadyTime', 'domComplete', 'domInteractive'); 22 | this.addMetric('readyStart', 'fetchStart', 'navigationStart'); 23 | this.addMetric('redirectTime', 'redirectEnd', 'redirectStart'); 24 | this.addMetric('appcacheTime', 'domainLookupStart', 'fetchStart'); 25 | this.addMetric('unloadEventTime', 'unloadEventEnd', 'unloadEventStart'); 26 | this.addMetric('domainLookupTime', 'domainLookupEnd', 'domainLookupStart'); 27 | this.addMetric('connectTime', 'connectEnd', 'connectStart'); 28 | this.addMetric('requestTime', 'responseEnd', 'requestStart'); 29 | this.addMetric('initDomTreeTime', 'domInteractive', 'responseEnd'); 30 | this.addMetric('loadEventTime', 'loadEventEnd', 'loadEventStart'); 31 | 32 | return this.timing; 33 | } 34 | 35 | NetworkTimings.prototype.addMetric = function(prop, a, b) { 36 | if (typeof this.timing[a] === 'number' && typeof this.timing[b] === 'number') { 37 | this.timing[prop] = this.timing[a] - this.timing[b]; 38 | } 39 | } 40 | 41 | module.exports = NetworkTimings; -------------------------------------------------------------------------------- /lib/metrics/RafRenderingStats.js: -------------------------------------------------------------------------------- 1 | var BaseMetrics = require('./BaseMetrics'), 2 | debug = require('debug')('bp:metrics:RafRenderingStats'), 3 | helpers = require('../helpers'); 4 | 5 | function RafBenchmarkingRenderingStats() { 6 | BaseMetrics.apply(this, arguments); 7 | } 8 | require('util').inherits(RafBenchmarkingRenderingStats, BaseMetrics); 9 | 10 | RafBenchmarkingRenderingStats.prototype.id = 'RafBenchmarkingRenderingStats'; 11 | RafBenchmarkingRenderingStats.prototype.probes = ['RafBenchmarkingProbe']; 12 | 13 | RafBenchmarkingRenderingStats.prototype.getResults = function() { 14 | if (this.__data.length > 0) { 15 | var meanFrameTime = this.getMeanFrameTime_(this.__data[0]); 16 | return { 17 | numAnimationFrames: this.__data[0].length - 1, 18 | numFramesSentToScreen: this.__data[0].length - 1, 19 | droppedFrameCount: this.getDroppedFrameCount_(this.__data[0]), 20 | meanFrameTime_raf: meanFrameTime, 21 | framesPerSec_raf: 1000 / meanFrameTime 22 | }; 23 | } else { 24 | debug('Did not get enough data to calculate metrics'); 25 | return {}; 26 | } 27 | } 28 | 29 | RafBenchmarkingRenderingStats.prototype.getMeanFrameTime_ = function(frameTimes) { 30 | var num_frames_sent_to_screen = frameTimes.length; 31 | var mean_frame_time_seconds = (frameTimes[frameTimes.length - 1] - frameTimes[0]) / num_frames_sent_to_screen; 32 | return mean_frame_time_seconds; 33 | } 34 | 35 | RafBenchmarkingRenderingStats.prototype.getDroppedFrameCount_ = function(frameTimes) { 36 | var droppedFrameCount = 0; 37 | var droppedFrameThreshold = 1000 / 55; 38 | for (var i = 1; i < frameTimes.length; i++) { 39 | var frameTime = frameTimes[i] - frameTimes[i - 1]; 40 | if (frameTime > droppedFrameThreshold) 41 | droppedFrameCount += Math.floor(frameTime / droppedFrameThreshold); 42 | } 43 | return droppedFrameCount; 44 | }; 45 | 46 | module.exports = RafBenchmarkingRenderingStats; -------------------------------------------------------------------------------- /lib/metrics/SampleMetric.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | debug = require('debug')('bp:metrics:SampleMetrics'), 3 | BaseMetrics = require('./BaseMetrics'); 4 | 5 | function SampleMetrics(probes) { 6 | BaseMetrics.apply(this, [{ 7 | probes: probes 8 | }]); 9 | } 10 | require('util').inherits(SampleMetrics, BaseMetrics); 11 | 12 | SampleMetrics.prototype.id = 'SampleMetrics'; 13 | SampleMetrics.prototype.probes = ['SampleProbe']; 14 | 15 | SampleMetrics.prototype.setup = function() { 16 | debug('Setup Method called'); 17 | return Q.delay(1); 18 | } 19 | 20 | SampleMetrics.prototype.start = function() { 21 | debug('Start Method called'); 22 | return Q.delay(1); 23 | } 24 | 25 | SampleMetrics.prototype.teardown = function() { 26 | debug('Teardown Method called'); 27 | return Q.delay(1); 28 | } 29 | 30 | SampleMetrics.prototype.onData = function() { 31 | debug('onData Method called'); 32 | } 33 | 34 | SampleMetrics.prototype.onError = function() { 35 | debug('onError Method called'); 36 | } 37 | 38 | SampleMetrics.prototype.getResults = function() { 39 | debug('Get Results called'); 40 | return {}; 41 | } 42 | 43 | module.exports = SampleMetrics; -------------------------------------------------------------------------------- /lib/metrics/TimelineMetrics.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | helpers = require('./../helpers'), 3 | RuntimePerfMetrics = require('./util/RuntimePerfMetrics'), 4 | BaseMetrics = require('./BaseMetrics'), 5 | StatData = require('./util/StatData'), 6 | debug = require('debug')('bp:metrics:TimelineMetrics'); 7 | 8 | function TimelineMetrics() { 9 | this.timelineMetrics = {}; 10 | this.runtimePerfMetrics = new RuntimePerfMetrics(); 11 | this.eventStacks = {}; 12 | 13 | BaseMetrics.apply(this, arguments); 14 | } 15 | 16 | require('util').inherits(TimelineMetrics, BaseMetrics); 17 | 18 | TimelineMetrics.prototype.id = 'TimelineMetrics'; 19 | TimelineMetrics.prototype.probes = ['PerfLogProbe', 'AndroidTracingProbe']; 20 | 21 | var TRACE_CATEGORIES = ['blink.console', 'devtools.timeline', 'disabled-by-default-devtools.timeline', 'toplevel', 'disabled-by-default-devtools.timeline.frame']; 22 | var eventCategoryRegEx = new RegExp('\\b(' + TRACE_CATEGORIES.join('|') + '|__metadata)\\b'); 23 | 24 | TimelineMetrics.prototype.setup = function(cfg) { 25 | cfg.browsers = cfg.browsers.map(function(browser) { 26 | if (helpers.deepEquals(browser, 'browserName', 'chrome') || 27 | (helpers.deepEquals(browser, 'browserName', 'android') && !helpers.deepEquals(browser, 'chromeOptions.androidPackage', 'com.android.chrome'))) { 28 | // Only add this for Chrome OR Android-hybrid, not for Android-Chrome 29 | helpers.extend(browser, { 30 | chromeOptions: { 31 | perfLoggingPrefs: {} 32 | } 33 | }); 34 | browser.chromeOptions.perfLoggingPrefs.traceCategories = [ 35 | browser.chromeOptions.perfLoggingPrefs.traceCategories || '', 36 | TRACE_CATEGORIES 37 | ].join(); 38 | } 39 | return browser; 40 | }); 41 | return Q(cfg); 42 | }; 43 | 44 | 45 | TimelineMetrics.prototype.getResults = function() { 46 | var res = {}; 47 | 48 | for (var key in this.timelineMetrics) { 49 | var stats = this.timelineMetrics[key].getStats(); 50 | if (stats.sum === 0) { 51 | res[key] = stats.count; 52 | } else { 53 | res[key] = stats.sum; 54 | res[key + '_avg'] = stats.mean; 55 | res[key + '_max'] = stats.max; 56 | res[key + '_count'] = stats.count; 57 | } 58 | } 59 | 60 | helpers.extend(res, this.runtimePerfMetrics.getResults()); 61 | 62 | return this.addAggregates_(res); 63 | }; 64 | 65 | TimelineMetrics.prototype.addAggregates_ = function(res) { 66 | var metrics = { 67 | 'Styles': ['UpdateLayoutTree', 'RecalculateStyles', 'ParseAuthorStyleSheet'], 68 | 'Javascript': ['FunctionCall', 'GCEvent', 'MajorGC', 'MinorGC', 'EvaluateScript'] 69 | } 70 | for (var key in metrics) { 71 | res[key] = metrics[key].reduce(function(prev, cur, i) { 72 | return prev + (typeof res[cur] === 'number' ? res[cur] : 0); 73 | }, 0); 74 | } 75 | return res 76 | } 77 | 78 | // Data from Safari/Appium (old format) 79 | TimelineMetrics.prototype.processTimelineRecord_ = function(e) { 80 | this.addData_(e, 'timeline'); 81 | 82 | if (Array.isArray(e.children)) { 83 | e.children.forEach(this.processTimelineRecord_.bind(this)); 84 | } 85 | }; 86 | 87 | // Timeline format at https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit#heading=h.yr4qxyxotyw 88 | TimelineMetrics.prototype.processTracingRecord_ = function(e) { 89 | switch (e.ph) { 90 | case 'I': // Instant Event 91 | case 'X': // Duration Event 92 | var duration = e.dur || e.tdur || 0; 93 | this.addData_({ 94 | type: e.name, 95 | data: e.args ? e.args.data : {}, 96 | startTime: e.ts / 1000, 97 | endTime: (e.ts + duration) / 1000 98 | }, 'tracing'); 99 | break; 100 | case 'B': // Begin Event 101 | if (typeof this.eventStacks[e.tid] === 'undefined') { 102 | this.eventStacks[e.tid] = []; 103 | } 104 | this.eventStacks[e.tid].push(e); 105 | break; 106 | case 'E': // End Event 107 | if (typeof this.eventStacks[e.tid] === 'undefined' || this.eventStacks[e.tid].length === 0) { 108 | debug('Encountered an end event that did not have a start event', e); 109 | } else { 110 | var b = this.eventStacks[e.tid].pop(); 111 | if (b.name !== e.name) { 112 | debug('Start and end events dont have the same name', e, b); 113 | } 114 | this.addData_({ 115 | type: e.name, 116 | data: helpers.extend(e.args.endData, b.args.beginData), 117 | startTime: b.ts / 1000, 118 | endTime: e.ts / 1000 119 | }, 'tracing'); 120 | } 121 | break; 122 | } 123 | }; 124 | 125 | TimelineMetrics.prototype.addData_ = function(e, source) { 126 | if (typeof this.timelineMetrics[e.type] === 'undefined') { 127 | this.timelineMetrics[e.type] = new StatData(); 128 | } 129 | this.timelineMetrics[e.type].add(e.startTime && e.endTime ? e.endTime - e.startTime : 0); 130 | this.runtimePerfMetrics.processRecord(e, source); 131 | } 132 | 133 | TimelineMetrics.prototype.onData = function(data) { 134 | if (data.type === 'perfLog') { 135 | var msg = data.value; 136 | if (msg.method === 'Timeline.eventRecorded') { 137 | this.processTimelineRecord_(msg.params); 138 | } else if (msg.method === 'Tracing.dataCollected') { 139 | if (eventCategoryRegEx.test(msg.params.cat)) { 140 | this.processTracingRecord_(msg.params); 141 | } 142 | } 143 | } else if (data.type === 'androidTracing') { 144 | msg = data.value; 145 | if (eventCategoryRegEx.test(msg.cat)) { 146 | this.processTracingRecord_(msg); 147 | } 148 | } 149 | }; 150 | 151 | module.exports = TimelineMetrics; 152 | -------------------------------------------------------------------------------- /lib/metrics/index.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | debug = require('debug')('bp:metrics'), 3 | helpers = require('../helpers'), 4 | ProbeManager = require('../probes'); 5 | 6 | function Metrics(metrics) { 7 | debug('Initializing Metrics'); 8 | var me = this, 9 | probeManager = this.probeManager = new ProbeManager(); 10 | this.metrics = metrics.map(function(metric) { 11 | var res = null; 12 | if (typeof metric === 'string') { 13 | var fn = require('./' + metric); 14 | res = new fn(); 15 | } else if (typeof metric === 'object') { 16 | res = metric; 17 | } 18 | probeManager.addProbes(res); 19 | return res; 20 | }).filter(function(metric) { 21 | return (typeof metric === 'object') 22 | }); 23 | } 24 | 25 | Metrics.prototype.allMetrics = function(method, args) { 26 | return this.metrics.map(function(metric) { 27 | return function() { 28 | if (typeof metric[method] === 'function') { 29 | debug(metric.id, method, 'called'); 30 | return metric[method].apply(metric, args); 31 | } else { 32 | return Q(); 33 | } 34 | } 35 | }).reduce(Q.when, Q()); 36 | } 37 | 38 | Metrics.prototype.setup = function(cfg) { 39 | var me = this; 40 | return this.allMetrics('setup', [cfg]).then(function() { 41 | return me.probeManager.setup(cfg); 42 | }); 43 | }; 44 | 45 | Metrics.prototype.start = function(browsers, browserConfig) { 46 | var me = this; 47 | return this.allMetrics('start', [browsers]).then(function() { 48 | return me.probeManager.start(browsers, browserConfig); 49 | }); 50 | } 51 | Metrics.prototype.teardown = function(browsers, browserConfig) { 52 | var me = this; 53 | return this.probeManager.teardown(browsers, browserConfig).then(function() { 54 | return me.allMetrics('teardown', [browsers]); 55 | }); 56 | } 57 | 58 | Metrics.prototype.getResults = function() { 59 | debug('Getting Results'); 60 | var args = arguments; 61 | return Q.allSettled(this.metrics.map(function(metric) { 62 | if (typeof metric.getResults === 'function') { 63 | debug('Getting results from ', metric.id); 64 | return metric.getResults.apply(metric, args); 65 | } else { 66 | return Q(); 67 | } 68 | })).spread(function() { 69 | var res = {}; 70 | Array.prototype.slice.call(arguments, 0).forEach(function(val) { 71 | if (val.state === 'fulfilled') { 72 | helpers.extend(res, val.value); 73 | } 74 | }); 75 | return res; 76 | }, function(err) { 77 | return err; 78 | }) 79 | }; 80 | 81 | 82 | module.exports = Metrics; 83 | module.exports.builtIns = [ 84 | 'TimelineMetrics', 85 | 'ChromeTracingMetrics', 86 | 'RafRenderingStats', 87 | 'NetworkTimings', 88 | 'NetworkResources' 89 | ]; -------------------------------------------------------------------------------- /lib/metrics/util/RenderingStats.js: -------------------------------------------------------------------------------- 1 | /* 2 | based on 3 | https://chromium.googlesource.com/chromium/src/+/e2f820e34c43102785cfc9f38bde8b5a052938b8/tools/telemetry/telemetry/web_perf/metrics/rendering_stats.py 4 | Just picking up mean frame time, ignoring everything about InputLatency 5 | 6 | Just returns frame_times and frame_timestamps that are used to calculate the metrics in 7 | https://chromium.googlesource.com/chromium/src/+/e2f820e34c43102785cfc9f38bde8b5a052938b8/tools/telemetry/telemetry/web_perf/metrics/smoothness.py 8 | */ 9 | 10 | var debug = require('debug')('bp:metrics:RenderingStats'); 11 | 12 | function GetTimestampEventName(events, pid) { 13 | var eventName = null; 14 | for (var i = 0; i < events.length; i++) { 15 | var event = events[i]; 16 | if (event.pid === pid && event.name === 'BenchmarkInstrumentation::DisplayRenderingStats' || event.name === 'BenchmarkInstrumentation::MainThreadRenderingStats') { 17 | if (typeof event.args.data !== 'undefined' && event.args['data']['frame_count'] === 1) { 18 | if (eventName === event.name) { 19 | return event.name 20 | } 21 | eventName = event.name; 22 | } 23 | } 24 | } 25 | return 'BenchmarkInstrumentation::ImplThreadRenderingStats'; 26 | }; 27 | 28 | var RenderingStats = function() { 29 | this.events = []; 30 | }; 31 | 32 | // List of events useful for calculations 33 | var events = [ 34 | 'BenchmarkInstrumentation::ImplThreadRenderingStats', 35 | 'BenchmarkInstrumentation::DisplayRenderingStats', 36 | 'BenchmarkInstrumentation::MainThreadRenderingStats', 37 | 'process_labels' 38 | ] 39 | var eventNameRegex = new RegExp('(' + events.join('|') + ')'); 40 | 41 | // Only store the events that are useful for calculations 42 | RenderingStats.prototype.addData = function(data) { 43 | if (eventNameRegex.test(data.name)) { 44 | this.events.push(data); 45 | } 46 | }; 47 | 48 | RenderingStats.prototype.getFrames = function() { 49 | var events = this.events; 50 | this.events.sort(function(a, b) { 51 | return (a.ts >= b.ts ? 1 : -1); 52 | }); 53 | 54 | this.frame_timestamps = []; 55 | this.frame_times = []; 56 | this.approximated_pixel_percentages = []; 57 | 58 | this.processId = findRenderProcess(events); 59 | debug('Process ID for render process is ', this.processId); 60 | 61 | timestamp_event_name = GetTimestampEventName(events, this.processId); 62 | debug('Timestamp Event name is ', timestamp_event_name); 63 | 64 | this._InitFrameTimestampsFromTimeline(events, timestamp_event_name); 65 | this._InitImplThreadRenderingStatsFromTimeline(events); 66 | if (this.frame_timestamps < 0) { 67 | debug('No timestamps found for tracing data'); 68 | } 69 | return { 70 | frame_timestamps: this.frame_timestamps, 71 | frame_times: this.frame_times 72 | }; 73 | }; 74 | 75 | 76 | function findRenderProcess(events) { 77 | // TODO - Find a better way to identify the running process - 78 | // Will break if site opens multiple tabs 79 | // Also breaks if site does not have a title 80 | for (var i = 0; i < events.length; i++) { 81 | if (events[i].name === 'process_labels') { 82 | return events[i].pid; 83 | } 84 | } 85 | debug('Looked at all events, could not find a process_labels. Pid is undefined'); 86 | }; 87 | 88 | RenderingStats.prototype._GatherEvents = function(event_name, events, timeline_range) { 89 | var processId = this.processId; 90 | var me = this; 91 | return { 92 | forEach: function(cb) { 93 | for (var i = 0; i < events.length; i++) { 94 | var event = events[i]; 95 | if (event.pid === processId && event.name === event_name && typeof event.args !== 'undefined' && typeof event.args.data !== 'undefined') { 96 | cb.call(me, event); 97 | } 98 | } 99 | } 100 | }; 101 | }; 102 | 103 | RenderingStats.prototype._AddFrameTimestamp = function(event) { 104 | var frame_count = event.args['data']['frame_count']; 105 | if (frame_count > 1) { 106 | debug('trace contains multi-frame render stats'); 107 | } 108 | if (frame_count == 1) { 109 | this.frame_timestamps.push(event.ts / 1000); // event.start is not available, only event.ts is 110 | if (this.frame_timestamps.length >= 2) { 111 | this.frame_times.push(this.frame_timestamps[this.frame_timestamps.length - 1] - 112 | this.frame_timestamps[this.frame_timestamps.length - 2]); 113 | } 114 | } 115 | }; 116 | 117 | RenderingStats.prototype._InitFrameTimestampsFromTimeline = function(events, timestamp_event_name) { 118 | var events = this._GatherEvents(timestamp_event_name, events); 119 | events.forEach(function(event) { 120 | this._AddFrameTimestamp(event); 121 | }); 122 | }; 123 | 124 | RenderingStats.prototype._InitImplThreadRenderingStatsFromTimeline = function(events, timeline_range) { 125 | var event_name = 'BenchmarkInstrumentation::ImplThreadRenderingStats'; 126 | var events = this._GatherEvents(event_name, events, timeline_range); 127 | events.forEach(function(event) { 128 | var data = event.args['data']; 129 | if (typeof data['visible_content_area'] !== 'undefined') { 130 | this.approximated_pixel_percentages.push(data['approximated_visible_content_area'] / data['visible_content_area'] * 100.0); 131 | } else { 132 | this.approximated_pixel_percentages.push(0); 133 | } 134 | }); 135 | } 136 | 137 | module.exports = RenderingStats; -------------------------------------------------------------------------------- /lib/metrics/util/RuntimePerfMetrics.js: -------------------------------------------------------------------------------- 1 | // Test based on rules from http://calendar.perfplanet.com/2013/the-runtime-performance-checklist/ 2 | var StatData = require('./StatData'); 3 | 4 | function RuntimePerfMetrics() { 5 | this.paintArea = new StatData(); 6 | this.nodesPerLayout = new StatData(); 7 | this.DirtyNodesPerLayout = new StatData(); 8 | this.layers = {}; 9 | this.expensivePaints = 0; 10 | this.expensiveEventHandlers = 0; 11 | this.styles = 0; 12 | 13 | this.hasData = false; 14 | this.eventDispatchFn = null; // To check if FunctionCall follows EventDispatch events 15 | 16 | this.frames = []; 17 | } 18 | 19 | 20 | RuntimePerfMetrics.prototype.id = 'RuntimePerfMetrics'; 21 | 22 | RuntimePerfMetrics.prototype.processRecord = function(record, source) { 23 | // If eventDispatch is ticking and this is not a FunctionCall, restart the ticks 24 | if (source === 'tracing' && this.eventDispatchFn !== null && record.type !== 'FunctionCall') { 25 | if (this.eventDispatchFn > 16) { 26 | this.expensiveEventHandlers++; 27 | } 28 | this.eventDispatchFn = null; 29 | } 30 | 31 | if (typeof rules[record.type] === 'function') { 32 | this.hasData = true; 33 | rules[record.type].apply(this, [record, source]); 34 | } 35 | }; 36 | 37 | var rules = { 38 | DrawFrame: function(event) { 39 | this.frames.push(event.startTime); 40 | }, 41 | EventDispatch: function(event) { 42 | var fnCallTime = 0; 43 | this.eventDispatchFn = 0; // start ticking eventDispatchFn 44 | if (Array.isArray(event.children)) { 45 | event.children.forEach(function(event) { 46 | if (event.type === 'FunctionCall') { 47 | fnCallTime += event.endTime - event.startTime; 48 | } 49 | }); 50 | } 51 | if (fnCallTime > 16) { 52 | this.expensiveEventHandlers++; 53 | } 54 | }, 55 | FunctionCall: function(event, source) { 56 | if (source === 'tracing' && this.eventDispatchFn !== null) { 57 | // Looks like a function call after eventDispatch since eventDispatchFn is ticking 58 | this.eventDispatchFn += (event.endTime - event.startTime); 59 | } 60 | }, 61 | Layout: function(event) { 62 | this.nodesPerLayout.add(event.data.totalObjects); 63 | this.DirtyNodesPerLayout.add(event.data.dirtyObjects); 64 | }, 65 | Paint: function(event) { 66 | if (event.endTime - event.startTime > 16) { 67 | // This paint took more than 1/60 ms or 16 ms 68 | this.expensivePaints++; 69 | } 70 | this.layers[event.data.layerId] = true; 71 | var clip = event.data.clip; 72 | this.paintArea.add(Math.abs((clip[0] - clip[3]) * (clip[1] - clip[7]))); 73 | } 74 | } 75 | 76 | function getFrameRate(frames) { 77 | frames.sort(); 78 | var range = 0.20; 79 | var start = parseInt(frames.length * range, 10); 80 | var end = parseInt(frames.length * (1 - range), 10); 81 | return (1000 * (end - start)) / (frames[end] - frames[start]); 82 | } 83 | 84 | RuntimePerfMetrics.prototype.getResults = function() { 85 | if (this.eventDispatchFn !== null) { 86 | // Last event on the chain is a Function call, so draining it 87 | this.processRecord({}, 'tracing'); 88 | } 89 | 90 | var paintAreaStat = this.paintArea.getStats(); 91 | if (this.hasData) { 92 | return { 93 | 'Layers': Object.keys(this.layers).length, 94 | 'PaintedArea_total': paintAreaStat.sum, 95 | 'PaintedArea_avg': paintAreaStat.mean, 96 | 'NodePerLayout_avg': this.nodesPerLayout.getStats().mean, 97 | 'ExpensivePaints': this.expensivePaints, 98 | 'ExpensiveEventHandlers': this.expensiveEventHandlers, 99 | 'framesPerSec (devtools)': getFrameRate(this.frames) 100 | } 101 | } else { 102 | return {}; 103 | } 104 | } 105 | 106 | module.exports = RuntimePerfMetrics; 107 | -------------------------------------------------------------------------------- /lib/metrics/util/StatData.js: -------------------------------------------------------------------------------- 1 | function StatData() { 2 | this.count = this.sum = this.sumsq = 0; 3 | this.max = this.min = null; 4 | } 5 | 6 | StatData.prototype.add = function(val) { 7 | if (typeof val === 'number') { 8 | this.count++; 9 | this.sum += val; 10 | this.sumsq += (val * val); 11 | if (this.max === null || val > this.max) { 12 | this.max = val; 13 | } 14 | if (this.min === null || val < this.min) { 15 | this.min = val; 16 | } 17 | } 18 | }; 19 | 20 | StatData.prototype.getStats = function() { 21 | return { 22 | mean: this.count === 0 ? 0 : this.sum / this.count, 23 | max: this.max, 24 | min: this.min, 25 | sum: this.sum, 26 | count: this.count 27 | } 28 | } 29 | 30 | module.exports = StatData; -------------------------------------------------------------------------------- /lib/metrics/util/statistics.js: -------------------------------------------------------------------------------- 1 | /* 2 | Based on 3 | https://chromium.googlesource.com/chromium/src/+/6df438ed2adaf24fa2f4a92d4f3863825247b910/tools/telemetry/telemetry/util/statistics.py 4 | */ 5 | 6 | /* 7 | Returns the float value of a number or the sum of a list. 8 | */ 9 | function Total(data) { 10 | if (typeof data === 'number') { 11 | return data; 12 | } else if (Array.isArray(data)) { 13 | return data.reduce(function(previousValue, currentValue, index, array) { 14 | return previousValue + currentValue; 15 | }); 16 | } 17 | } 18 | 19 | /* 20 | Returns the quotient, or zero if the denominator is zero 21 | */ 22 | function DivideIfPossibleOrZero(numerator, denominator) { 23 | return denominator ? numerator / denominator : 0; 24 | } 25 | 26 | /* 27 | Calculates arithmetic mean. 28 | 29 | Args: 30 | data: A list of samples. 31 | 32 | Returns: 33 | The arithmetic mean value, or 0 if the list is empty. 34 | */ 35 | function ArithmeticMean(data) { 36 | numerator_total = Total(data); 37 | denominator_total = Total(data.length); 38 | return DivideIfPossibleOrZero(numerator_total, denominator_total); 39 | } 40 | 41 | /* 42 | Sorts the samples, and map them linearly to the range [0,1]. 43 | 44 | They're mapped such that for the N samples, the first sample is 0.5/N and the 45 | last sample is (N-0.5)/N. 46 | 47 | Background: The discrepancy of the sample set i/(N-1); i=0, ..., N-1 is 2/N, 48 | twice the discrepancy of the sample set (i+1/2)/N; i=0, ..., N-1. In our case 49 | we don't want to distinguish between these two cases, as our original domain 50 | is not bounded (it is for Monte Carlo integration, where discrepancy was 51 | first used). 52 | */ 53 | function NormalizeSamples(samples) { 54 | if (!samples) { 55 | return 1; 56 | } 57 | samples = samples.sort(); 58 | var low = Math.min.apply(null, samples); 59 | var high = Math.max.apply(null, samples); 60 | var new_low = 0.5 / samples.length; 61 | var new_high = (samples.length - 0.5) / samples.length; 62 | if (high - low == 0.0) { 63 | return { 64 | samples: samples.map(function(s) { 65 | return 0.5; 66 | }), 67 | scale: 1 68 | } 69 | } 70 | var scale = (new_high - new_low) / (high - low); 71 | for (var i = 0; i < samples.length; i++) { 72 | samples[i] = (samples[i] - low) * scale + new_low 73 | } 74 | return { 75 | samples: samples, 76 | scale: scale 77 | } 78 | } 79 | 80 | /* 81 | Clamp a value between some low and high value 82 | */ 83 | function Clamp(value, low, high) { 84 | low = typeof low === 'undefined' ? 0 : low; 85 | high = typeof high === 'undefined' ? 0 : high; 86 | console.log(value, low, high) 87 | return Math.min(Math.max(value, low), high); 88 | } 89 | 90 | /* 91 | Computes the discrepancy of a set of 1D samples from the interval [0,1]. 92 | 93 | The samples must be sorted. We define the discrepancy of an empty set 94 | of samples to be zero. 95 | 96 | http://en.wikipedia.org/wiki/Low-discrepancy_sequence 97 | http://mathworld.wolfram.com/Discrepancy.html 98 | */ 99 | 100 | function Discrepancy(samples, location_count) { 101 | if (!samples) { 102 | return 0; 103 | } 104 | 105 | var max_local_discrepancy = 0; 106 | var inv_sample_count = 1.0 / samples.length; 107 | var locations = [], 108 | count_less = [], 109 | count_less_equal = []; 110 | 111 | if (location_count) { 112 | //Generate list of equally spaced locations. 113 | var sample_index = 0; 114 | for (var i = 0; i < location_count; i++) { 115 | var location = i / location_count - 1; 116 | locations.push(location); 117 | while (sample_index < samples.length && samples[sample_index] < location) 118 | sample_index += 1; 119 | count_less.push(sample_index); 120 | while (sample_index < samples.length && samples[sample_index] <= location) 121 | sample_index += 1; 122 | count_less_equal.push(sample_index); 123 | } 124 | } else { 125 | if (samples[0] > 0.0) { 126 | locations.push(0.0); 127 | count_less.push(0); 128 | count_less_equal.push(0); 129 | } 130 | for (var i = 0; i < samples.length; i++) { 131 | locations.push(samples[i]); 132 | count_less.push(i); 133 | count_less_equal.push(i + 1); 134 | } 135 | if (samples[-1] < 1.0) { 136 | locations.push(1.0); 137 | count_less.push(samples.length); 138 | count_less_equal.push(samples.length); 139 | } 140 | } 141 | 142 | // Iterate over the intervals defined by any pair of locations. 143 | for (var i = 0; i < locations.length; i++) { 144 | for (var j = i + 1; j < locations.length; j++) { 145 | // # Length of interval 146 | var length = locations[j] - locations[i]; 147 | 148 | // Local discrepancy for closed interval 149 | var count_closed = count_less_equal[j] - count_less[i]; 150 | var local_discrepancy_closed = Math.abs(count_closed * inv_sample_count - length); 151 | var max_local_discrepancy = Math.max(local_discrepancy_closed, max_local_discrepancy); 152 | 153 | // Local discrepancy for open interval 154 | var count_open = count_less[j] - count_less_equal[i]; 155 | var local_discrepancy_open = Math.abs(count_open * inv_sample_count - length); 156 | var max_local_discrepancy = Math.max(local_discrepancy_open, max_local_discrepancy); 157 | } 158 | } 159 | return max_local_discrepancy; 160 | } 161 | 162 | /* 163 | A discrepancy based metric for measuring timestamp jank. 164 | 165 | TimestampsDiscrepancy quantifies the largest area of jank observed in a series 166 | of timestamps. Note that this is different from metrics based on the 167 | max_time_interval. For example, the time stamp series A = [0,1,2,3,5,6] and 168 | B = [0,1,2,3,5,7] have the same max_time_interval = 2, but 169 | Discrepancy(B) > Discrepancy(A). 170 | 171 | Two variants of discrepancy can be computed: 172 | 173 | Relative discrepancy is following the original definition of 174 | discrepancy. It characterized the largest area of jank, relative to the 175 | duration of the entire time stamp series. We normalize the raw results, 176 | because the best case discrepancy for a set of N samples is 1/N (for 177 | equally spaced samples), and we want our metric to report 0.0 in that 178 | case. 179 | 180 | Absolute discrepancy also characterizes the largest area of jank, but its 181 | value wouldn't change (except for imprecisions due to a low 182 | |interval_multiplier|) if additional 'good' intervals were added to an 183 | exisiting list of time stamps. Its range is [0,inf] and the unit is 184 | milliseconds. 185 | 186 | The time stamp series C = [0,2,3,4] and D = [0,2,3,4,5] have the same 187 | absolute discrepancy, but D has lower relative discrepancy than C. 188 | 189 | |timestamps| may be a list of lists S = [S_1, S_2, ..., S_N], where each 190 | S_i is a time stamp series. In that case, the discrepancy D(S) is: 191 | D(S) = max(D(S_1), D(S_2), ..., D(S_N)) 192 | */ 193 | function TimestampsDiscrepancy(timestamps, absolute, location_count) { 194 | absolute = typeof absolute === 'undefined' ? true : absolute; 195 | location_count = typeof location_count === 'undefined' ? null : location_count; 196 | 197 | if (!timestamps) { 198 | return 0; 199 | } 200 | 201 | var normal = NormalizeSamples(timestamps); 202 | var samples = normal.samples, 203 | sample_scale = normal.scale; 204 | var discrepancy = Discrepancy(samples, location_count); 205 | var inv_sample_count = 1.0 / samples.length; 206 | 207 | if (absolute) 208 | discrepancy /= sample_scale; 209 | else 210 | discrepancy = Clamp((discrepancy - inv_sample_count) / (1.0 - inv_sample_count)); 211 | return discrepancy; 212 | } 213 | 214 | module.exports = { 215 | ArithmeticMean: ArithmeticMean, 216 | TimestampsDiscrepancy: TimestampsDiscrepancy 217 | }; -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | var sanitizers = [ 2 | // debugBrowser 3 | function(opts) { 4 | if (typeof opts.debugBrowser === 'undefined') { 5 | opts.debugBrowser = false; 6 | } 7 | return opts; 8 | }, 9 | 10 | // configFile 11 | function(opts) { 12 | var fs = require('fs'); 13 | if (opts.configFile) { 14 | try { 15 | config = JSON.parse(fs.readFileSync(opts.configFile, 'utf-8')); 16 | if (config) { 17 | for (var key in opts) { 18 | if (typeof opts[key] !== 'undefined') { 19 | config[key] = opts[key]; 20 | } 21 | } 22 | } 23 | opts = config; 24 | } catch (e) { 25 | throw new Error('Could not read or parse configuration file - ' + e); 26 | } 27 | delete opts.configFile; 28 | } 29 | return opts; 30 | }, 31 | 32 | // selenium 33 | function(opts) { 34 | opts.selenium = opts.selenium || 'http://localhost:4444/wd/hub'; 35 | if (typeof opts.selenium === 'string') { 36 | if (opts.selenium === 'ondemand.saucelabs.com' || opts.selenium === 'hub.browserstack.com') { 37 | opts.selenium = opts.selenium + '/wd/hub'; 38 | } 39 | 40 | if (!opts.selenium.match(/^http(s)?:\/\//)) { 41 | opts.selenium = 'http://' + opts.selenium 42 | } 43 | var url = require('url'); 44 | opts.selenium = url.parse(opts.selenium); 45 | } 46 | 47 | if (typeof opts.username !== 'undefined') { 48 | opts.selenium.user = opts.username; 49 | } 50 | 51 | if (typeof opts.accesskey !== 'undefined') { 52 | opts.selenium.pwd = opts.accesskey; 53 | } 54 | if (typeof opts.selenium.port !== 'number') { 55 | opts.selenium.port = parseInt(opts.selenium.port, 10); 56 | } 57 | if (isNaN(opts.selenium.port)) { 58 | opts.selenium.port = null; 59 | } 60 | 61 | return opts; 62 | }, 63 | 64 | // browsers 65 | function(opts) { 66 | var browserConfig = {}; // Defaults for browsers to be added here 67 | 68 | var passedBrowsers = opts.browsers || opts.browser; 69 | if (typeof passedBrowsers === 'undefined') { 70 | passedBrowsers = [{ 71 | browserName: 'chrome', 72 | version: 35 73 | }]; 74 | } else if (typeof passedBrowsers === 'string') { 75 | passedBrowsers = passedBrowsers.split(/[,;]/); 76 | } 77 | 78 | var result = []; 79 | passedBrowsers.forEach(function(passedBrowser) { 80 | passedBrowser = (typeof passedBrowser === 'string' ? { 81 | 'browserName': passedBrowser 82 | } : passedBrowser); 83 | 84 | if (typeof browserConfig[passedBrowser.browserName] !== 'undefined') { 85 | var matchingBrowserCfg = browserConfig[passedBrowser.browserName] || browserConfig[passedBrowser.browserName] 86 | for (var key in matchingBrowserCfg) { 87 | if (typeof passedBrowser[key] === 'undefined') { 88 | passedBrowser[key] = matchingBrowserCfg[key]; 89 | } 90 | } 91 | } 92 | // Add activity name for android browsers 93 | if (passedBrowser.browserName && passedBrowser.browserName.match(/android/gi)) { 94 | passedBrowser.chromeOptions = passedBrowser.chromeOptions || {}; 95 | if (typeof passedBrowser.chromeOptions.androidPackage === 'undefined') { 96 | passedBrowser.chromeOptions.androidPackage = 'com.android.chrome'; 97 | } 98 | } 99 | 100 | // Setting platform if it does not exist 101 | if (typeof passedBrowser.platform === 'undefined' || typeof passedBrowser.platformName === 'undefined') { 102 | if (opts.selenium.hostname.match(/ondemand.saucelabs.com/) || opts.selenium.hostname.match(/hub.browserstack.com/)) { 103 | passedBrowser.platform = 'WINDOWS'; 104 | } 105 | } 106 | 107 | result.push(passedBrowser); 108 | }); 109 | opts.browsers = result; 110 | return opts; 111 | }, 112 | 113 | // username and accesskey or password 114 | function(opts) { 115 | opts.accesskey = opts.accesskey || opts.password; 116 | delete opts.password; 117 | if (opts.selenium.hostname.match(/ondemand.saucelabs.com/)) { 118 | opts.SAUCE_USERNAME = opts.SAUCE_USERNAME || opts.username; 119 | opts.SAUCE_ACCESSKEY = opts.SAUCE_ACCESSKEY || opts.accesskey; 120 | if (typeof opts.SAUCE_USERNAME !== 'undefined' && typeof opts.SAUCE_ACCESSKEY !== 'undefined') { 121 | opts.selenium.auth = opts.SAUCE_USERNAME + ':' + opts.SAUCE_ACCESSKEY; 122 | delete opts.SAUCE_ACCESSKEY; 123 | delete opts.SAUCE_USERNAME; 124 | } 125 | } else if (opts.selenium.hostname.match(/hub.browserstack.com/)) { 126 | opts.BROWSERSTACK_USERNAME = opts.BROWSERSTACK_USERNAME || opts.username; 127 | opts.BROWSERSTACK_KEY = opts.BROWSERSTACK_KEY || opts.accesskey; 128 | if (typeof opts.BROWSERSTACK_USERNAME !== 'undefined') { 129 | opts.browsers.forEach(function(browser) { 130 | browser['browserstack.user'] = opts.BROWSERSTACK_USERNAME; 131 | browser['browserstack.key'] = opts.BROWSERSTACK_KEY; 132 | }); 133 | delete opts.BROWSERSTACK_USERNAME; 134 | delete opts.BROWSERSTACK_KEY; 135 | delete opts.selenium.user; 136 | delete opts.selenium.pwd; 137 | } 138 | } 139 | 140 | delete opts.username; 141 | delete opts.password; 142 | return opts; 143 | }, 144 | 145 | // preScript, preScriptFile 146 | function(opts) { 147 | if (opts.preScriptFile) { 148 | var path = require('path'); 149 | opts.preScript = require(path.resolve(opts.preScriptFile)); 150 | delete opts.preScriptFile; 151 | } 152 | opts.preScript = opts.preScript || function(browser) { 153 | return; 154 | }; 155 | return opts; 156 | }, 157 | 158 | // actions 159 | function(opts) { 160 | opts.actions = opts.actions || 'scroll'; 161 | if (typeof opts.actions === 'string') { 162 | opts.actions = opts.actions.split(/[,;]/); 163 | } else if (typeof opts.actions === 'function') { 164 | opts.actions = [opts.actions]; 165 | } 166 | return opts; 167 | }, 168 | 169 | // metrics 170 | function(opts) { 171 | opts.metrics = opts.metrics || require('./metrics').builtIns; 172 | if (typeof opts.metrics === 'string') { 173 | opts.metrics = opts.metrics.split(/[,;]/); 174 | } else if (typeof opts.metrics === 'function') { 175 | opts.metrics = [opts.metrics]; 176 | } 177 | return opts; 178 | }, 179 | 180 | /** 181 | * Metric options 182 | * 183 | * @example opts.metricOptions = { "METRIC_ID" : { option1 : value1, ... , optionN : valueN } } 184 | * @param {object} opts 185 | */ 186 | function (opts) { 187 | opts.metricOptions = opts.metricOptions || {}; 188 | return opts; 189 | } 190 | ]; 191 | 192 | 193 | module.exports = { 194 | scrub: function(cfg) { 195 | cfg = cfg || {}; 196 | sanitizers.forEach(function(sanitizer, i) { 197 | cfg = sanitizer(cfg); 198 | }); 199 | return cfg; 200 | }, 201 | sanitizers: sanitizers 202 | }; -------------------------------------------------------------------------------- /lib/probes/AndroidTracingProbe.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | fs = require('fs'), 3 | util = require('util'), 4 | debug = require('debug')('bp:probes:AndroidTracingProbe'), 5 | events = require('events'), 6 | helpers = require('../helpers'), 7 | childProcess = require('child_process'), 8 | byline = require('byline'); 9 | 10 | function AndroidTracingProbe(id) { 11 | if (id) { 12 | this.id = id; 13 | } 14 | debug('Initialize'); 15 | events.EventEmitter.call(this); 16 | } 17 | 18 | util.inherits(AndroidTracingProbe, events.EventEmitter); 19 | 20 | AndroidTracingProbe.prototype.id = 'AndroidTracingProbe'; 21 | 22 | AndroidTracingProbe.prototype.isEnabled = function(browser) { 23 | // TRUE for Android Chrome browser - Activity Name is always set for Android-Chrome browser 24 | if (helpers.deepEquals(browser, 'browserName', 'android') && helpers.deepEquals(browser, 'chromeOptions.androidPackage', 'com.android.chrome')) { 25 | return true; 26 | } 27 | return false; 28 | } 29 | 30 | AndroidTracingProbe.prototype.setup = function(cfg) { 31 | var me = this; 32 | var enabled = false; 33 | this.debugBrowser = cfg.debugBrowser; 34 | cfg.browsers.forEach(function(browser) { 35 | enabled = me.isEnabled(browser); 36 | }); 37 | if (enabled) { 38 | debug('Setting up android tracing'); 39 | return this.run_('adb server start').fin(function() { 40 | return me.run_('adb logcat -c'); 41 | }); 42 | } 43 | }; 44 | 45 | var lifeCycle = function(methodName) { 46 | return function(browser, browserConfig) { 47 | var me = this; 48 | if (me.isEnabled(browserConfig)) { 49 | return me[methodName](browser); 50 | } 51 | }; 52 | } 53 | 54 | AndroidTracingProbe.prototype.start = lifeCycle('startRecordingTrace_'); 55 | AndroidTracingProbe.prototype.teardown = lifeCycle('stopRecordingTrace_'); 56 | 57 | AndroidTracingProbe.prototype.startRecordingTrace_ = function(browser) { 58 | var me = this, 59 | query = 'Logging performance trace to file'; 60 | 61 | debug('Starting android tracing'); 62 | return me.run_('adb shell pm grant com.android.chrome android.permission.WRITE_EXTERNAL_STORAGE').then(function(){ 63 | me.run_('adb shell pm grant com.android.chrome android.permission.READ_EXTERNAL_STORAGE') 64 | }).then(function(){ 65 | return me.run_('adb shell \'am broadcast -a com.android.chrome.GPU_PROFILER_START -e continuous "" -e categories "benchmark,disabled-by-default-devtools.timeline,toplevel,disabled-by-default-devtools.timeline.frame"\'') 66 | }).then(function() { 67 | return me.waitForLogCat_(query); 68 | }).then(function(line) { 69 | debug(line); 70 | }); 71 | }; 72 | 73 | AndroidTracingProbe.prototype.stopRecordingTrace_ = function(browser) { 74 | var me = this, 75 | query = 'Profiler finished. Results are in '; 76 | 77 | debug('Tearing down android tracing'); 78 | 79 | return this.run_('adb shell \'am broadcast -a com.android.chrome.GPU_PROFILER_STOP\'').then(function() { 80 | return me.waitForLogCat_(query) 81 | }).then(function(line) { 82 | try { 83 | return me.pullLogs_(line.split(query)[1].slice(0, -1)); 84 | } catch (e) { 85 | debug(e); 86 | return Q(); 87 | } 88 | }).then(function(dataStream) { 89 | if (dataStream) { 90 | // Format is {traceEvents: [{pid:...,cat:...}]} 91 | dataStream.forEach(function(data) { 92 | me.emit('data', { 93 | type: 'androidTracing', 94 | value: data 95 | }); 96 | }); 97 | } 98 | debug('Read all data'); 99 | }); 100 | }; 101 | 102 | AndroidTracingProbe.prototype.run_ = function(commandString) { 103 | debug('$ ' + commandString); 104 | var deferred = Q.defer(); 105 | var process = childProcess.exec(commandString, { 106 | timeout: 1000 * 60 * 2 107 | }, function() { 108 | deferred.resolve(); 109 | }); 110 | 111 | return deferred.promise; 112 | }; 113 | 114 | AndroidTracingProbe.prototype.waitForLogCat_ = function(query, timeout) { 115 | var deferred = Q.defer(), 116 | line = '', 117 | commandString = "adb logcat", 118 | search = new RegExp(query, "i"); 119 | 120 | timeout = timeout || 1000 * 60 * 2; 121 | var command = commandString.split(/\s/); 122 | var logcat = childProcess.spawn(command[0], command.slice(1)); 123 | stream = byline.createStream(logcat.stdout); 124 | 125 | var timerHandle = setTimeout(function() { 126 | done(false); 127 | }, timeout); 128 | 129 | stream.on('data', function(data) { 130 | var line = data.toString(); 131 | //debug('> ', line); 132 | if (line.match(search)) { 133 | done(line); 134 | } 135 | }); 136 | 137 | var completed = false; 138 | 139 | function done(arg) { 140 | if (timerHandle) { 141 | clearTimeout(timerHandle); 142 | } 143 | if (!completed) { 144 | completed = true; 145 | if (arg) { 146 | deferred.resolve(arg); 147 | } else { 148 | deferred.reject(); 149 | } 150 | } 151 | logcat.kill(); 152 | } 153 | 154 | return deferred.promise; 155 | }; 156 | 157 | AndroidTracingProbe.prototype.pullLogs_ = function(filename) { 158 | var me = this, 159 | path = require('path'), 160 | file = path.basename(filename); 161 | 162 | return me.run_(['adb pull', filename.replace('/storage/emulated/0/', '/sdcard/'), file].join(' ')).then(function() { 163 | // TODO Convert this to streams 164 | var trace = JSON.parse(fs.readFileSync(file)); 165 | if (!me.debugBrowser) { 166 | fs.unlinkSync(file); 167 | } 168 | return trace.traceEvents; 169 | }); 170 | }; 171 | 172 | module.exports = AndroidTracingProbe; -------------------------------------------------------------------------------- /lib/probes/NavTimingProbe.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | util = require('util'), 3 | wd = require('wd'), 4 | events = require('events'), 5 | helpers = require('../helpers'); 6 | 7 | function NavTimingProbe() { 8 | events.EventEmitter.call(this); 9 | } 10 | 11 | util.inherits(NavTimingProbe, events.EventEmitter); 12 | 13 | NavTimingProbe.prototype.id = 'NavTimingProbe'; 14 | 15 | NavTimingProbe.prototype.teardown = function(browser) { 16 | var code = function() { 17 | var requestAnimationFrame = (function() { 18 | return window.requestAnimationFrame || 19 | window.webkitRequestAnimationFrame || 20 | window.mozRequestAnimationFrame || 21 | window.oRequestAnimationFrame || 22 | window.msRequestAnimationFrame || 23 | function(callback) { 24 | window.setTimeout(callback, 1000 / 60); 25 | }; 26 | })().bind(window); 27 | 28 | requestAnimationFrame(function() { 29 | var result = {}; 30 | var performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance; 31 | if (typeof performance !== 'undefined') { 32 | var data = performance.timing; 33 | for (var key in data) { 34 | if (typeof data[key] === 'number') // Firefox spits out a toJSON function also 35 | result[key] = data[key]; 36 | } 37 | if (window.chrome && window.chrome.loadTimes) { // Chrome 38 | result.firstPaint = (window.chrome.loadTimes().firstPaintTime - window.chrome.loadTimes().startLoadTime) * 1000; 39 | } else if (typeof window.performance.timing.msFirstPaint === 'number') { // IE 40 | result.firstPaint = data.msFirstPaint - data.navigationStart; 41 | } 42 | } 43 | window.__navTimings = result; 44 | }); 45 | }; 46 | 47 | var me = this; 48 | return browser.execute(helpers.fnCall(code)).then(function() { 49 | return browser.waitFor({ 50 | asserter: wd.asserters.jsCondition('(typeof window.__navTimings !== "undefined")', false), 51 | timeout: 1000 * 60 * 10, 52 | pollFreq: 1000 53 | }); 54 | }).then(function(res) { 55 | return browser.eval('window.__navTimings'); 56 | }).then(function(res) { 57 | me.emit('data', res); 58 | }); 59 | }; 60 | module.exports = NavTimingProbe; -------------------------------------------------------------------------------- /lib/probes/NetworkResourcesProbe.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | util = require('util'), 3 | wd = require('wd'), 4 | events = require('events'), 5 | helpers = require('../helpers'); 6 | 7 | /** 8 | * Network resources timing Probe 9 | * 10 | * @see http://www.w3.org/TR/resource-timing/ 11 | * @constructor 12 | */ 13 | function NetworkResourcesProbe() { 14 | 15 | /** 16 | * "outputRawData" config option to decide whether or not Metric should return raw data 17 | * 18 | * "resultsBeforeStart" window.performance.getEntries() track all resources, even before browser-perf is started. 19 | * With this option, metric could return all ("true") results or only between Probe "start" and "teardown" ("false") 20 | * 21 | * @type {{outputRawData: boolean, resultsBeforeStart: boolean}} 22 | */ 23 | this.defaultOptions = { 24 | 'outputRawData' : false, 25 | 'resultsBeforeStart' : false 26 | }; 27 | 28 | this.lastResourceTime = 0; 29 | 30 | events.EventEmitter.call(this); 31 | } 32 | 33 | util.inherits(NetworkResourcesProbe, events.EventEmitter); 34 | 35 | NetworkResourcesProbe.prototype.id = 'NetworkResourcesProbe'; 36 | 37 | /** 38 | * Client-side script that need to be executed 39 | * inside browser to get Network Resources statistics 40 | * 41 | * @private 42 | */ 43 | NetworkResourcesProbe.prototype._clientGetData = function() { 44 | window.__networkResources = (window.performance && typeof window.performance.getEntries == 'function') 45 | ? window.performance.getEntries() 46 | : []; 47 | }; 48 | 49 | NetworkResourcesProbe.prototype.setup = function(config) { 50 | var options = config.metricOptions['NetworkResources'] || {}; 51 | this.options = helpers.extend(this.defaultOptions, options); 52 | }; 53 | 54 | /** 55 | * Called on metrics.stats 56 | * 57 | * @param browser 58 | */ 59 | NetworkResourcesProbe.prototype.start = function(browser) { 60 | var me = this; 61 | 62 | // Execute window.performance.getEntries() to get resources 63 | // that were before fetched before browser-perf metrics started 64 | // and calculate last loaded resource 65 | browser.execute(helpers.fnCall(this._clientGetData)) 66 | .then(function() { 67 | return browser.eval('window.__networkResources') 68 | }) 69 | .then(function(res) { 70 | me.beforeData = res; 71 | 72 | // If "resultsBeforeStart" option is false and there are any information 73 | if (!me.options.resultsBeforeStart && me.beforeData && me.beforeData.length > 0) { 74 | var resourceTimes = me.beforeData.map(function(networkResource, idx) { return networkResource.responseEnd; }); 75 | me.lastResourceTime = Math.max.apply(null, resourceTimes); 76 | } 77 | }); 78 | }; 79 | 80 | /** 81 | * Called on metrics.teardown 82 | * 83 | * @param browser 84 | * @returns {*} 85 | */ 86 | NetworkResourcesProbe.prototype.teardown = function(browser) { 87 | var me = this; 88 | return browser.execute(helpers.fnCall(this._clientGetData)) 89 | .then(function() { 90 | return browser.eval('window.__networkResources') 91 | }) 92 | .then(function(res) { 93 | // filter results to starts with this.lastResourceTime 94 | return res.filter(function(element) { 95 | return element['startTime'] >= me.lastResourceTime 96 | }); 97 | }) 98 | .then(function(res) { 99 | me.emit('data', res); 100 | }); 101 | }; 102 | module.exports = NetworkResourcesProbe; 103 | -------------------------------------------------------------------------------- /lib/probes/PerfLogProbe.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | Url = require('url'), 3 | events = require('events'), 4 | Q = require('q'), 5 | request = require('request'), 6 | JSONStream = require('JSONStream'), 7 | eventStream = require('event-stream'), 8 | helpers = require('../helpers'), 9 | debug = require('debug')('bp:probes:PerfLogProbe'); 10 | 11 | function PerfLogProbe() { 12 | events.EventEmitter.call(this); 13 | } 14 | 15 | util.inherits(PerfLogProbe, events.EventEmitter); 16 | 17 | PerfLogProbe.prototype.id = 'PerfLogProbe'; 18 | 19 | PerfLogProbe.prototype.setup = function(cfg) { 20 | var me = this; 21 | cfg.browsers = cfg.browsers.map(function(browser) { 22 | helpers.extend(browser, { 23 | loggingPrefs: { 24 | performance: 'ALL' 25 | } 26 | }); 27 | return browser; 28 | }); 29 | return Q(cfg); 30 | }; 31 | 32 | PerfLogProbe.prototype.start = function(browser) { 33 | var me = this; 34 | return browser.logTypes().then(function(logs) { 35 | debug('Supported log types', logs); 36 | me.enabled = (logs.indexOf('performance') !== -1); 37 | }).then(function() { 38 | if (me.enabled) { 39 | return browser.log('performance'); 40 | } 41 | }); 42 | }; 43 | 44 | PerfLogProbe.prototype.teardown = function(browser) { 45 | var me = this; 46 | if (this.enabled) { 47 | return Q.promise(function(resolve, reject, notify) { 48 | var url = [ 49 | browser.configUrl.href, 50 | browser.configUrl.href.match(/\/$/) ? '' : '/', // making sure the last part of the path doesn't get stripped 51 | 'session/', 52 | browser.sessionID, 53 | '/log' 54 | ].join(''); 55 | 56 | debug('Getting Performance log', Url.format(url)); 57 | var logStream = request({ 58 | url: url, 59 | method: 'POST', 60 | json: { 61 | type: 'performance' 62 | } 63 | }).on('error', reject); 64 | 65 | //logStream.pipe(require('fs').createWriteStream('_perflog.json')); 66 | 67 | logStream.pipe(JSONStream.parse('value.*')).on('error', reject) 68 | .pipe(eventStream.map(function(data, cb) { 69 | if (typeof data.message !== 'undefined') { 70 | // ChromeDriver - format: message: "[{method:'Tracing.dataCollected', params:{cat:...,pid:...}}]" 71 | cb(null, JSON.parse(data.message).message); 72 | } else { 73 | // Appium - format: [{startTime:..., name:..., endTime:...}] 74 | cb(null, { 75 | method: 'Timeline.eventRecorded', 76 | params: data 77 | }); 78 | } 79 | })).on('data', function(data) { 80 | me.emit('data', { 81 | type: 'perfLog', 82 | value: data 83 | }); 84 | }).on('error', reject).on('end', resolve); 85 | }); 86 | } 87 | }; 88 | 89 | module.exports = PerfLogProbe; -------------------------------------------------------------------------------- /lib/probes/RafBenchmarkingProbe.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | util = require('util'), 3 | events = require('events'), 4 | helpers = require('../helpers'), 5 | debug = require('debug')('bp:probes:RafBenchmarkingProbe'); 6 | 7 | function RafBenchmarkingProbe() { 8 | events.EventEmitter.call(this); 9 | } 10 | 11 | util.inherits(RafBenchmarkingProbe, events.EventEmitter); 12 | 13 | RafBenchmarkingProbe.prototype.id = 'RafBenchmarkingProbe'; 14 | 15 | RafBenchmarkingProbe.prototype.start = function(browser) { 16 | var code = function() { 17 | var getTimeMs = (function() { 18 | if (window.performance) 19 | return (performance.now || 20 | performance.mozNow || 21 | performance.msNow || 22 | performance.oNow || 23 | performance.webkitNow).bind(window.performance); 24 | else 25 | return function() { 26 | return new Date().getTime(); 27 | }; 28 | })(); 29 | 30 | var requestAnimationFrame = (function() { 31 | return window.requestAnimationFrame || 32 | window.webkitRequestAnimationFrame || 33 | window.mozRequestAnimationFrame || 34 | window.oRequestAnimationFrame || 35 | window.msRequestAnimationFrame || 36 | function(callback) { 37 | window.setTimeout(callback, 1000 / 60); 38 | }; 39 | })().bind(window); 40 | 41 | window.__RafRecorder = { 42 | frames: [], 43 | flush: true, 44 | record: function(timeStamp) { 45 | if (__RafRecorder.flush) { 46 | __RafRecorder.frames = []; 47 | __RafRecorder.flush = false; 48 | } 49 | __RafRecorder.frames.push(timeStamp); 50 | requestAnimationFrame(__RafRecorder.record); 51 | }, 52 | get: function() { 53 | __RafRecorder.flush = true; 54 | return __RafRecorder.frames; 55 | } 56 | }; 57 | 58 | requestAnimationFrame(window.__RafRecorder.record); 59 | 60 | }; 61 | 62 | return browser.execute(helpers.fnCall(code)); 63 | }; 64 | 65 | RafBenchmarkingProbe.prototype.teardown = function(browser) { 66 | debug('Clearing timer Interval'); 67 | var me = this; 68 | return browser.eval('window.__RafRecorder.get()').then(function(res) { 69 | if (Array.isArray(res) && res.length > 0) { 70 | me.emit('data', res); 71 | } 72 | clearTimeout(me.timerHandle); 73 | }, function(err) { 74 | me.emit('error', err); 75 | clearTimeout(me.timerHandle); 76 | }); 77 | }; 78 | 79 | module.exports = RafBenchmarkingProbe; -------------------------------------------------------------------------------- /lib/probes/SampleProbe.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | util = require('util'), 3 | events = require('events'), 4 | helpers = require('../helpers'), 5 | debug = require('debug')('bp:probes:SampleProbe'); 6 | 7 | function SampleProbe(id) { 8 | if (id) { 9 | this.id = id; 10 | } 11 | debug('Initialize'); 12 | events.EventEmitter.call(this); 13 | } 14 | 15 | util.inherits(SampleProbe, events.EventEmitter); 16 | 17 | SampleProbe.prototype.id = 'SampleProbe'; 18 | 19 | SampleProbe.prototype.setup = function(cfg) { 20 | debug('Setup'); 21 | this.timerHandle = null; 22 | return Q.delay(1); 23 | }; 24 | 25 | SampleProbe.prototype.start = function(cfg, browser) { 26 | debug('start'); 27 | var me = this; 28 | this.timerHandle = setInterval(function() { 29 | debug('Event fired'); 30 | me.emit('data', { 31 | time: new Date().getTime() 32 | }); 33 | }); 34 | return Q.delay(1); 35 | }; 36 | 37 | SampleProbe.prototype.teardown = function(cfg, browser) { 38 | debug('teardown'); 39 | clearInterval(this.timerHandle); 40 | return Q.delay(1); 41 | }; 42 | 43 | module.exports = SampleProbe; -------------------------------------------------------------------------------- /lib/probes/index.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | helpers = require('../helpers'), 3 | debug = require('debug')('bp:probes'); 4 | 5 | function ProbeManager() { 6 | this.probes = {}; 7 | } 8 | 9 | ProbeManager.prototype.addProbes = function(metric) { 10 | var me = this; 11 | metric.probes = metric.probes || []; 12 | metric.probes.forEach(function(probe) { 13 | if (typeof probe === 'string') { 14 | 15 | if (typeof me.probes[probe] === 'object') { 16 | probe = me.probes[probe]; 17 | } else { 18 | var fn = require('./' + probe); 19 | probe = new fn(); 20 | } 21 | } 22 | 23 | if (typeof probe !== 'object' || typeof probe.id === 'undefined') { 24 | throw 'Probe needs to be an object and is ' + typeof probes + ' and should have an id'; 25 | } 26 | if (typeof me.probes[probe.id] === 'undefined') { 27 | me.probes[probe.id] = probe; 28 | } 29 | debug('Registering probe', probe.id); 30 | (typeof metric.onData === 'function') && probe.on('data', metric.onData.bind(metric)); 31 | (typeof metric.onError === 'function') && probe.on('error', metric.onError.bind(metric)); 32 | }); 33 | }; 34 | 35 | function promise(method) { 36 | return function() { 37 | var args = arguments, 38 | me = this; 39 | debug('' + method); 40 | return Object.keys(this.probes).map(function(probeKey) { 41 | return function() { 42 | probe = me.probes[probeKey]; 43 | if (typeof probe[method] === 'function') { 44 | debug(probe.id, method, 'called'); 45 | return probe[method].apply(probe, args); 46 | } else { 47 | return Q(); 48 | } 49 | } 50 | }).reduce(Q.when, Q()); 51 | } 52 | } 53 | 54 | ProbeManager.prototype.setup = promise('setup'); 55 | ProbeManager.prototype.start = promise('start'); 56 | ProbeManager.prototype.teardown = promise('teardown'); 57 | module.exports = ProbeManager; -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | /* Module to be used by other web driver runners like protractor*/ 2 | var Metrics = require('./metrics'), 3 | options = require('./options'), 4 | helpers = require('./helpers'), 5 | Q = require('q'), 6 | debug = require('debug')('bp:selenium:runner'), 7 | wd = require('wd'); 8 | 9 | var metrics = null; 10 | 11 | function Runner(opts) { 12 | this.opts = options.scrub(opts); 13 | this.metrics = null; 14 | this.browser = null; 15 | }; 16 | 17 | Runner.prototype.setupMetrics_ = function() { 18 | var me = this; 19 | if (this.metrics === null) { 20 | this.metrics = new Metrics(this.opts.metrics); 21 | return this.metrics.setup(this.opts); 22 | } else { 23 | return Q(); 24 | } 25 | }; 26 | 27 | Runner.prototype.attachBrowser_ = function(sessionId) { 28 | if (this.browser === null) { 29 | this.browser = wd.promiseRemote(this.opts.selenium); 30 | this.browser.on('status', function(data) { 31 | //log.debug(data); 32 | }); 33 | this.browser.on('command', function(meth, path, data) { 34 | if (data && typeof data === 'object') { 35 | var data = JSON.stringify(data); 36 | } 37 | debug(meth, (path || '').substr(0, 70), (data || '').substr(0, 70)); 38 | }); 39 | return this.browser.attach(sessionId); 40 | } else { 41 | return Q(); 42 | } 43 | }; 44 | 45 | Runner.prototype.config = function(cb) { 46 | var me = this; 47 | this.setupMetrics_().then(function() { 48 | cb(null, me.opts); 49 | }, function(err) { 50 | cb(err); 51 | }); 52 | } 53 | 54 | Runner.prototype.start = function(sessionId, cb) { 55 | var me = this; 56 | if (typeof cb !== 'function') { 57 | cb = function() {}; 58 | } 59 | this.setupMetrics_().then(function() { 60 | return me.attachBrowser_(sessionId); 61 | }).then(function() { 62 | me.metrics.start(me.browser).then(cb, cb); 63 | }); 64 | }; 65 | 66 | Runner.prototype.stop = function(cb) { 67 | var me = this; 68 | this.metrics.teardown(this.browser).then(function() { 69 | return me.metrics.getResults(); 70 | }).then(function(data) { 71 | cb(undefined, data); 72 | }, function(err) { 73 | cb(err, null); 74 | }); 75 | }; 76 | 77 | module.exports = Runner; -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Parashuram 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-perf", 3 | "version": "1.4.11", 4 | "description": "Measure browser rendering performance metrics", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha --recursive --require=./test/test.helper.js ./test" 8 | }, 9 | "bin": { 10 | "browser-perf": "lib/cli.js" 11 | }, 12 | "author": "Parashuram", 13 | "license": "BSD-2-Clause", 14 | "dependencies": { 15 | "JSONStream": "^1.0.6", 16 | "byline": "^4.2.1", 17 | "cli-table": "~0.3.1", 18 | "commander": "^2.9.0", 19 | "debug": "^2.2.0", 20 | "event-stream": "^3.3.2", 21 | "glob": "^6.0.2", 22 | "jsmin": "~1.0.1", 23 | "q": "^1.4.1", 24 | "request": "^2.65.0", 25 | "wd": "^0.4.0" 26 | }, 27 | "devDependencies": { 28 | "chai": "~3.4.0", 29 | "chai-as-promised": "^5.1.0", 30 | "mocha": "^2.3.3", 31 | "sinon": "^1.17.2" 32 | }, 33 | "directories": { 34 | "test": "test" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/axemclion/browser-perf.git" 39 | }, 40 | "keywords": [ 41 | "browser-perf", 42 | "rendering", 43 | "telemetry", 44 | "chromium", 45 | "performance", 46 | "high performance web sites", 47 | "metrics", 48 | "monitoring", 49 | "web development", 50 | "webperf" 51 | ], 52 | "bugs": { 53 | "url": "https://github.com/axemclion/browser-perf/issues" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/e2e/e2e.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | debug = require('debug')('bp:test:e2e'), 3 | browserPerf = require('../../'); 4 | 5 | var expectedMetrics = { 6 | chrome: [ 7 | // ChromeTracingMetrics & RafRenderingMetrics 8 | 'mean_frame_time', 9 | 'meanFrameTime_raf', 10 | 11 | // Network Timings 12 | 'firstPaint', 13 | 'connectStart', 14 | 'domainLookupStart', 15 | 'domComplete', 16 | 'domInteractive', 17 | 'domLoading', 18 | 'fetchStart', 19 | 'navigationStart', 20 | 21 | // RuntimePerfMetrics 22 | 'ExpensiveEventHandlers', 23 | 'ExpensivePaints', 24 | 'Layers', 25 | 'NodePerLayout_avg', 26 | 'PaintedArea_avg', 27 | 'PaintedArea_total', 28 | 'framesPerSec (devtools)', 29 | 'Styles', 30 | 'Javascript', 31 | 32 | // TimelineMetrics 33 | 'Decode Image', 34 | 'CompositeLayers', 35 | 'Layout', 36 | 'Paint', 37 | //'RecalculateStyles', 38 | //'EvaluateScript', 39 | //'EventDispatch', 40 | 'FireAnimationFrame', 41 | 'FunctionCall', 42 | //'GCEvent', 43 | //'XHRReadyStateChange', 44 | 'UpdateLayerTree' 45 | ], 46 | firefox: [ 47 | 'meanFrameTime_raf', 48 | // Network Timings 49 | 'connectStart', 50 | 'domainLookupStart', 51 | 'domComplete', 52 | 'domInteractive', 53 | 'domLoading', 54 | 'fetchStart', 55 | 'navigationStart', 56 | ] 57 | }; 58 | 59 | describe('End To End Test Cases', function() { 60 | it('fails if selenium is not running', function(done) { 61 | this.timeout(60*1000); 62 | browserPerf('http://google.com', function(err, res) { 63 | expect(err).to.not.be.null; 64 | expect(err).to.not.be.empty; 65 | expect(res).to.be.empty; 66 | done(); 67 | }, { 68 | selenium: 'nohost:4444' 69 | }); 70 | }); 71 | 72 | describe('gets enough statistics from browsers', function() { 73 | this.timeout(10 * 60 * 1000); // 10 minutes for E2E tests 74 | it('should work for a sample page', function(done) { 75 | var url = 'http://nparashuram.com/perfslides/'; 76 | browserPerf(url, function(err, res) { 77 | if (err) { 78 | console.log(err); 79 | } 80 | expect(err).to.be.empty; 81 | expect(res).to.not.be.empty; 82 | res.forEach(function(data) { 83 | expect(data._url).to.equal(url); 84 | debug('Testing', data._browserName); 85 | expect(data).to.include.keys(expectedMetrics[data._browserName]); 86 | }); 87 | done(); 88 | }, { 89 | selenium: process.env.SELENIUM || 'http://localhost:4444/wd/hub', 90 | username: process.env.USERNAME, 91 | accesskey: process.env.ACCESSKEY, 92 | browsers: [{ 93 | browserName: 'chrome', 94 | version: 49, 95 | name: 'Browserperf-E2E Tests' 96 | }, { 97 | browserName: 'firefox', 98 | version: 44, 99 | name: 'Browserperf-E2E Tests' 100 | }] 101 | }); 102 | }); 103 | }); 104 | }); -------------------------------------------------------------------------------- /test/e2e/runner.spec.js: -------------------------------------------------------------------------------- 1 | var wd = require('wd'), 2 | browserPerf = require('../../'), 3 | url = require('url'), 4 | chai = require("chai"), 5 | expect = chai.expect; 6 | 7 | chai.should(); 8 | 9 | describe('Runner', function() { 10 | this.timeout(10 * 60 * 1000); // 10 minutes for E2E tests 11 | var config = { 12 | host: process.env.SELENIUM || 'http://localhost:4444/wd/hub', 13 | username: process.env.USERNAME, 14 | accesskey: process.env.ACCESSKEY 15 | }; 16 | 17 | 18 | var seleniumAddress = url.parse(config.host); 19 | var browser = wd.remote(seleniumAddress.hostname, seleniumAddress.port, config.username, config.accesskey); 20 | 21 | var runner = new browserPerf.runner({ 22 | selenium: config.host, 23 | username: config.username, 24 | accesskey: config.accesskey, 25 | browsers: [{ 26 | browserName: 'chrome', 27 | version: 49, 28 | name: 'runner.spec.js' 29 | }] 30 | }); 31 | 32 | it('should be run via a runner inside another test case', function(done) { 33 | runner.config(function(err, cfg) { // Call this before starting anything 34 | var capabilities = cfg.browsers[0]; 35 | browser.init(capabilities, function() { 36 | runner.start(browser.getSessionId(), function() { // call this as soon as the browser is available 37 | browser.get("http://admc.io/wd/test-pages/guinea-pig.html", function() { 38 | browser.title(function(err, title) { 39 | title.should.include('WD'); 40 | browser.elementById('i am a link', function(err, el) { 41 | browser.clickElement(el, function() { 42 | browser.eval("window.location.href", function(err, href) { 43 | href.should.include('guinea-pig2'); 44 | runner.stop(function(err, res) { // call this before closing the browser 45 | expect(err).to.be.empty; 46 | expect(res).to.not.be.empty; 47 | browser.quit(); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | }); 53 | }); 54 | }) 55 | }) 56 | }); 57 | }); 58 | }); 59 | }) -------------------------------------------------------------------------------- /test/res/android-hybrid.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "selenium": "localhost:9515", 3 | "browsers": [{ 4 | "browserName": "android", 5 | "chromeOptions": { 6 | "androidActivity": "io.cordova.hellocordova.MainActivity", 7 | "androidPackage": "io.cordova.hellocordova" 8 | } 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /test/res/android.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "selenium": "http://localhost:9515", 3 | "browsers": [{ 4 | "browserName": "android" 5 | }] 6 | } -------------------------------------------------------------------------------- /test/res/browserstack.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "selenium": "http://hub.browserstack.com:80/wd/hub", 3 | "browsers": [{ 4 | "browserName": "chrome", 5 | "version": "35", 6 | "platform": "WINDOWS", 7 | "browserstack.user": "parsahuram", 8 | "browserstack.key": "", 9 | "browserstack.debug": "true" 10 | }] 11 | } -------------------------------------------------------------------------------- /test/res/chrome_canary.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "browsers": [{ 3 | "browserName": "chrome", 4 | "chromeOptions": { 5 | "binary": "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" 6 | } 7 | }], 8 | "selenium": { 9 | "hostname": "localhost", 10 | "port": 4444 11 | }, 12 | "debug": true, 13 | "tmp": { 14 | "binary": "%LOCALAPPDATA%/Google/Chrome SxS/Application/chrome.exe", 15 | "binary": "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" 16 | } 17 | } -------------------------------------------------------------------------------- /test/res/ios-hybrid-appium.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "selenium": "http://localhost:4723/wd/hub", 3 | "browsers": [{ 4 | "platformName": "iOS", 5 | "platformVersion": "8.3", 6 | "deviceName": "iPhone 6", 7 | "app": "/Users/axemclion/_workspace/tmp/sample/platforms/ios/build/emulator/HelloCordova.app", 8 | "bundleId": "io.cordova.hellocordova", 9 | "autoWebview": true 10 | }] 11 | } -------------------------------------------------------------------------------- /test/res/ios-safari-appium.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "selenium": "http://localhost:4723/wd/hub", 3 | "browsers": [{ 4 | "platformName": "iOS", 5 | "platformVersion": "8.4", 6 | "browserName": "Safari", 7 | "deviceName": "iPhone 6" 8 | }] 9 | } -------------------------------------------------------------------------------- /test/res/saucelabs.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "selenium": { 3 | "hostname": "ondemand.saucelabs.com", 4 | "port": "80", 5 | "user": "browserperf", 6 | "pwd": "" 7 | }, 8 | "browsers": [{ 9 | "browserName": "chrome", 10 | "platform": "linux", 11 | "version": "35" 12 | }] 13 | } -------------------------------------------------------------------------------- /test/res/selenium_debug.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "browsers": [{ 3 | "browserName": "chrome", 4 | "debug": true 5 | }], 6 | "selenium": { 7 | "hostname": "localhost", 8 | "port": 4444 9 | }, 10 | "debug": true 11 | } -------------------------------------------------------------------------------- /test/res/selenium_local.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "browsers": [{ 3 | "browserName": "chrome" 4 | }, { 5 | "browserName": "firefox" 6 | }, { 7 | "browserName": "IE" 8 | }], 9 | "selenium": { 10 | "hostname": "localhost", 11 | "port": 4444 12 | } 13 | } -------------------------------------------------------------------------------- /test/test.helper.js: -------------------------------------------------------------------------------- 1 | require('q').longStackSupport = true; 2 | 3 | if (!process.env.DEBUG) { 4 | process.env.DEBUG = 'bp:*'; 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/actions.spec.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | wd = require('wd'), 3 | sinon = require('sinon'), 4 | chai = require("chai"), 5 | chaiAsPromised = require("chai-as-promised"), 6 | expect = chai.expect; 7 | 8 | chai.use(chaiAsPromised); 9 | 10 | var Actions = require('../../lib/actions/index.js'); 11 | 12 | describe('Actions', function() { 13 | var browser = wd.remote('localhost:4444'), 14 | execute = sinon.stub(browser, 'execute').returns(Q.when(1)), 15 | waitFor = sinon.stub(browser, 'waitFor').returns(Q.when(1)), 16 | eval = sinon.stub(browser, 'eval').returns(Q(1)), 17 | sleep = sinon.stub(browser, 'sleep').returns(Q(1)), 18 | get = sinon.stub(browser, 'get').returns(Q.when(1)); 19 | 20 | var click = sinon.stub().returns(Q.when(1)), 21 | type = sinon.stub().returns(Q.when(1)), 22 | elementByCssSelector = sinon.stub(browser, 'elementByCssSelector').returns(Q.when({ 23 | click: click, 24 | type: type 25 | })); 26 | 27 | function scrollAssertions() { 28 | expect(execute.calledOnce).to.be.true; 29 | expect(waitFor.calledOnce).to.be.true; 30 | execute.reset(); 31 | waitFor.reset(); 32 | } 33 | 34 | it('is passed as strings', function(done) { 35 | expect(new Actions(['scroll']).perform(browser).then(scrollAssertions)).to.eventually.be.fulfilled.and.notify(done); 36 | }); 37 | 38 | it('should work when more than one action is performed', function(done) { 39 | expect(new Actions(['scroll', 'scroll']).perform(browser).then(function() { 40 | expect(execute.calledTwice).to.be.true; 41 | expect(waitFor.calledTwice).to.be.true; 42 | execute.reset(); 43 | waitFor.reset(); 44 | })).to.eventually.be.fulfilled.and.notify(done); 45 | }); 46 | 47 | it('performs actions passed as function', function(done) { 48 | expect(new Actions([Actions.actions.scroll()]).perform(browser).then(scrollAssertions)).to.eventually.be.fulfilled.and.notify(done); 49 | }); 50 | 51 | it('performs actions scroll with params', function(done) { 52 | expect(new Actions([Actions.actions.scroll({ 53 | direction: 'left', 54 | pollFreq: 10000 55 | })]).perform(browser).then(function() { 56 | expect(waitFor.args[0][0].pollFreq).to.equal(10000); 57 | }).then(scrollAssertions)).to.eventually.be.fulfilled.and.notify(done); 58 | }); 59 | 60 | it('performs wait action with params', function(done) { 61 | expect(new Actions([Actions.actions.wait(500)]).perform(browser).then(function() { 62 | expect(sleep.calledWith(500)).to.be.true; 63 | })).to.eventually.be.fulfilled.and.notify(done); 64 | }); 65 | 66 | var loginParams = { 67 | page: 'login.html', 68 | username: { 69 | field: 'username', 70 | val: 'username' 71 | }, 72 | password: { 73 | field: 'password', 74 | val: 'password' 75 | }, 76 | submit: { 77 | field: 'submit' 78 | } 79 | }; 80 | 81 | it('performs login with passed in params', function(done) { 82 | expect(new Actions([Actions.actions.login(loginParams)]).perform(browser).then(function() { 83 | expect(elementByCssSelector.args).to.deep.equal([ 84 | [loginParams.username.field], 85 | [loginParams.password.field], 86 | [loginParams.submit.field] 87 | ]); 88 | expect(type.args).to.deep.equal([ 89 | [loginParams.username.val], 90 | [loginParams.password.val] 91 | ]); 92 | })).to.eventually.be.fulfilled.and.notify(done); 93 | }); 94 | 95 | it('perfoms login and then scroll', function(done) { 96 | expect(new Actions([Actions.actions.login(loginParams), 'scroll']).perform(browser).then(function() { 97 | expect(get.calledBefore(elementByCssSelector)).to.be.true; 98 | expect(elementByCssSelector.calledBefore(type)).to.be.true; 99 | expect(type.calledBefore(click)).to.be.true; 100 | expect(click.calledBefore(waitFor)).to.be.true; 101 | })).to.eventually.be.fulfilled.and.notify(done); 102 | }); 103 | 104 | it('should work with chrome extensions', function(done) { 105 | eval.restore(); 106 | eval = sinon.stub(browser, 'eval').returns(Q('true')); 107 | expect(new Actions([Actions.actions.scroll()]).perform(browser).then(scrollAssertions)).to.eventually.be.fulfilled.and.notify(function() { 108 | eval.restore(); 109 | eval = sinon.stub(browser, 'eval').returns(Q(1)); 110 | done(); 111 | }); 112 | }); 113 | }); -------------------------------------------------------------------------------- /test/unit/docs.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"), 2 | expect = chai.expect; 3 | 4 | var Docs = require('../../docs'); 5 | 6 | describe('Metric Docs', function() { 7 | 8 | it('should return docs for specific source', function() { 9 | var apiDocs = new Docs('TimelineMetrics'); 10 | expect(apiDocs.metrics).to.not.be.empty; 11 | for (var key in apiDocs.metrics) { 12 | expect(apiDocs.metrics[key].source).to.equal('TimelineMetrics'); 13 | } 14 | }); 15 | 16 | it('should return docs for source when specified as array', function() { 17 | var apiDocs = new Docs(['TimelineMetrics', 'NetworkTimings']); 18 | expect(apiDocs.metrics).to.not.be.empty; 19 | var result = apiDocs.metrics; 20 | for (var key in result) { 21 | expect(result[key].source == 'TimelineMetrics' || result[key].source == 'NetworkTimings').to.be.true; 22 | } 23 | }); 24 | 25 | it('should return docs for a specific metric', function() { 26 | var apiDocs = new Docs(); 27 | var result = apiDocs.get('meanFrameTime_raf') 28 | expect(result).to.contain.keys(['type', 'unit', 'summary', 'details', 'source', 'tags']); 29 | expect(result.source).to.equal('RafRenderingStats'); 30 | }); 31 | 32 | it('should return empty object for unmatched metric', function() { 33 | var apiDocs = new Docs(); 34 | var result = apiDocs.get('UNAVAILABLE_METRIC'); 35 | expect(result).to.be.empty; 36 | }); 37 | 38 | it('should get a specific property for a metric', function() { 39 | var apiDocs = new Docs(); 40 | expect(apiDocs.getProp('meanFrameTime_raf', 'unit')).to.equal('ms'); 41 | }); 42 | 43 | it('should get a specific property for an undefined metric', function() { 44 | var apiDocs = new Docs(); 45 | expect(apiDocs.getProp('UNAVAILABLE', 'unit')).to.be.undefined; 46 | }); 47 | }); -------------------------------------------------------------------------------- /test/unit/helpers.spec.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | chai = require("chai"), 3 | chaiAsPromised = require("chai-as-promised"), 4 | expect = chai.expect; 5 | 6 | var helpers = require('../../lib/helpers'); 7 | 8 | chai.use(chaiAsPromised); 9 | 10 | describe('Helpers', function () { 11 | 12 | describe('deepEquals', function () { 13 | it('should check objects correctly', function () { 14 | var de = helpers.deepEquals; 15 | 16 | expect(de(undefined, 'a.b.c', 1)).to.be.false; 17 | expect(de(null, 'a.b.c', 1)).to.be.false; 18 | expect(de([], 'a', 1)).to.be.false; 19 | expect(de({ a: 1 }, 'a', 1)).to.be.true; 20 | expect(de({ a: { b: 1 } }, 'a.b', 1)).to.be.true; 21 | expect(de({ a: [1, 2] }, 'a.b', 2)).to.be.false; 22 | expect(de({}, 'browserName', 'chrome')).to.be.false; 23 | expect(de({ browserName: 'chrome' }, 'browserName', 'chrome')).to.be.true; 24 | expect(de({ browserName: 'chrome', chromeOptions: { activity: 'com.android.chrome' } }, 'chromeOptions.activity', 'com.android.chrome')).to.be.true; 25 | expect(de({ browserName: 'chrome' }, 'chromeOptions.activity', 'com.android.chrome')).to.be.false; 26 | }); 27 | }); 28 | }); -------------------------------------------------------------------------------- /test/unit/metrics.spec.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | sinon = require('sinon'), 3 | chai = require("chai"), 4 | util = require('util'), 5 | events = require('events') 6 | chaiAsPromised = require("chai-as-promised"), 7 | expect = chai.expect; 8 | 9 | chai.use(chaiAsPromised); 10 | 11 | describe('Metrics', function() { 12 | var Metrics = require('../../lib/metrics'), 13 | SampleMetric = require('../../lib/metrics/SampleMetric'), 14 | SampleProbe = require('../../lib/probes/SampleProbe'); 15 | 16 | describe('Metrics and Probes Lifecycle', function() { 17 | function testOrder() { 18 | var arr = Array.prototype.slice.call(arguments, 0); 19 | for (var i = 1; i < arr.length; i++) { 20 | expect(arr[i - 1].calledBefore(arr[i])).to.be.true; 21 | } 22 | } 23 | 24 | function getSpies(item, arr) { 25 | var res = {}; 26 | arr.forEach(function(a) { 27 | res[a] = sinon.spy(item, a); 28 | }); 29 | return res; 30 | } 31 | 32 | it('should have correct lifecycle for single probe and metric', function(done) { 33 | var probe = new SampleProbe(), 34 | metric = new SampleMetric([probe]), 35 | spyProbe = getSpies(probe, ['setup', 'start', 'teardown']), 36 | spyMetric = getSpies(metric, ['setup', 'start', 'teardown', 'onData', 'getResults']); 37 | 38 | var metrics = new Metrics([metric]); 39 | expect(metrics.setup().then(function() { 40 | return metrics.start(); 41 | }).then(function() { 42 | return Q.delay(50); 43 | }).then(function() { 44 | return metrics.teardown(); 45 | }).then(function() { 46 | return metrics.getResults(); 47 | }).then(function(res) { 48 | testOrder( 49 | spyMetric.setup, spyProbe.setup, 50 | spyMetric.start, spyProbe.start, 51 | spyMetric.onData, 52 | spyProbe.teardown, spyMetric.teardown, 53 | spyMetric.getResults 54 | ); 55 | })).to.eventually.be.fulfilled.and.notify(done); 56 | }); 57 | 58 | 59 | it('should have correct lifecycle for multiple probes and metric', function(done) { 60 | var probe1 = new SampleProbe('probe1'), 61 | probe2 = new SampleProbe('probe2'), 62 | metric = new SampleMetric([probe1, probe2]), 63 | spyProbe1 = getSpies(probe1, ['setup', 'start', 'teardown']), 64 | spyProbe2 = getSpies(probe2, ['setup', 'start', 'teardown']), 65 | spyMetric = getSpies(metric, ['setup', 'start', 'teardown', 'onData', 'getResults']); 66 | 67 | var metrics = new Metrics([metric]); 68 | expect(metrics.setup().then(function() { 69 | return metrics.start(); 70 | }).then(function() { 71 | return Q.delay(50); 72 | }).then(function() { 73 | return metrics.teardown(); 74 | }).then(function() { 75 | return metrics.getResults(); 76 | }).then(function(res) { 77 | testOrder( 78 | spyMetric.setup, spyProbe1.setup, spyProbe2.setup, 79 | spyMetric.start, spyProbe1.start, spyProbe2.start, 80 | spyProbe1.teardown, spyProbe2.teardown, spyMetric.teardown, 81 | spyMetric.getResults 82 | ); 83 | })).to.eventually.be.fulfilled.and.notify(done); 84 | }); 85 | 86 | it('should call shared probes only once', function(done) { 87 | var probe = new SampleProbe('probe'), 88 | metric1 = new SampleMetric([probe]), 89 | metric2 = new SampleMetric([probe]), 90 | spyProbe = getSpies(probe, ['setup', 'start', 'teardown']), 91 | spyMetric1 = getSpies(metric1, ['setup', 'start', 'teardown', 'onData', 'getResults']), 92 | spyMetric2 = getSpies(metric2, ['setup', 'start', 'teardown', 'onData', 'getResults']); 93 | 94 | var metrics = new Metrics([metric1, metric2]); 95 | expect(metrics.setup().then(function() { 96 | return metrics.start(); 97 | }).then(function() { 98 | return Q.delay(50); 99 | }).then(function() { 100 | return metrics.teardown(); 101 | }).then(function() { 102 | return metrics.getResults(); 103 | }).then(function(res) { 104 | testOrder( 105 | spyMetric1.setup, spyMetric2.setup, spyProbe.setup, 106 | spyMetric1.start, spyMetric2.start, spyProbe.start, 107 | spyMetric1.onData, spyMetric2.onData, 108 | spyProbe.teardown, spyMetric1.teardown, spyMetric2.teardown 109 | ); 110 | expect(spyProbe.setup.calledOnce).to.be.true; 111 | expect(spyProbe.teardown.calledOnce).to.be.true; 112 | expect(spyProbe.start.calledOnce).to.be.true; 113 | })).to.eventually.be.fulfilled.and.notify(done); 114 | }); 115 | }); 116 | }); -------------------------------------------------------------------------------- /test/unit/options/actions.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var options = require('../../../lib/options'); 4 | 5 | describe('Options', function() { 6 | describe('Actions', function() { 7 | var test = options.scrub; 8 | it('default', function() { 9 | var res = test({}); 10 | expect(res.actions).to.deep.equal(['scroll']); 11 | }); 12 | }); 13 | }); -------------------------------------------------------------------------------- /test/unit/options/browsers.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | fs = require('fs'); 3 | 4 | var options = require('../../../lib/options'); 5 | 6 | describe('Options', function() { 7 | describe('Browsers', function() { 8 | var test = options.scrub; 9 | 10 | it('default', function() { 11 | var res = test({}); 12 | expect(res.browsers).deep.equal([{ 13 | browserName: 'chrome', 14 | version: 35 15 | }]); 16 | }); 17 | 18 | it('single browser as string', function() { 19 | var res = test({ 20 | browsers: 'chrome' 21 | }); 22 | expect(res.browsers).deep.equal([{ 23 | browserName: 'chrome' 24 | }]); 25 | }); 26 | 27 | it('multiple browsers as strings', function() { 28 | var res = test({ 29 | browsers: 'chrome,firefox' 30 | }); 31 | expect(res.browsers.length).to.equal(2); 32 | expect(res.browsers).deep.equal([{ 33 | browserName: 'chrome' 34 | }, { 35 | browserName: 'firefox' 36 | }]); 37 | }); 38 | 39 | it('string array', function() { 40 | var res = test({ 41 | browsers: ['chrome', 'firefox'] 42 | }); 43 | expect(res.browsers.length).to.equal(2); 44 | expect(res.browsers).deep.equal([{ 45 | browserName: 'chrome' 46 | }, { 47 | browserName: 'firefox' 48 | }]); 49 | }); 50 | 51 | it('object array', function() { 52 | var cfg = [{ 53 | browserName: 'chrome' 54 | }, { 55 | browserName: 'firefox' 56 | }] 57 | var res = test({ 58 | browsers: cfg 59 | }); 60 | expect(res.browsers.length).to.equal(2); 61 | expect(res.browsers).deep.equal(cfg); 62 | }); 63 | 64 | it('should parse browserstack credentials', function() { 65 | var res = test({ 66 | browser: ['chrome', 'firefox'], 67 | BROWSERSTACK_USERNAME: 'username', 68 | BROWSERSTACK_KEY: 'key', 69 | selenium: 'hub.browserstack.com' 70 | }); 71 | expect(res.browsers.length).to.eq(2); 72 | expect(res.browsers[0]['browserstack.user']).to.eq('username'); 73 | expect(res.browsers[0]['browserstack.key']).to.eq('key'); 74 | expect(res.browsers[1]['browserstack.user']).to.eq('username'); 75 | expect(res.browsers[1]['browserstack.key']).to.eq('key'); 76 | }); 77 | 78 | it('should parse saucelabs credentials for Sauce tests', function() { 79 | var res = test({ 80 | browser: 'chrome', 81 | SAUCE_USERNAME: 'username', 82 | SAUCE_ACCESSKEY: 'key', 83 | 84 | selenium: 'ondemand.saucelabs.com' 85 | }); 86 | expect(res.browsers.length).to.eq(1); 87 | expect(res.selenium.auth).to.eq('username:key'); 88 | }); 89 | 90 | it('should not use sauce credentials for non-sauce selenium', function() { 91 | var res = test({ 92 | browser: 'chrome', 93 | SAUCE_USERNAME: 'username', 94 | SAUCE_ACCESSKEY: 'key', 95 | }); 96 | expect(res.browsers.length).to.eq(1); 97 | expect(res.selenium.auth).to.be.null; 98 | }); 99 | 100 | it('should add appropriate chromeOptions for android chrome', function() { 101 | var res = test({ 102 | browsers: [{ 103 | browserName: 'android' 104 | }] 105 | }); 106 | expect(res.browsers[0]).to.deep.eq({ 107 | browserName: 'android', 108 | chromeOptions: { 109 | androidPackage: 'com.android.chrome' 110 | } 111 | }); 112 | }); 113 | 114 | it('should not mess up WebView projects', function() { 115 | var res = { 116 | browsers: [{ 117 | browserName: 'android', 118 | chromeOptions: { 119 | androidPackage: 'io.cordova.hellocordova', 120 | androidActivity: 'io.cordova.hellocordova.HelloCordova' 121 | } 122 | }] 123 | }; 124 | expect(test(res).browsers).to.deep.eq(res.browsers); 125 | }); 126 | }); 127 | }); -------------------------------------------------------------------------------- /test/unit/options/configFile.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | glob = require('glob').sync; 3 | 4 | var options = require('../../../lib/options'); 5 | 6 | describe('Options', function() { 7 | describe('Config File', function() { 8 | it('throws an exception when the file does not exist', function() { 9 | expect(function() { 10 | options.scrub({ 11 | configFile: 'nosuchfile.ext' 12 | }); 13 | }).to.throw(Error); 14 | }) 15 | it('parses selenium local file', function() { 16 | var res = options.scrub({ 17 | configFile: __dirname + '/../../res/selenium_local.config.json' 18 | }); 19 | }); 20 | }); 21 | }); -------------------------------------------------------------------------------- /test/unit/options/defaults.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var options = require('../../../lib/options'); 4 | 5 | describe('Options', function() { 6 | describe('Defaults', function() { 7 | it('has defaults', function() { 8 | var res = options.scrub(); 9 | expect(res.browsers.length).to.be.eq(1); 10 | expect(res.selenium.hostname).to.not.be.undefined; 11 | }) 12 | }); 13 | }); -------------------------------------------------------------------------------- /test/unit/options/selenium.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | fs = require('fs'); 3 | 4 | var options = require('../../../lib/options'); 5 | 6 | describe('Options', function() { 7 | var test = options.scrub; 8 | describe('Selenium', function() { 9 | it('have defaults for selenium', function() { 10 | var res = test({}).selenium; 11 | expect(res.hostname).to.equal('localhost'); 12 | expect(res.port).to.equal(4444); 13 | expect(res.user).to.be.undefined; 14 | expect(res.pwd).to.be.undefined; 15 | }); 16 | 17 | it('parse simple string configurations', function() { 18 | var res = test({ 19 | selenium: 'localhost:4444' 20 | }).selenium; 21 | expect(res.hostname).to.equal('localhost'); 22 | expect(res.port).to.equal(4444); 23 | expect(res.user).to.be.undefined; 24 | expect(res.pwd).to.be.undefined; 25 | }); 26 | 27 | it('parse string configurations', function() { 28 | var res = test({ 29 | selenium: 'http://localhost:4444' 30 | }).selenium; 31 | expect(res.hostname).to.equal('localhost'); 32 | expect(res.port).to.equal(4444); 33 | expect(res.user).to.be.undefined; 34 | expect(res.pwd).to.be.undefined; 35 | 36 | }); 37 | 38 | it('parse object configurations', function() { 39 | var res = test({ 40 | selenium: { 41 | hostname: 'localhost', 42 | port: 4444 43 | } 44 | }).selenium; 45 | expect(res.hostname).to.equal('localhost'); 46 | expect(res.port).to.equal(4444); 47 | expect(res.user).to.be.undefined; 48 | expect(res.pwd).to.be.undefined; 49 | 50 | }); 51 | 52 | it('parse Saucelabs configuration', function() { 53 | var res = test({ 54 | selenium: 'ondemand.saucelabs.com:80', 55 | username: 'sauceuser', 56 | accesskey: 'saucekey' 57 | }).selenium 58 | expect(res.hostname).to.equal('ondemand.saucelabs.com'); 59 | expect(res.port).to.equal(80); 60 | expect(res.auth).to.equal('sauceuser:saucekey'); 61 | }); 62 | }); 63 | }); -------------------------------------------------------------------------------- /test/unit/util.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | describe('Util', function() { 3 | it('extend works correctly', function() { 4 | var e = require('../../lib/helpers').extend; 5 | expect(e({}, {})).to.deep.equal({}); 6 | expect(e(undefined, {})).to.deep.equal({}); 7 | 8 | expect(e({ 9 | a: 1 10 | }, { 11 | b: 1 12 | })).to.deep.equal({ 13 | a: 1, 14 | b: 1 15 | }); 16 | 17 | expect(e({ 18 | a: 1, 19 | b: [1, 2] 20 | }, { 21 | b: 3 22 | })).to.deep.equal({ 23 | a: 1, 24 | b: 3 25 | }); 26 | 27 | expect(e({ 28 | a: 1, 29 | b: [1, 2] 30 | }, { 31 | b: [3] 32 | })).to.deep.equal({ 33 | a: 1, 34 | b: [1, 2, 3] 35 | }); 36 | 37 | expect(e({ 38 | a: { 39 | x: 1, 40 | y: 1 41 | }, 42 | b: [10] 43 | }, { 44 | a: { 45 | z: 1 46 | }, 47 | b: { 48 | a: 1 49 | } 50 | })).to.deep.equal({ 51 | a: { 52 | x: 1, 53 | y: 1, 54 | z: 1 55 | }, 56 | b: { 57 | a: 1 58 | } 59 | }); 60 | var inlineMod = { 61 | a: 1 62 | }; 63 | e(inlineMod, { 64 | b: 2 65 | }); 66 | expect(inlineMod).to.deep.equal({ 67 | a: 1, 68 | b: 2 69 | }); 70 | }); 71 | }); --------------------------------------------------------------------------------