Slide 1

Slide 1 text

R O O M S E R V I C E & S PA S BayJax @ Intuit 2015-06-10

Slide 2

Slide 2 text

Philip Tellis @bluesmoon https://github.com/lognormal/boomerang

Slide 3

Slide 3 text

W H AT D O E S A PA G E L O O K L I K E O N T H E N E T W O R K ?

Slide 4

Slide 4 text

H O W D O D I F F E R E N T B R O W S E R S H A N D L E PA R A L L E L I Z AT I O N ?

Slide 5

Slide 5 text

W H I C H PA G E C O M P O N E N T S A F F E C T P E R C E I V E D L AT E N C Y ?

Slide 6

Slide 6 text

A R E A N Y O F T H E M S P O F S ? • Static JavaScript files, external CSS files • Anything that blocks onload if you have scripts that run on onload

Slide 7

Slide 7 text

N AV I G AT I O N T I M I N G P E R F O R M A N C E T I M I N G

Slide 8

Slide 8 text

N AV I G AT I O N T I M I N G AVA I L A B I L I T Y • IE >= 9 • FF >= 7 • Chrome >= 6 • Safari >= 8 • Opera >= 15 • Latest Android, Blackberry, Opera Mobile, Chrome for Android, Firefox for Android, IE Mobile

Slide 9

Slide 9 text

N AV I G AT I O N T I M I N G E X A M P L E var loadEventDuration = performance.timing.loadEventEnd - performance.timing.loadEventStart;

Slide 10

Slide 10 text

R E S O U R C E T I M I N G P E R F O R M A N C E T I M E L I N E

Slide 11

Slide 11 text

R E S O U R C E T I M I N G AVA I L A B I L I T Y • IE >= 10 • Chrome • Firefox >= 37 • Opera >= 16 • Latest Opera Mobile, Chrome for Android, IE Mobile

Slide 12

Slide 12 text

F U N W I T H R E S O U R C E T I M I N G • Generate a RUM waterfall • Or a performance heatmap 
 https://github.com/zeman/perfmap • Calculate a cache-hit-ratio per resource • Identify problem resources

Slide 13

Slide 13 text

L I M I TAT I O N S O F R E S O U R C E T I M I N G • Does not report on Domain Lookup, Connect, Timeout or Security errors • Inconsistent reporting of 4xx/5xx errors • Chrome: No ResourceTiming information • Firefox: All timestamps are non-zero but duration is 0 • Internet Explorer: All timestamps are non-zero • Doesn’t tell you if a response is a 304 or 200

Slide 14

Slide 14 text

C O R S • Cross-domain resources only tell you start & end time • Set the Timing-Allow-Origin: * response header on the resource to report all timings • Do this for resources served from your CDN or if you’re a 3rd party C R O S S - O R I G I N R E S O U R C E S H A R I N G

Slide 15

Slide 15 text

M E A S U R I N G X H R S function instrumentXHR() { var proxy_XMLHttpRequest, orig_XMLHttpRequest = window.XMLHttpRequest, readyStateMap; if (!orig_XMLHttpRequest) { // Nothing to instrument return; } readyStateMap = [ "uninitialized", "open", "responseStart", "domInteractive", "responseEnd" ]; // We could also inherit from window.XMLHttpRequest, but for this implementation, // we'll use composition proxy_XMLHttpRequest = function() { var req, perf = { timing: {}, resource: {} }, orig_open, orig_send; req = new orig_XMLHttpRequest; orig_open = req.open; orig_send = req.send; req.open = function(method, url, async) { if (async) { req.addEventListener('readystatechange', function() { perf.timing[readyStateMap[req.readyState]] = new Date().getTime(); }, false); } req.addEventListener('load', function() { perf.timing["loadEventEnd"] = new Date().getTime(); perf.resource.status = req.status; }, false); req.addEventListener('timeout', function() { perf.timing["timeout"] = new Date().getTime(); }, false); req.addEventListener('error', function() { perf.timing["error"] = new Date().getTime(); }, false); req.addEventListener('abort', function() { perf.timing["abort"] = new Date().getTime(); }, false); perf.resource.name = url; perf.resource.method = method; // call the original open method return orig_open.apply(req, arguments); }; req.send = function() { perf.timing["requestStart"] = new Date().getTime(); // call the original send method return orig_send.apply(req, arguments); }; req.performance = perf; return req; }; window.XMLHttpRequest = proxy_XMLHttpRequest; }

Slide 16

Slide 16 text

function instrumentXHR { var proxy_XMLHttpRequest orig_XMLHttpRequest readyStateMap if (!orig_XMLHttpRequest // Nothing to instrument return } readyStateMap // We could also inherit from window.XMLHttpRequest, but for this implementation, // we'll use composition proxy_XMLHttpRequest var req orig_open orig_send req req perf req perf perf req req req perf perf }; req perf }; req return }; window.XMLHttpRequest } M E A S U R I N G X H R S TL;DR: Proxy XMLHttpRequest Intercept open() & send() and capture events

Slide 17

Slide 17 text

A C T U A L I M P L E M E N TAT I O N I S M O R E C O M P L I C AT E D , W H AT F O L L O W S I S S I M P L I F I E D , B U T R E P R E S E N TAT I V E WA R N I N G ! S I M P L I F I E D C O D E F O L L O W S Complete code at https://github.com/lognormal/boomerang/blob/master/plugins/auto_xhr.js

Slide 18

Slide 18 text

L E T ’ S B R E A K I T D O W N — P R O X Y var orig_XMLHttpRequest = window.XMLHttpRequest; var proxy_XMLHttpRequest = function() { var req, resource = { timing: {}, initiator: “xhr” }, orig_open, orig_send; req = new orig_XMLHttpRequest(); orig_open = req.open; orig_send = req.send; // define new open // define new send req.resource = resource; return req; }; window.XMLHttpRequest = proxy_XMLHttpRequest;

Slide 19

Slide 19 text

L E T ’ S B R E A K I T D O W N — S E N D req.send = function() { resource.timing.requestStart = BOOMR.now(); // call the original send method return orig_send.apply(req, arguments); };

Slide 20

Slide 20 text

L E T ’ S B R E A K I T D O W N — O P E N var a = document.createElement(“A”) req.open = function(method, url, async) { a.href = url; // resolve relative URLs // Default value of async is true if (async === undefined) { async = true; } if (async) { // add listener on readystatechange } // add listeners on load, timeout, error & abort resource.url = a.href; resource.method = method; // call the original open method return orig_open.apply(req, arguments); };

Slide 21

Slide 21 text

R E A D Y S TAT E C H A N G E F O R A S Y N C R E Q U E S T S • 0 == uninitialized • 1 == open • 2 == responseStart • 3 == domInteractive • 4 == responseEnd

Slide 22

Slide 22 text

B U T W E C A N D O M O R E O N M O D E R N B R O W S E R S res = performance.getEntriesByName(a.href); /* res.redirectStart, res.redirectEnd, res.fetchStart, res.domainLookupStart, res.domainLookupEnd, res.connectStart, res.connectEnd, res.requestStart, res.responseStart, res.responseEnd */

Slide 23

Slide 23 text

Thank You

Slide 24

Slide 24 text

Thank You But wait, there’s more…

Slide 25

Slide 25 text

I T ’ S 2 0 1 5 . T H E S I N S PA D O E S N O T S TA N D F O R S I M P L E !

Slide 26

Slide 26 text

• The initial XHR fetches XML or JSON • This may reference Images, styles or JS Modules that need to be fetched • The combination of all these resources initiated by a user action is what we really want to measure

Slide 27

Slide 27 text

E N T E R T H E 
 M U TAT I O N O B S E R V E R

Slide 28

Slide 28 text

M U TAT I O N O B S E R V E R var o = {observer: null, timer: null}; function done(mutations) { if (o.timer) { clearTimeout(o.timer); o.timer = null; } if (callback) { callback.call(callback_ctx, mutations, callback_data); callback = null; } if (o.observer) { o.observer.disconnect(); o.observer = null; } } o.observer = new MutationObserver(done); if (timeout) { o.timer = setTimeout(done, o.timeout); } o.observer.observe(el, config);

Slide 29

Slide 29 text

The rest of the code is complicated, you can see it at https://github.com/lognormal/boomerang/blob/master/plugins/auto_xhr.js but tl;dr…

Slide 30

Slide 30 text

M U TAT I O N O B S E R V E R • A MutationObserver listens for DOM mutations • Start listening when an XHR returns; set a short timeout • Attach load and error event handlers to and set timeouts on any IMG, SCRIPT, LINK & IFRAME nodes. • Cached resources may not fire events at all, so also check ResourceTiming • The load event does not tell you when the element became visible

Slide 31

Slide 31 text

M U TAT I O N O B S E R V E R S U P P O R T

Slide 32

Slide 32 text

I T G E T S M O R E C O M P L I C AT E D W H E N 2 + X H R S S TA R T C O N C U R R E N T LY

Slide 33

Slide 33 text

We could also watch the history.popstate event

Slide 34

Slide 34 text

We could have stopped here, but…

Slide 35

Slide 35 text

S PA F R A M E W O R K S • Angular.js • Backbone.js • Ember.js • React.js • It’s Wednesday, here’s a new framework.js

Slide 36

Slide 36 text

A N G U L A R . J S 1. Listen for Angular.js routing events like $routeChangeStart 2. Call out to boomerang in your Angular app angular.module("app") .run(["$rootScope", function($rootScope) { var hadRouteChange = false; $rootScope.$on("$routeChangeStart", function() { hadRouteChange = true; }); function hookAngularBoomerang() { if (window.BOOMR && BOOMR.version) { if (BOOMR.plugins && BOOMR.plugins.Angular) { BOOMR.plugins.Angular.hook($rootScope, hadRouteChange); } return true; } } if (!hookAngularBoomerang()) { document.addEventListener("onBoomerangLoaded", hookAngularBoomerang); } }]);

Slide 37

Slide 37 text

O T H E R F R A M E W O R K S A R E S I M I L A R

Slide 38

Slide 38 text

T H E E N D R E S U LT I S V I S I B L E I N M P U L S E

Slide 39

Slide 39 text

C AV E AT A B O U T T E S T I N G W I N D O W. P E R F O R M A N C E • On Firefox 31, checking window.performance in an anonymous iframe throws an exception • So we tried:
 if (“performance” in window) {}

Slide 40

Slide 40 text

C AV E AT A B O U T T E S T I N G W I N D O W. P E R F O R M A N C E • But jslint complains about that • So we switched to:
 if (window.hasOwnProperty(“performance")) {
 // I know right?
 }

Slide 41

Slide 41 text

C AV E AT A B O U T T E S T I N G W I N D O W. P E R F O R M A N C E • Which does not work on Internet Explorer 10+!# • So we ended up with:
 try {
 if ("performance" in window && window.performance)
 ...
 }
 catch(e) {
 // WTF
 }

Slide 42

Slide 42 text

Thank You For real this time :)