Created
March 3, 2012 00:41
-
-
Save chrisirhc/1962987 to your computer and use it in GitHub Desktop.
PPS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Frontend Coding Challenge | |
=========================== | |
ThousandEyes Frontend Coding Challenge: | |
Build a piece of javascript code that can be inserted in any webpage (e.g. using greasemonkey) and computes a Page Performance Score (PPS) for that page. | |
The PPS is loosely defined as a value between 0 and 100, PPS=100 being a super fast page with no errors and PPS=0 a very slow page with lots of errors. PPS also includes ajax/iframe requests that happen during or after the page is loaded. Some factors to include in PPS are: | |
- being able to tell if the page loaded or not | |
- measure dom load and page load of the page | |
- measure number of components that did not load properly (e.g. images that did not load, css files that did not load, ajax calls that failed, iframes with broken links) | |
- measure time it takes to load each ajax request and iframe (even if they happen in parallel with the page load, or after the page is loaded) | |
You can implement only a subset of the above and come up with additional metrics. | |
Ideally the js code should only be included in a single place in the page (in the <head> of the page). | |
Jquery is encouraged but extra bonus points if this can be done without using it. Extra bonus points if the code works in all browsers (FF, Chrome and Safari, IE optional). | |
Resources: | |
* Greasemonkey (need to install from latest code on github in order to work in mac osx w/ FF 7) | |
http://wiki.greasespot.net/Greasemonkey_Manual:Editing | |
https://github.com/greasemonkey/greasemonkey | |
Personal notes | |
============== | |
// Useful for checking | |
http://stevesouders.com/cuzillion | |
Run YSlow after the speed is there. | |
Note that Greasemonkey scripts load at the DOMContentLoaded event. | |
(http://wiki.greasespot.net/DOMContentLoaded) | |
Check for everything I need at http://microjs.com/ . | |
YSlow Bookmarklet: | |
javascript:(function(y,p,o){p=y.body.appendChild(y.createElement('iframe'));p.id='YSLOW-bookmarklet';p.style.cssText='display:none';o=p.contentWindow.document;o.open().write('<head><body onload="YUI_config={win:window.parent,doc:window.parent.document};var d=document;d.getElementsByTagName(\'head\')[0].appendChild(d.createElement(\'script\')).src=\'http://d.yimg.com/jc/yslow-bookmarklet.js\'">');o.close()}(document)) | |
Own version of YSlow | |
javascript:(function(y,p,o){p=y.body.appendChild(y.createElement('iframe'));p.id='YSLOW-bookmarklet';p.style.cssText='display:none';o=p.contentWindow.document;o.open().write('<head><body onload="YUI_config={win:window.parent,doc:window.parent.document};var d=document;d.getElementsByTagName(\'head\')[0].appendChild(d.createElement(\'script\')).src=\'http://localhost:8000/yslow-bookmarklet.js\'">');o.close()}(document)) | |
Read through jiffy and episodeJS. | |
Hooking into XMLHttpRequest | |
- http://www.ilinsky.com/articles/XMLHttpRequest/ | |
- https://github.com/ilinsky/xmlhttprequest | |
- http://stackoverflow.com/questions/629671/how-can-i-intercept-xmlhttprequests-from-a-greasemonkey-script | |
- http://www.quora.com/JavaScript/Whats-the-best-way-to-create-an-XMLHttpRequest-wrapper-proxy | |
- http://oreilly.com/pub/h/4133 | |
Testing | |
Try loading the following: http://500px.com/photo/2309581 | |
Minimizer | |
http://marijnhaverbeke.nl/uglifyjs | |
PostMessage support across browsers | |
http://caniuse.com/#feat=x-doc-messaging | |
http://www.samdutton.com/navigationTiming/ | |
Known Issues | |
============ | |
Problems when a frame reloads. Multiple frames with the same url. | |
Hooks in the AJAX severely slows down realtime applications such as Google Instant. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name PPS | |
// @namespace http://github.com/chrisirhc/ | |
// @include * | |
// @exclude http://www.google.com/* | |
// ==/UserScript== | |
var PPS = {}; | |
// Expose it | |
window.PPS = PPS; | |
PPS.q = []; | |
// Start with 100 and decrease it | |
PPS.rawScore = 100; | |
PPS.THRESHOLDTIME = 3000; | |
// Since normal greasemonkey DOM does not allow access to prototype of objects | |
// http://wiki.greasespot.net/Content_Script_Injection | |
function contentEval(source) { | |
// Check for function input. | |
if ('function' === typeof source) { | |
// Execute this function with no arguments, by adding parentheses. | |
// One set around the function, required for valid syntax, and a | |
// second empty set calls the surrounded function. | |
source = '(' + source + ')();'; | |
} | |
// Create a script node holding this source code. | |
var script = document.createElement('script'); | |
// script.setAttribute("type", "application/javascript"); | |
script.type = "application/javascript"; | |
script.async = true; | |
script.textContent = source; | |
// Insert the script node into the page, so it will run, and immediately | |
// remove it to clean up. | |
document.body.appendChild(script); | |
document.body.removeChild(script); | |
} | |
// Modified from EPISODES | |
// Wrapper for addEventListener and attachEvent. | |
if ( "undefined" !== typeof(window.attachEvent) ) { | |
PPS.addEventListener = function(source, sType, callback) { | |
return source.attachEvent("on" + sType, callback); | |
}; | |
} else if ( window.addEventListener ) { | |
PPS.addEventListener = function(source, sType, callback) { | |
return source.addEventListener(sType, callback, false); | |
}; | |
} else { | |
console.log("PPS won't work on this browser"); | |
} | |
if (window.top !== window.self) { | |
// If it's not the top window send the messages | |
PPS.mark = function (markName, markTime, markSrc) { | |
window.top.postMessage( | |
[ "mark", markName, markTime || new Date().getTime(), markSrc || "", document.location.toString() ], | |
"*"); | |
}; | |
} else { | |
// If it's the not the top window add straight to queue | |
PPS.mark = function (markName, markTime, markSrc) { | |
PPS.q.push( [ "mark", markName, markTime || new Date().getTime() , markSrc || ""] ); | |
}; | |
PPS.addEventListener(window, "message", PPS.messageHandlerBeforeLoad); | |
} | |
PPS.instrumentElement = function (loadableName, i) { | |
return function() { | |
PPS.mark(loadableName + " " + i + ""); | |
}; | |
}; | |
PPS.errorElement = function (loadableName, i) { | |
return function() { | |
PPS.mark(loadableName + " " + i + ""); | |
// Probably decrease score | |
}; | |
}; | |
// Instrumentation of all the elements in the page | |
PPS.instrumentation = function() { | |
// My own version of a peeler | |
var loadables = [ | |
["Image", document.images], | |
["Script", document.scripts], | |
["IFrame", document.getElementsByTagName("iframe")], | |
["Frame", document.getElementsByTagName("frame")] | |
]; | |
var elementType; | |
var elementList; | |
for (var j = 0; j < loadables.length; j++) { | |
elementType = loadables[j][0]; | |
elementList = loadables[j][1]; | |
for (var i = 0; i < elementList.length; i++) { | |
PPS.addEventListener(elementList[i], "load", PPS.instrumentElement(elementType, i)); | |
PPS.addEventListener(elementList[i], "error", PPS.errorElement(elementType, i)); | |
} | |
} | |
}; | |
PPS.messageHandlerBeforeLoad = function (e) { | |
// process later | |
PPS.q.push(e.data); | |
}; | |
PPS.messageHandlerAfterLoad = function (e) { | |
if ("yslowscore" === e.data[0]) { | |
var yslowScore = e.data[1]; | |
PPS.yslowScore.nodeValue = yslowScore.toFixed(0); | |
PPS.textScore.nodeValue = (parseFloat(yslowScore) / 2.0 + PPS.rawScore / 2.0).toFixed(0); | |
// document.getElementById('YSLOW-bookmarklet').style.display = 'none'; | |
} | |
}; | |
PPS.addEventListener(window, "error", function () { | |
console.log("error found!"); | |
}); | |
// Hooks to XMLHttpRequest | |
PPS.hookXHR = function () { | |
(function(open) { | |
XMLHttpRequest.prototype.open = function(method, url, async, user, pass) { | |
this.addEventListener("readystatechange", function() { | |
}, false); | |
open.call(this, method, url, async, user, pass); | |
}; | |
})(XMLHttpRequest.prototype.open); | |
}; | |
// Call to insert YSlow into page | |
PPS.insertYSLOW = function () { | |
(function(y, p, o){ | |
p=y.getElementById('YSLOW-bookmarklet'); | |
o=p.contentWindow.document; | |
o.open().write('<head><body onload="YUI_config={win:window.parent,doc:window.parent.document};' + | |
'var d=document;' + | |
'var script = d.createElement(\'script\');' + | |
'script.src=\'http://chrisirhc.github.com/yslow/pkg/bookmarklet/dev/yslow-bookmarklet.js\';' + | |
'script.onload=function() {' + | |
'document.ysview.setAntiIframe(true);' + | |
'YSLOW.util.event.addListener(\'componentFetchDone\', function () {' + | |
'window.top.postMessage([\'yslowscore\', document.ysview.yscontext.result_set.overall_score], \'*\');' + | |
'});' + | |
'YSLOW.controller.run();' + | |
'};' + | |
'd.getElementsByTagName(\'head\')[0].appendChild(script);' + | |
'">' | |
); | |
o.close(); | |
})(document); | |
}; | |
PPS.onLoad = function () { | |
// Decorate with information from Navigation Timing API | |
var performance = window.performance || window.mozPerformance || window.msPerformance || window.webkitPerformance; | |
if ( "undefined" !== typeof(performance) && "undefined" !== typeof(performance.timing) && "undefined" !== typeof(performance.timing.navigationStart) ) { | |
// TODO: Only use a few later | |
for (var performanceVar in performance.timing) { | |
if (performance.timing[performanceVar] > 0) { | |
// TODO mark start and end | |
PPS.mark(performanceVar, performance.timing[performanceVar]); | |
} | |
} | |
} | |
// Draw out the performance | |
var visualizer = document.createElement("div"); | |
visualizer.style.bottom = "5px"; | |
visualizer.style.left = "0"; | |
visualizer.style.position = "fixed"; | |
visualizer.style.background = "rgba(0,0,0,0.25)"; | |
visualizer.style.height = "30px"; | |
visualizer.style.width = "100%"; | |
visualizer.style["z-index"] = "-100"; | |
document.body.appendChild(visualizer); | |
var marks = []; | |
PPS.q.forEach(function(i) { | |
// TODO check | |
if (Object.prototype.toString.call(i) === "[object Array]" && i[0] === "mark") { | |
marks.push(i); | |
} | |
}); | |
marks.sort(function(a, b) { | |
if ( a[2] < b[2] ) { | |
return -1; | |
} else if ( a[2] === b[2] ) { | |
return 0; | |
} | |
return 1; | |
}); | |
var start = marks[0][2]; | |
var end = marks[marks.length-1][2]; | |
var l = end - start; | |
var topOffset = 0; | |
var block; | |
// Larger than 3s load time | |
if (l > 3000) { | |
block = document.createElement("div"); | |
block.style.position = "absolute"; | |
block.style.background = "rgba(0,0,0,0.5)"; | |
block.style.height = "100%"; | |
// console.log(((marks[i][2] - start)/ l * 100)); | |
block.style.left = (3000 / l * 100).toPrecision(3) + "%"; | |
block.style.right = 0 + "px"; | |
block.style.top = 0 + "px"; | |
block.style.color = "white"; | |
var text = document.createTextNode("More than 3s >"); | |
block.appendChild(text); | |
visualizer.appendChild(block); | |
} | |
var timeTaken; | |
for (var i = 0; i < marks.length; i++) { | |
block = document.createElement("div"); | |
block.style.position = "absolute"; | |
block.style.background = "green"; | |
block.style.height = "2px"; | |
block.style.width = "2px"; | |
// console.log(((marks[i][2] - start)/ l * 100)); | |
block.style.left = ((marks[i][2] - start)/ l * 100).toPrecision(3) + "%"; | |
block.style.top = topOffset + "px"; | |
topOffset += 2; | |
visualizer.appendChild(block); | |
timeTaken = marks[i][2] - start - PPS.THRESHOLDTIME; | |
if (PPS.rawScore > 0 && timeTaken > 0) { | |
// Arbitrary way of decreasing score | |
PPS.rawScore -= timeTaken / 1000; | |
} | |
} | |
visualizer.style.height = topOffset + 20 + "px"; | |
var textScoreBlock = document.createElement("div"); | |
PPS.textScore = document.createTextNode("Loading..."); | |
textScoreBlock.appendChild(document.createTextNode("Live Performance Score: " + PPS.rawScore + " / 100 | ")); | |
var openYSlow = document.createElement("a"); | |
openYSlow.style.color = "white"; | |
openYSlow.href = "#"; | |
openYSlow.title = "Open YSlow"; | |
openYSlow.appendChild(document.createTextNode("YSlow Score: ")); | |
PPS.yslowScore = openYSlow.appendChild(document.createTextNode("Loading...")); | |
openYSlow.appendChild(document.createTextNode(" / 100")); | |
openYSlow.onclick = function () { | |
PPS.yslowcontainer.style.display = "block"; | |
visualizer.style.bottom = window.innerHeight / 2 + "px"; | |
textScoreBlock.style.bottom = window.innerHeight / 2 + topOffset + 20 + "px"; | |
return false; | |
}; | |
textScoreBlock.appendChild(openYSlow); | |
textScoreBlock.appendChild(document.createTextNode(" | Overall Score: ")); | |
textScoreBlock.appendChild(PPS.textScore); | |
textScoreHeight = 20; | |
textScoreBlock.style.bottom = (topOffset + 5) + textScoreHeight + "px"; | |
textScoreBlock.style.fontSize = "18px"; | |
textScoreBlock.style.textAlign = "center"; | |
textScoreBlock.style.color = "white"; | |
textScoreBlock.style.left = "0"; | |
textScoreBlock.style.position = "fixed"; | |
textScoreBlock.style.background = "rgba(0,0,0,0.75)"; | |
textScoreBlock.style.height = textScoreHeight + "px"; | |
textScoreBlock.style.width = "100%"; | |
textScoreBlock.style["zIndex"] = "100"; | |
document.body.appendChild(textScoreBlock); | |
PPS.yslowcontainer = document.createElement("div"); | |
PPS.yslowcontainer.style.display = "none"; | |
document.body.appendChild(PPS.yslowcontainer); | |
PPS.yslowframe = PPS.yslowcontainer.appendChild(document.createElement('iframe')); | |
PPS.yslowframe.id='YSLOW-bookmarklet'; | |
// TODO support for no postMessage | |
window.removeEventListener("message", PPS.messageHandlerBeforeLoad); | |
window.addEventListener("message", PPS.messageHandlerAfterLoad, false); | |
// Run YSLOW and get score | |
if (typeof(PPS.yslowframe.contentWindow) === "undefined") { | |
console.log("loaded"); | |
contentEval(PPS.insertYSLOW); | |
} else { | |
PPS.insertYSLOW(); | |
} | |
}; | |
PPS.instrumentation(); | |
// Add hooks | |
contentEval(PPS.hookXHR); | |
// TODO check whether this should be unsafewindow | |
// TODO ensure that this still runs even after onload event has fired. | |
// Only do this for top window | |
if (window.top === window.self) { | |
PPS.addEventListener(window, "load", PPS.onLoad); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment