Skip to content

Instantly share code, notes, and snippets.

@chrisirhc
Created March 3, 2012 00:41
Show Gist options
  • Save chrisirhc/1962987 to your computer and use it in GitHub Desktop.
Save chrisirhc/1962987 to your computer and use it in GitHub Desktop.
PPS
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.
// ==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