├── README.md └── waterfall.js /README.md: -------------------------------------------------------------------------------- 1 | # Waterfall 2 | 3 | A bookmarklet to create page load waterfall in the browser using the Resource Timing API 4 | 5 | Just add the bookmarklet below to your bookmarks bar. 6 | 7 | ``` 8 | javascript:(function(){var el=document.createElement('script');el.type='text/javascript';el.src='//andydavies.github.io/waterfall/bookmarklet/waterfall.js';document.getElementsByTagName('body')[0].appendChild(el);})(); 9 | ``` 10 | 11 | # Works In* 12 | 13 | IE 10, Chromium Nightly 14 | 15 | *When I say 'works in' I mean you'll get a waterfall but sometimes there will be odd things about it! 16 | 17 | # To Do 18 | 19 | - Add DOM event markers e.g. onload etc. 20 | - Add AppCache, fix TCP times 21 | - ~~Truncate URL display~~ 22 | - Remove protocol from URL display 23 | - Fix blocked timings 24 | - Display blocked / active times for 3rd party resources 25 | - Check if bookmarklet script loaded before adding it 26 | - Add iframe support 27 | - Add tooltip with full URL and timing details 28 | - Add row number? 29 | - Add legend 30 | - Cleanup and refactor drawing code 31 | - ~~Add Close button~~ 32 | - Add Jdrop / HAR Storage links 33 | - ~~Flexible width~~ 34 | 35 | # To check 36 | 37 | - Why is the API sometimes unavailable in IE10? 38 | - "about:blank" in IE10 on http://t.uk.msn.com/ - valid 39 | 40 | # Browser Issues 41 | 42 | Here are the issues and queries found so far with the browser implementations of Resource Timing - no CORS testing so far. 43 | 44 | (Convert these to GH issues???) 45 | 46 | ## Chromium 47 | 48 | All of these issues are already marked as fixed on [crbug.com](http://crbug.com/) but waterfall code hasn't been tested against a recent build. 49 | 50 | - [Unexpected connectStart and connectEnd values for connections that are currently open](http://code.google.com/p/chromium/issues/detail?id=165897) 51 | - [Entries include dataURIs](http://code.google.com/p/chromium/issues/detail?id=165963&) 52 | - [Cross Origin resources have responseEnd of 0](http://code.google.com/p/chromium/issues/detail?id=166006) 53 | - [List of entries excludes resources retrieved from cache](http://code.google.com/p/chromium/issues/detail?id=166404) 54 | - [fetchStart times always equal startTime](http://code.google.com/p/chromium/issues/detail?id=166710) 55 | 56 | ## IE10 57 | 58 | (no link to issue tracker for these) 59 | 60 | - Resource Timing is only available in IE9/IE10 document modes but during testing API wasn't present for some sites where it previously worked and a restart seemed to fix it. _This needs re-checking to ensure it wasn't my error._ 61 | - connectStart == connectEnd for both Navigation and Resource Timing entries - would expect different values for early page requests where new TCP connections would be opened. 62 | - Frequently domainLookupStart == domainLookupEnd even for domains the Windows VM has never seen previously. 63 | - responseStart == responseEnd in Navigation Timing for both first and repeat views even when waterfall in dev tools shows a response 64 | 65 | ## Change Log 66 | 67 | 2013-01-01 Add JSDoc comments, start re-factoring drawing code 68 | -------------------------------------------------------------------------------- /waterfall.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Uses Resource Timing API to build a page load waterfall 3 | * 4 | * Only currently works in IE10, and Chromium nightly builds, has a few issues still to be fixed, 5 | * contributions welcomed! 6 | * 7 | * Feel free to do what you want with it, @andydavies 8 | * 9 | * To use, create a bookmark with the script below in, load a page, click the bookmark 10 | * 11 | * javascript:(function(){var el=document.createElement('script');el.type='text/javascript';el.src='http://andydavies.me/sandbox/waterfall.js';document.getElementsByTagName('body')[0].appendChild(el);})(); 12 | */ 13 | 14 | (function waterfall(w,d) { 15 | 16 | var xmlns = "http://www.w3.org/2000/svg"; 17 | 18 | var barColors = { 19 | blocked: "rgb(204, 204, 204)", 20 | thirdParty: "rgb(0, 0, 0)", 21 | redirect: "rgb(255, 221, 0)", 22 | appCache: "rgb(161, 103, 38)", 23 | dns: "rgb(48, 150, 158)", 24 | tcp: "rgb(255, 157, 66)", 25 | ssl: "rgb(213,102, 223)", 26 | request: "rgb(64, 255, 64)", 27 | response: "rgb(52, 150, 255)" 28 | } 29 | 30 | /** 31 | * Creates array of timing entries from Navigation and Resource Timing Interfaces 32 | * @returns {object[]} 33 | */ 34 | function getTimings() { 35 | 36 | var entries = []; 37 | 38 | // Page times come from Navigation Timing API 39 | entries.push(createEntryFromNavigationTiming()); 40 | 41 | // Other entries come from Resource Timing API 42 | var resources = []; 43 | 44 | if(w.performance.getEntriesByType !== undefined) { 45 | resources = w.performance.getEntriesByType("resource"); 46 | } 47 | else if(w.performance.webkitGetEntriesByType !== undefined) { 48 | resources = w.performance.webkitGetEntriesByType("resource"); 49 | } 50 | 51 | for(var n = 0; n < resources.length; n++) { 52 | if(resources[n].name.indexOf('/waterfall.js') === -1){ 53 | entries.push(createEntryFromResourceTiming(resources[n])); 54 | } 55 | } 56 | 57 | return entries; 58 | } 59 | 60 | /** 61 | * Creates an entry from a PerformanceResourceTiming object 62 | * @param {object} resource 63 | * @returns {object} 64 | */ 65 | function createEntryFromNavigationTiming() { 66 | 67 | var timing = w.performance.timing; 68 | 69 | // TODO: Add fetchStart and duration, fix TCP timings 70 | 71 | return { 72 | url: d.URL, 73 | start: 0, 74 | duration: timing.responseEnd - timing.navigationStart, 75 | redirectStart: timing.redirectStart === 0 ? 0 : timing.redirectStart - timing.navigationStart, 76 | redirectDuration: timing.redirectEnd - timing.redirectStart, 77 | appCacheStart: 0, // TODO 78 | appCacheDuration: 0, // TODO 79 | dnsStart: timing.domainLookupStart - timing.navigationStart, 80 | dnsDuration: timing.domainLookupEnd - timing.domainLookupStart, 81 | tcpStart: timing.connectStart - timing.navigationStart, 82 | tcpDuration: (timing.secureConnectionStart > 0 ? timing.secureConnectionStart : timing.connectEnd) - timing.connectStart, 83 | sslStart: timing.secureConnectionStart > 0 ? timing.secureConnectionStart - timing.navigationStart : 0, 84 | sslDuration: timing.secureConnectionStart > 0 ? timing.connectEnd - timing.secureConnectionStart : 0, 85 | requestStart: timing.requestStart - timing.navigationStart, 86 | requestDuration: timing.responseStart - timing.requestStart, 87 | responseStart: timing.responseStart - timing.navigationStart, 88 | responseDuration: timing.responseEnd - timing.responseStart 89 | } 90 | } 91 | 92 | /** 93 | * Creates an entry from a PerformanceResourceTiming object 94 | * @param {object} resource 95 | * @returns {object} 96 | */ 97 | function createEntryFromResourceTiming(resource) { 98 | 99 | // TODO: Add fetchStart and duration, fix TCP timings 100 | // NB 101 | // AppCache: start = fetchStart, end = domainLookupStart, connectStart or requestStart 102 | // TCP: start = connectStart, end = secureConnectionStart or connectEnd 103 | 104 | return { 105 | url: resource.name, 106 | start: resource.startTime, 107 | duration: resource.duration, 108 | redirectStart: resource.redirectStart, 109 | redirectDuration: resource.redirectEnd - resource.redirectStart, 110 | appCacheStart: 0, // TODO 111 | appCacheDuration: 0, // TODO 112 | dnsStart: resource.domainLookupStart, 113 | dnsDuration: resource.domainLookupEnd - resource.domainLookupStart, 114 | tcpStart: resource.connectStart, 115 | tcpDuration: (resource.secureConnectionStart > 0 ? resource.secureConnectionStart : resource.connectEnd) - resource.connectStart, 116 | sslStart: resource.secureConnectionStart > 0 ? resource.secureConnectionStart : 0, 117 | sslDuration: resource.secureConnectionStart > 0 ? resource.connectEnd - resource.secureConnectionStart : 0, 118 | requestStart: resource.requestStart, 119 | requestDuration: resource.responseStart - resource.requestStart, 120 | responseStart: resource.responseStart, 121 | // ??? - Chromium returns zero for responseEnd for 3rd party URLs, bug? 122 | responseDuration: resource.responseStart == 0 ? 0 : resource.responseEnd - resource.responseStart 123 | } 124 | } 125 | 126 | /** 127 | * Draw waterfall 128 | * @param {object[]} entries 129 | */ 130 | function drawWaterfall(entries) { 131 | 132 | var maxTime = 0; 133 | for(var n = 0; n < entries.length; n++) { 134 | maxTime = Math.max(maxTime, entries[n].start + entries[n].duration); 135 | } 136 | 137 | var containerID= "waterfall-div", 138 | container = d.getElementById(containerID), 139 | closeBtn = createCloseBtn(); 140 | 141 | if (container === null) { 142 | container = d.createElement('div'); 143 | container.id = containerID; 144 | } 145 | 146 | container.style.cssText = 'background:#fff;border: 2px solid #000;position:absolute;top:0;left:0;right:0;z-index:99999;margin:0px 8px;padding:0px;'; 147 | container.appendChild(closeBtn); 148 | d.body.appendChild(container); 149 | 150 | var rowHeight = 10; 151 | var rowPadding = 2; 152 | var barOffset = 200; 153 | 154 | //calculate size of chart 155 | // - max time 156 | // - number of entries 157 | var width = (window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth) - 16; 158 | var height = (entries.length + 1) * (rowHeight + rowPadding); // +1 for axis 159 | 160 | container.width = width; 161 | container.height = height; 162 | 163 | var svg = createSVG(width, height); 164 | 165 | // scale 166 | // TO DO - When to switch from seconds to milliseconds ??? 167 | var scaleFactor = maxTime / (width - 5 - barOffset); 168 | 169 | // draw axis 170 | var interval = 1000 / scaleFactor; 171 | var numberOfLines = maxTime / interval; 172 | var x1 = barOffset, 173 | y1 = rowHeight + rowPadding, 174 | y2 = height; 175 | 176 | for(var n = 0; n < numberOfLines; n++) { 177 | svg.appendChild(createSVGText(x1, 0, 0, rowHeight, "font: 10px sans-serif;", "middle", n)); 178 | svg.appendChild(createSVGLine(x1, y1, x1, y2, "stroke: #ccc;")); 179 | x1 += interval; 180 | } 181 | 182 | // draw resource entries 183 | for(var n = 0; n < entries.length; n++) { 184 | 185 | var entry = entries[n]; 186 | 187 | var row = createSVGGroup("translate(0," + (n + 1) * (rowHeight + rowPadding) + ")"); 188 | 189 | row.appendChild(createSVGText(5, 0, 0, rowHeight, "font: 10px sans-serif;", "start", shortenURL(entry.url))); 190 | 191 | row.appendChild(drawBar(entry, barOffset, rowHeight, scaleFactor)); 192 | 193 | svg.appendChild(row); 194 | // console.log(JSON.stringify(entry) + "\n" ); 195 | } 196 | 197 | container.appendChild(svg); 198 | } 199 | 200 | // TODO: Split out row, bar and axis drawing 201 | // drawAxis 202 | // drawRow() 203 | 204 | /** 205 | * Draw bar for resource 206 | * @param {object} entry Details of URL, and timings for individual resource 207 | * @param {int} barOffset Offset of the start of the bar along x axis 208 | * @param {int} rowHeight 209 | * @param {double} scaleFactor Factor used to scale down chart elements 210 | * @returns {element} SVG Group element containing bar 211 | * 212 | * TODO: Scale bar using SVG transform? - any accuracy issues? 213 | */ 214 | function drawBar(entry, barOffset, rowHeight, scaleFactor) { 215 | 216 | var bar = createSVGGroup("translate(" + barOffset + ", 0)"); 217 | 218 | // add tooltip 219 | var title = document.createElementNS(xmlns,"title") 220 | title.textContent = JSON.stringify(entry, function(key, value){ 221 | if (typeof value == "object") return value; 222 | // keep tooltip to just non-zero durations. 223 | if (!key.endsWith('Start') && key != 'url' && value != 0) 224 | return value.toFixed(1); 225 | }, ' '); 226 | bar.appendChild(title); 227 | 228 | bar.appendChild(createSVGRect(entry.start / scaleFactor, 0, entry.duration / scaleFactor, rowHeight, "fill:" + barColors.blocked)); 229 | 230 | // TODO: Test for 3rd party and colour appropriately 231 | 232 | if(entry.redirectDuration > 0) { 233 | bar.appendChild(createSVGRect(entry.redirectStart / scaleFactor , 0, entry.redirectDuration / scaleFactor, rowHeight, "fill:" + barColors.redirect)); 234 | } 235 | 236 | if(entry.appCacheDuration > 0) { 237 | bar.appendChild(createSVGRect(entry.appCacheStart / scaleFactor , 0, entry.appCacheDuration / scaleFactor, rowHeight, "fill:" + barColors.appCache)); 238 | } 239 | 240 | if(entry.dnsDuration > 0) { 241 | bar.appendChild(createSVGRect(entry.dnsStart / scaleFactor , 0, entry.dnsDuration / scaleFactor, rowHeight, "fill:" + barColors.dns)); 242 | } 243 | 244 | if(entry.tcpDuration > 0) { 245 | bar.appendChild(createSVGRect(entry.tcpStart / scaleFactor , 0, entry.tcpDuration / scaleFactor, rowHeight, "fill:" + barColors.tcp)); 246 | } 247 | 248 | if(entry.sslDuration > 0) { 249 | bar.appendChild(createSVGRect(entry.sslStart / scaleFactor , 0, entry.sslDuration / scaleFactor, rowHeight, "fill:" + barColors.ssl)); 250 | } 251 | 252 | if(entry.requestDuration > 0) { 253 | bar.appendChild(createSVGRect(entry.requestStart / scaleFactor , 0, entry.requestDuration / scaleFactor, rowHeight, "fill:" + barColors.request)); 254 | } 255 | 256 | if(entry.responseDuration > 0) { 257 | bar.appendChild(createSVGRect(entry.responseStart / scaleFactor , 0, entry.responseDuration / scaleFactor, rowHeight, "fill:" + barColors.response)); 258 | } 259 | 260 | return bar; 261 | } 262 | 263 | // drawBarSegment - start, length, height, fill 264 | 265 | /** 266 | * Shorten URLs over 40 characters 267 | * @param {string} url URL to be shortened 268 | * @returns {string} Truncated URL 269 | * 270 | * TODO: Remove protocol 271 | */ 272 | function shortenURL(url) { 273 | // Strip off any query string and fragment 274 | var strippedURL = url.match("[^?#]*") 275 | 276 | var shorterURL = strippedURL[0]; 277 | if(shorterURL.length > 40) { 278 | shorterURL = shorterURL.slice(0, 25) + " ... " + shorterURL.slice(-10); 279 | } 280 | 281 | return shorterURL; 282 | } 283 | 284 | /** 285 | *Create Close Button 286 | * returns {element} span element 287 | */ 288 | function createCloseBtn(){ 289 | var btnEle = d.createElement('span'); 290 | btnEle.innerHTML = 'x'; 291 | btnEle.style.cssText = 'position:absolute;margin:-3px 5px;right:0;font-size:22px;cursor:pointer'; 292 | addEvent(btnEle,'click',closeBtnHandler); 293 | return btnEle; 294 | } 295 | 296 | /** 297 | * Create SVG element 298 | * @param {int} width 299 | * @param {int} height 300 | * @returns {element} SVG element 301 | */ 302 | function createSVG(width, height) { 303 | var el = d.createElementNS(xmlns, "svg"); 304 | 305 | el.setAttribute("width", width); 306 | el.setAttribute("height", height); 307 | 308 | return el; 309 | } 310 | 311 | /** 312 | * Create SVG Group element 313 | * @param {string} transform SVG tranformation to apply to group element 314 | * @returns {element} SVG Group element 315 | */ 316 | function createSVGGroup(transform) { 317 | var el = d.createElementNS(xmlns, "g"); 318 | 319 | el.setAttribute("transform", transform); 320 | 321 | return el; 322 | } 323 | 324 | /** 325 | * Create SVG Rect element 326 | * @param {int} x 327 | * @param {int} y 328 | * @param {int} width 329 | * @param {int} height 330 | * @param {string} style 331 | * @returns {element} SVG Rect element 332 | */ 333 | function createSVGRect(x, y, width, height, style) { 334 | var el = d.createElementNS(xmlns, "rect"); 335 | 336 | el.setAttribute("x", x); 337 | el.setAttribute("y", y); 338 | el.setAttribute("width", width); 339 | el.setAttribute("height", height); 340 | el.setAttribute("style", style); 341 | 342 | return el; 343 | } 344 | 345 | /** 346 | * Create SVG Rect element 347 | * @param {int} x1 348 | * @param {int} y1 349 | * @param {int} x2 350 | * @param {int} y2 351 | * @param {string} style 352 | * @returns {element} SVG Line element 353 | */ 354 | function createSVGLine(x1, y1, x2, y2, style) { 355 | var el = d.createElementNS(xmlns, "line"); 356 | 357 | el.setAttribute("x1", x1); 358 | el.setAttribute("y1", y1); 359 | el.setAttribute("x2", x2); 360 | el.setAttribute("y2", y2); 361 | el.setAttribute("style", style); 362 | 363 | return el; 364 | } 365 | 366 | /** 367 | * Create SVG Text element 368 | * @param {int} x 369 | * @param {int} y 370 | * @param {int} dx 371 | * @param {int} dy 372 | * @param {string} style 373 | * @param {string} anchor 374 | * @param {string} text 375 | * @returns {element} SVG Text element 376 | */ 377 | function createSVGText(x, y, dx, dy, style, anchor, text) { 378 | var el = d.createElementNS(xmlns, "text"); 379 | 380 | el.setAttribute("x", x); 381 | el.setAttribute("y", y); 382 | el.setAttribute("dx", dx); 383 | el.setAttribute("dy", dy); 384 | el.setAttribute("style", style); 385 | el.setAttribute("text-anchor", anchor); 386 | 387 | el.appendChild(d.createTextNode(text)); 388 | 389 | return el; 390 | } 391 | 392 | /** 393 | * Event Handler for Close Button 394 | */ 395 | function closeBtnHandler(e){ 396 | var elem = d.getElementById("waterfall-div"); 397 | if(elem){ 398 | elem.parentNode.removeChild(elem); 399 | } 400 | } 401 | 402 | /** 403 | * Add Events On DOM Elements 404 | * @param {element} elem 405 | * @param {event} event 406 | * @param {function} fn 407 | * return {EventListener} listener that fires event 408 | */ 409 | function addEvent(elem, event, fn) { 410 | if (elem.addEventListener) { 411 | elem.addEventListener(event, fn, false); 412 | } else { 413 | elem.attachEvent("on" + event, function() { 414 | return(fn.call(elem, w.event)); 415 | }); 416 | } 417 | } 418 | 419 | // Check for Navigation Timing and Resource Timing APIs 420 | 421 | if(w.performance !== undefined && 422 | (w.performance.getEntriesByType !== undefined || 423 | w.performance.webkitGetEntriesByType !== undefined)) { 424 | 425 | var timings = getTimings(); 426 | 427 | drawWaterfall(timings); 428 | } 429 | else { 430 | alert("Resource Timing API not supported"); 431 | } 432 | })(window,window.document); 433 | --------------------------------------------------------------------------------