blob: 01905bde986efad4c680a195e3d2a6b4b3b034f4 [file] [log] [blame]
(function(window, document) {
/** Cache used by various methods */
var cache = {
'counter': 0,
'lastAction': 'load',
'lastChart': 'bar',
'lastFilterBy': 'all',
'responses': { /* 'all': null, 'desktop': null, 'major': null, ... */ },
'timers': { /* 'cleanup': null, 'load': null, 'post': null, ... */ },
'trash': createElement('div')
};
/**
* Used to filter Browserscope results by browser category.
*
* @see http://www.browserscope.org/user/tests/howto#urlparams
*/
var filterMap = {
'all': 3,
'desktop': 'top-d',
'family': 0,
'major': 1,
'minor': 2,
'mobile': 'top-m',
'popular': 'top',
'prerelease': 'top-d-e'
};
/** Used to resolve a value's internal [[Class]] */
var toString = {}.toString;
/**
* The `uaToken` is prepended to the value of the data cell of the Google
* visualization data table object that matches the user's browser name. After
* the chart is rendered the element containing the `uaToken` is assigned the
* `ui.browserscope.uaClass` class name to allow for the creation of a visual
* indicator to help the user more easily find their browser's results.
*/
var uaToken = '\u2028';
/** Math shortcuts */
var floor = Math.floor,
max = Math.max,
min = Math.min;
/** Utility shortcuts */
var each = Benchmark.each,
extend = Benchmark.extend,
filter = Benchmark.filter,
forOwn = Benchmark.forOwn,
formatNumber = Benchmark.formatNumber,
hasKey = Benchmark.hasKey,
indexOf = Benchmark.indexOf,
interpolate = Benchmark.interpolate,
invoke = Benchmark.invoke,
map = Benchmark.map,
reduce = Benchmark.reduce;
/*--------------------------------------------------------------------------*/
/**
* Registers an event listener.
*
* @private
* @param {Element} element The element.
* @param {String} eventName The name of the event to listen to.
* @param {Function} handler The event handler.
* @returns {Element} The element.
*/
function addListener(element, eventName, handler) {
if ((element = typeof element == 'string' ? query(element)[0] : element)) {
if (typeof element.addEventListener != 'undefined') {
element.addEventListener(eventName, handler, false);
} else if (typeof element.attachEvent != 'undefined') {
element.attachEvent('on' + eventName, handler);
}
}
return element;
}
/**
* Shortcut for `document.createElement()`.
*
* @private
* @param {String} tagName The tag name of the element to create.
* @param {String} name A name to assign to the element.
* @param {Document|Element} context The document object used to create the element.
* @returns {Element} Returns a new element.
*/
function createElement(tagName, name, context) {
var result;
name && name.nodeType && (context = name, name = 0);
context = context ? context.ownerDocument || context : document;
name || (name = '');
try {
// set name attribute for IE6/7
result = context.createElement('<' + tagName + ' name="' + name + '">');
} catch(e) {
(result = context.createElement(tagName)).name = name;
}
return result;
}
/**
* Creates a new style element.
*
* @private
* @param {String} cssText The css text of the style element.
* @param {Document|Element} context The document object used to create the element.
* @returns {Element} Returns the new style element.
*/
function createStyleSheet(cssText, context) {
// use a text node, "x", to work around innerHTML issues with style elements
// http://msdn.microsoft.com/en-us/library/ms533897(v=vs.85).aspx#1
var div = createElement('div', context);
div.innerHTML = 'x<style>' + cssText + '</style>';
return div.lastChild;
}
/**
* Gets the text content of an element.
*
* @private
* @param {Element} element The element.
* @returns {String} The text content of the element.
*/
function getText(element) {
element = query(element)[0];
return element && (element.textContent || element.innerText) || '';
}
/**
* Injects a script into the document.
*
* @private
* @param {String} src The external script source.
* @param {Object} sibling The element to inject the script after.
* @param {Document} context The document object used to create the script element.
* @returns {Object} The new script element.
*/
function loadScript(src, sibling, context) {
context = sibling ? sibling.ownerDocument || [sibling, sibling = 0][0] : context;
var script = createElement('script', context),
nextSibling = sibling ? sibling.nextSibling : query('script', context).pop();
script.src = src;
return (sibling || nextSibling).parentNode.insertBefore(script, nextSibling);
}
/**
* Queries the document for elements by id or tagName.
*
* @private
* @param {String} selector The css selector to match.
* @param {Document|Element} context The element whose descendants are queried.
* @returns {Array} The array of results.
*/
function query(selector, context) {
var result = [];
selector || (selector = '');
context = typeof context == 'string' ? query(context)[0] : context || document;
if (selector.nodeType) {
result = [selector];
}
else if (context) {
each(selector.split(','), function(selector) {
each(/^#/.test(selector)
? [context.getElementById(selector.slice(1))]
: context.getElementsByTagName(selector), function(node) {
result.push(node);
});
});
}
return result;
}
/**
* Set an element's innerHTML property.
*
* @private
* @param {Element} element The element.
* @param {String} html The HTML to set.
* @param {Object} object The template object used to modify the html.
* @returns {Element} The element.
*/
function setHTML(element, html, object) {
if ((element = query(element)[0])) {
element.innerHTML = interpolate(html, object);
}
return element;
}
/**
* Displays a message in the "results" element.
*
* @private
* @param {String} text The text to display.
* @param {Object} object The template object used to modify the text.
*/
function setMessage(text, object) {
var me = ui.browserscope,
cont = me.container;
if (cont) {
cont.className = 'bs-rt-message';
setHTML(cont, text, object);
}
}
/*--------------------------------------------------------------------------*/
/**
* Adds a style sheet to the current chart and assigns the `ui.browserscope.uaClass`
* class name to the chart element containing the user's browser name.
*
* @private
* @returns {Boolean} Returns `true` if the operation succeeded, else `false`.
*/
function addChartStyle() {
var me = ui.browserscope,
cssText = [],
context = frames[query('iframe', me.container)[0].name].document,
chartNodes = query('text,textpath', context),
uaClass = me.uaClass,
result = false;
if (chartNodes.length) {
// extract CSS rules for `uaClass`
each(query('link,style'), function(node) {
// avoid access denied errors on external style sheets
// outside the same origin policy
try {
var sheet = node.sheet || node.styleSheet;
each(sheet.cssRules || sheet.rules, function(rule) {
if ((rule.selectorText || rule.cssText).indexOf('.' + uaClass) > -1) {
cssText.push(rule.style && rule.style.cssText || /[^{}]*(?=})/.exec(rule.cssText) || '');
}
});
} catch(e) { }
});
// insert custom style sheet
query('head', context)[0].appendChild(
createStyleSheet('.' + uaClass + '{' + cssText.join(';') + '}', context));
// scan chart elements for a match
each(chartNodes, function(node) {
var nextSibling;
if ((node.string || getText(node)).charAt(0) == uaToken) {
// for VML
if (node.string) {
// IE requires reinserting the element to render correctly
node.className = uaClass;
nextSibling = node.nextSibling;
node.parentNode.insertBefore(node.removeNode(), nextSibling);
}
// for SVG
else {
node.setAttribute('class', uaClass);
}
result = true;
}
});
}
return result;
}
/**
* Periodically executed callback that removes injected script and iframe elements.
*
* @private
*/
function cleanup() {
var me = ui.browserscope,
timings = me.timings,
timers = cache.timers,
trash = cache.trash,
delay = timings.cleanup * 1e3;
// remove injected scripts and old iframes when benchmarks aren't running
if (timers.cleanup && !ui.running) {
// if expired, destroy the element to prevent pseudo memory leaks.
// http://dl.dropbox.com/u/513327/removechild_ie_leak.html
each(query('iframe,script'), function(element) {
var expire = +(/^browserscope-\d+-(\d+)$/.exec(element.name) || 0)[1] + max(delay, timings.timeout * 1e3);
if (new Date > expire || /browserscope\.org|google\.com/.test(element.src)) {
trash.appendChild(element);
trash.innerHTML = '';
}
});
}
// schedule another round
timers.cleanup = setTimeout(cleanup, delay);
}
/**
* A simple data object cloning utility.
*
* @private
* @param {Mixed} data The data object to clone.
* @returns {Mixed} The cloned data object.
*/
function cloneData(data) {
var fn,
ctor,
result = data;
if (isArray(data)) {
result = map(data, cloneData);
}
else if (data === Object(data)) {
ctor = data.constructor;
result = ctor == Object ? {} : (fn = function(){}, fn.prototype = ctor.prototype, new fn);
forOwn(data, function(value, key) {
result[key] = cloneData(value);
});
}
return result;
}
/**
* Creates a Browserscope results object.
*
* @private
* @returns {Object|Null} Browserscope results object or null.
*/
function createSnapshot() {
// clone benches, exclude those that are errored, unrun, or have hz of Infinity
var benches = invoke(filter(ui.benchmarks, 'successful'), 'clone'),
fastest = filter(benches, 'fastest'),
slowest = filter(benches, 'slowest'),
neither = filter(benches, function(bench) {
return indexOf(fastest, bench) + indexOf(slowest, bench) == -2;
});
function merge(destination, source) {
destination.count = source.count;
destination.cycles = source.cycles;
destination.hz = source.hz;
destination.stats = extend({}, source.stats);
}
// normalize results on slowest in each category
each(fastest.concat(slowest), function(bench) {
merge(bench, indexOf(fastest, bench) > -1 ? fastest[fastest.length - 1] : slowest[0]);
});
// sort slowest to fastest
// (a larger `mean` indicates a slower benchmark)
neither.sort(function(a, b) {
a = a.stats; b = b.stats;
return (a.mean + a.moe > b.mean + b.moe) ? -1 : 1;
});
// normalize the leftover benchmarks
reduce(neither, function(prev, bench) {
// if the previous slower benchmark is indistinguishable from
// the current then use the previous benchmark's values
if (prev.compare(bench) == 0) {
merge(bench, prev);
}
return bench;
});
// append benchmark ids for duplicate names or names with no alphanumeric/space characters
// and use the upper limit of the confidence interval to compute a lower hz
// to avoid recording inflated results caused by a high margin or error
return reduce(benches, function(result, bench, key) {
var stats = bench.stats;
result || (result = {});
key = toLabel(bench.name);
result[key && !hasKey(result, key) ? key : key + bench.id ] = floor(1 / (stats.mean + stats.moe));
return result;
}, null);
}
/**
* Retrieves the "cells" array from a given Google visualization data row object.
*
* @private
* @param {Object} object The data row object.
* @returns {Array} An array of cell objects.
*/
function getDataCells(object) {
// resolve cells by duck typing because of munged property names
var result = [];
forOwn(object, function(value) {
return !(isArray(value) && (result = value));
});
// remove empty entries which occur when not all the tests are recorded
return filter(result, Boolean);
}
/**
* Retrieves the "labels" array from a given Google visualization data table object.
*
* @private
* @param {Object} object The data table object.
* @returns {Array} An array of label objects.
*/
function getDataLabels(object) {
var result = [],
labelMap = {};
// resolve labels by duck typing because of munged property names
forOwn(object, function(value) {
return !(isArray(value) && 0 in value && 'type' in value[0] && (result = value));
});
// create a data map of labels to names
each(ui.benchmarks, function(bench) {
var key = toLabel(bench.name);
labelMap[key && !hasKey(labelMap, key) ? key : key + bench.id ] = bench.name;
});
// replace Browserscope's basic labels with benchmark names
return each(result, function(cell) {
var name = labelMap[cell.label];
name && (cell.label = name);
});
}
/**
* Retrieves the "rows" array from a given Google visualization data table object.
*
* @private
* @param {Object} object The data table object.
* @returns {Array} An array of row objects.
*/
function getDataRows(object) {
var name,
filterBy = cache.lastFilterBy,
browserName = toBrowserName(getText(query('strong', '#bs-ua')[0]), filterBy),
uaClass = ui.browserscope.uaClass,
result = [];
// resolve rows by duck typing because of munged property names
forOwn(object, function(value, key) {
return !(isArray(value) && 0 in value && !('type' in value[0]) && (name = key, result = value));
});
// remove empty rows and set the `p.className` on the browser
// name cell that matches the user's browser name
if (result.length) {
result = object[name] = filter(result, function(value) {
var cells = getDataCells(value),
first = cells[0],
second = cells[1];
// cells[0] is the browser name cell so instead we check cells[1]
// for the presence of ops/sec data to determine if a row is empty or not
if (first && second && second.f) {
delete first.p.className;
if (browserName == toBrowserName(first.f, filterBy)) {
first.p.className = uaClass;
}
return true;
}
});
}
return result;
}
/**
* Checks if a value has an internal [[Class]] of Array.
*
* @private
* @param {Mixed} value The value to check.
* @returns {Boolean} Returns `true` if the value has an internal [[Class]] of
* Array, else `false`.
*/
function isArray(value) {
return toString.call(value) == '[object Array]';
}
/**
* Executes a callback at a given delay interval until it returns `false`.
*
* @private
* @param {Function} callback The function called every poll interval.
* @param {Number} delay The delay between callback calls (secs).
*/
function poll(callback, delay) {
function poller(init) {
if (init || callback() !== false) {
setTimeout(poller, delay * 1e3);
}
}
poller(true);
}
/**
* Cleans up the last action and sets the current action.
*
* @private
* @param {String} action The current action.
*/
function setAction(action) {
clearTimeout(cache.timers[cache.lastAction]);
cache.lastAction = action;
}
/**
* Converts the browser name version number to the format allowed by the
* specified filter.
*
* @private
* @param {String} name The full browser name .
* @param {String} filterBy The filter formating rules to apply.
* @returns {String} The converted browser name.
*/
function toBrowserName(name, filterBy) {
name || (name = '');
if (filterBy == 'all') {
// truncate something like 1.0.0 to 1
name = name.replace(/(\d+)[.0]+$/, '$1');
}
else if (filterBy == 'family') {
// truncate something like XYZ 1.2 to XYZ
name = name.replace(/[.\d\s]+$/, '');
}
else if (/minor|popular/.test(filterBy) && /\d+(?:\.[1-9])+$/.test(name)) {
// truncate something like 1.2.3 to 1.2
name = name.replace(/(\d+\.[1-9])(\.[.\d]+$)/, '$1');
}
else {
// truncate something like 1.0 to 1 or 1.2.3 to 1 but leave something like 1.2 alone
name = name.replace(/(\d+)(?:(\.[1-9]$)|(\.[.\d]+$))/, '$1$2');
}
return name;
}
/**
* Replaces non-alphanumeric characters with spaces because Browserscope labels
* can only contain alphanumeric characters and spaces.
*
* @private
* @param {String} text The text to be converted.
* @returns {String} The Browserscope safe label text.
* @see http://code.google.com/p/browserscope/issues/detail?id=271
*/
function toLabel(text) {
return (text || '').replace(/[^a-z0-9]+/gi, ' ');
}
/*--------------------------------------------------------------------------*/
/**
* Loads Browserscope's cumulative results table.
*
* @static
* @memberOf ui.browserscope
* @param {Object} options The options object.
*/
function load(options) {
options || (options = {});
var fired,
me = ui.browserscope,
cont = me.container,
filterBy = cache.lastFilterBy = options.filterBy || cache.lastFilterBy,
responses = cache.responses,
response = cache.responses[filterBy],
visualization = window.google && google.visualization;
function onComplete(response) {
var lastResponse = responses[filterBy];
if (!fired) {
// set the fired flag to avoid Google's own timeout
fired = true;
// render if the filter is still the same, else cache the result
if (filterBy == cache.lastFilterBy) {
me.render({ 'force': true, 'response': lastResponse || response });
} else if(!lastResponse && response && !response.isError()) {
responses[filterBy] = response;
}
}
}
// set last action in case the load fails and a retry is needed
setAction('load');
// exit early if there is no container element or the response is cached
// and retry if the visualization library hasn't loaded yet
if (!cont || !visualization || !visualization.Query || response) {
cont && onComplete(response);
}
else if (!ui.running) {
// set our own load timeout to display an error message and retry loading
cache.timers.load = setTimeout(onComplete, me.timings.timeout * 1e3);
// set "loading" message and attempt to load Browserscope data
setMessage(me.texts.loading);
// request Browserscope pass chart data to `google.visualization.Query.setResponse()`
(new visualization.Query(
'//www.browserscope.org/gviz_table_data?category=usertest_' + me.key + '&v=' + filterMap[filterBy],
{ 'sendMethod': 'scriptInjection' }
))
.send(onComplete);
}
}
/**
* Creates a Browserscope beacon and posts the benchmark results.
*
* @static
* @memberOf ui.browserscope
*/
function post() {
var idoc,
iframe,
body = document.body,
me = ui.browserscope,
key = me.key,
timings = me.timings,
name = 'browserscope-' + (cache.counter++) + '-' + (+new Date),
snapshot = createSnapshot();
// set last action in case the post fails and a retry is needed
setAction('post');
if (key && snapshot && me.postable && !ui.running && !/Simulator/i.test(Benchmark.platform)) {
// create new beacon
// (the name contains a timestamp so `cleanup()` can determine when to remove it)
iframe = createElement('iframe', name);
body.insertBefore(iframe, body.firstChild);
idoc = frames[name].document;
iframe.style.display = 'none';
// expose results snapshot
me.snapshot = snapshot;
// set "posting" message and attempt to post the results snapshot
setMessage(me.texts.post);
// Note: We originally created an iframe to avoid Browerscope's old limit
// of one beacon per page load. It's currently used to implement custom
// request timeout and retry routines.
idoc.write(interpolate(
// the doctype is required so Browserscope detects the correct IE compat mode
'#{doctype}<title></title><body><script>' +
'with(parent.ui.browserscope){' +
'var _bTestResults=snapshot,' +
'_bC=function(){clearTimeout(_bT);parent.setTimeout(function(){purge();load()},#{refresh}*1e3)},' +
'_bT=setTimeout(function(){_bC=function(){};render()},#{timeout}*1e3)' +
'}<\/script>' +
'<script src=//www.browserscope.org/user/beacon/#{key}?callback=_bC><\/script>',
{
'doctype': /css/i.test(document.compatMode) ? '<!doctype html>' : '',
'key': key,
'refresh': timings.refresh,
'timeout': timings.timeout
}
));
// avoid the IE spinner of doom
// http://www.google.com/search?q=IE+throbber+of+doom
idoc.close();
}
else {
me.load();
}
}
/**
* Purges the Browserscope response cache.
*
* @static
* @memberOf ui.browserscope
* @param {String} key The key of a single cache entry to clear.
*/
function purge(key) {
// we don't pave the cache object with a new one to preserve existing references
var responses = cache.responses;
if (key) {
delete responses[key];
} else {
forOwn(responses, function(value, key) {
delete responses[key];
});
}
}
/**
* Renders the cumulative results table.
* (tweak the dimensions and styles to best fit your environment)
*
* @static
* @memberOf ui.browserscope
* @param {Object} options The options object.
*/
function render(options) {
options || (options = {});
// coordinates, dimensions, and sizes are in px
var areaHeight,
cellWidth,
data,
labels,
rowCount,
rows,
me = ui.browserscope,
cont = me.container,
responses = cache.responses,
visualization = window.google && google.visualization,
lastChart = cache.lastChart,
chart = cache.lastChart = options.chart || lastChart,
lastFilterBy = cache.lastFilterBy,
filterBy = cache.lastFilterBy = options.filterBy || lastFilterBy,
lastResponse = responses[filterBy],
response = responses[filterBy] = 'response' in options ? (response = options.response) && !response.isError() && response : lastResponse,
areaWidth = '100%',
cellHeight = 80,
fontSize = 13,
height = 'auto',
hTitle = 'operations per second (higher is better)',
hTitleHeight = 48,
left = 240,
legend = 'top',
maxChars = 0,
maxCharsLimit = 20,
maxOps = 0,
minHeight = 480,
minWidth = cont && cont.offsetWidth || 948,
title = '',
top = 50,
vTitle = '',
vTitleWidth = 48,
width = minWidth;
function retry(force) {
var action = cache.lastAction;
if (force || ui.running) {
cache.timers[action] = setTimeout(retry, me.timings.retry * 1e3);
} else {
me[action].apply(me, action == 'render' ? [options] : []);
}
}
// set action to clear any timeouts and prep for retries
setAction(response ? 'render' : cache.lastAction);
// exit early if there is no container element, the data filter has changed or nothing has changed
if (!cont || visualization && (filterBy != lastFilterBy ||
(!options.force && chart == lastChart && response == lastResponse))) {
cont && filterBy != lastFilterBy && load(options);
}
// retry if response data is empty/errored or the visualization library hasn't loaded yet
else if (!response || !visualization) {
// set error message for empty/errored response
!response && visualization && setMessage(me.texts.error);
retry(true);
}
// visualization chart gallary
// http://code.google.com/apis/chart/interactive/docs/gallery.html
else if (!ui.running) {
cont.className = '';
data = cloneData(response.getDataTable());
labels = getDataLabels(data);
rows = getDataRows(data);
rowCount = rows.length;
chart = chart.charAt(0).toUpperCase() + chart.slice(1).toLowerCase();
// adjust data for non-tabular displays
if (chart != 'Table') {
// remove "# Tests" run count label (without label data the row will be ignored)
labels.pop();
// modify row data
each(rows, function(row) {
each(getDataCells(row), function(cell, index, cells) {
var lastIndex = cells.length - 1;
// cells[1] through cells[lastIndex - 1] are ops/sec cells
if (/^[\d.,]+$/.test(cell.f)) {
// assign ops/sec as cell value
cell.v = +cell.f.replace(/,/g, '');
// add rate to the text
cell.f += ' ops/sec';
// capture highest ops value to use when computing the left coordinate
maxOps = max(maxOps, cell.v);
}
// cells[0] is the browser name cell
// cells[lastIndex] is the run count cell and has no `f` property
else if (cell.f) {
// add test run count to browser name
cell.f += chart == 'Pie' ? '' : ' (' + (cells[lastIndex].v || 1) + ')';
// capture longest char count to use when computing left coordinate/cell width
maxChars = min(maxCharsLimit, max(maxChars, cell.f.length));
}
// compute sum of all ops/sec for pie charts
if (chart == 'Pie') {
if (index == lastIndex) {
cells[1].f = formatNumber(cells[1].v) + ' total ops/sec';
} else if (index > 1 && typeof cell.v == 'number') {
cells[1].v += cell.v;
}
}
// if the browser name matches the user's browser then style it
if (cell.p && cell.p.className) {
// prefix the browser name with a line separator (\u2028) because it's not rendered
// (IE may render a negligible space in the tooltip of browser names truncated with ellipsis)
cell.f = uaToken + cell.f;
// poll until the chart elements exist and are styled
poll(function() { return !addChartStyle(); }, 0.01);
}
});
});
// adjust captions and chart dimensions
if (chart == 'Bar') {
// use minHeight to avoid sizing issues when there is only 1 bar
height = max(minHeight, top + (rowCount * cellHeight));
// compute left by adding the longest approximate vAxis text width and
// a right pad of 10px
left = (maxChars * (fontSize / 1.6)) + 10;
// get percentage of width left after subtracting the chart's left
// coordinate and room for the ops/sec number
areaWidth = (100 - (((left + 50) / width) * 100)) + '%';
}
else {
// swap captions (the browser list caption is blank to conserve space)
vTitle = [hTitle, hTitle = vTitle][0];
height = minHeight;
if (chart == 'Pie') {
legend = 'right';
title = 'Total operations per second by browser (higher is better)';
}
else {
hTitleHeight = 28;
// compute left by getting the sum of the horizontal space wanted
// for the vAxis title's width, the approximate vAxis text width, and
// the 13px gap between the chart and the right side of the vAxis text
left = vTitleWidth + (formatNumber(maxOps).length * (fontSize / 1.6)) + 13;
// compute cell width by adding the longest approximate hAxis text
// width and wiggle room of 26px
cellWidth = (maxChars * (fontSize / 2)) + 26;
// use minWidth to avoid clipping the key
width = max(minWidth, left + (rowCount * cellWidth));
}
}
// get percentage of height left after subtracting the vertical space wanted
// for the hAxis title's height, text size, the chart's top coordinate,
// and the 8px gap between the chart and the top of the hAxis text
areaHeight = (100 - (((hTitleHeight + fontSize + top + 8) / height) * 100)) + '%';
// make chart type recognizable
chart += 'Chart';
}
if (rowCount && visualization[chart]) {
new visualization[chart](cont).draw(data, {
'colors': ui.browserscope.colors,
'fontSize': fontSize,
'is3D': true,
'legend': legend,
'height': height,
'title': title,
'width': width,
'chartArea': { 'height': areaHeight, 'left': left, 'top': top, 'width': areaWidth },
'hAxis': { 'baseline': 0, 'title': hTitle },
'vAxis': { 'baseline': 0, 'title': vTitle }
});
} else {
setMessage(me.texts.empty);
}
}
}
/*--------------------------------------------------------------------------*/
// expose
ui.browserscope = {
/**
* Your Browserscope API key.
*
* @memberOf ui.browserscope
* @type String
*/
'key': '',
/**
* A flag to indicate if posting is enabled or disabled.
*
* @memberOf ui.browserscope
* @type Boolean
*/
'postable': true,
/**
* The selector of the element to contain the entire Browserscope UI.
*
* @memberOf ui.browserscope
* @type String
*/
'selector': '',
/**
* The class name used to style the user's browser name when it appears
* in charts.
*
* @memberOf ui.browserscope
* @type String
*/
'uaClass': 'rt-ua-cur',
/**
* Object containing various timings settings.
*
* @memberOf ui.browserscope
* @type Object
*/
'timings': {
/**
* The delay between removing abandoned script and iframe elements (secs).
*
* @memberOf ui.browserscope.timings
* @type Number
*/
'cleanup': 10,
/**
* The delay before refreshing the cumulative results after posting (secs).
*
* @memberOf ui.browserscope.timings
* @type Number
*/
'refresh': 3,
/**
* The delay between load attempts (secs).
*
* @memberOf ui.browserscope.timings
* @type Number
*/
'retry': 5,
/**
* The time to wait for a request to finish (secs).
*
* @memberOf ui.browserscope.timings
* @type Number
*/
'timeout': 10
},
/**
* Object containing various text messages.
*
* @memberOf ui.browserscope
* @type Object
*/
'texts': {
/**
* The text shown when their is no recorded data available to report.
*
* @memberOf ui.browserscope.texts
* @type String
*/
'empty': 'No data available',
/**
* The text shown when the cumulative results data cannot be retrieved.
*
* @memberOf ui.browserscope.texts
* @type String
*/
'error': 'The get/post request has failed :(',
/**
* The text shown while waiting for the cumulative results data to load.
*
* @memberOf ui.browserscope.texts
* @type String
*/
'loading': 'Loading cumulative results data&hellip;',
/**
* The text shown while posting the results snapshot to Browserscope.
*
* @memberOf ui.browserscope.texts
* @type String
*/
'post': 'Posting results snapshot&hellip;',
/**
* The text shown while benchmarks are running.
*
* @memberOf ui.browserscope.texts
* @type String
*/
'wait': 'Benchmarks running. Please wait&hellip;'
},
// loads cumulative results table
'load': load,
// posts benchmark snapshot to Browserscope
'post': post,
// purges the Browserscope response cache
'purge': purge,
// renders cumulative results table
'render': render
};
/*--------------------------------------------------------------------------*/
addListener(window, 'load', function() {
var me = ui.browserscope,
key = me.key,
placeholder = key && query(me.selector)[0];
// create results html
if (placeholder) {
setHTML(placeholder,
'<h1 id=bs-logo><a href=//www.browserscope.org/user/tests/table/#{key}>' +
'<span>Browserscope</span></a></h1>' +
'<div class=bs-rt><div id=bs-chart></div></div>',
{ 'key': key });
// the element the charts are inserted into
me.container = query('#bs-chart')[0];
// Browserscope's UA div is inserted before an element with the id of "bs-ua-script"
loadScript('//www.browserscope.org/ua?o=js', me.container).id = 'bs-ua-script';
// the "autoload" string can be created with
// http://code.google.com/apis/loader/autoloader-wizard.html
loadScript('//www.google.com/jsapi?autoload=' + encodeURIComponent('{' +
'modules:[{' +
'name:"visualization",' +
'version:1,' +
'packages:["corechart","table"],' +
'callback:ui.browserscope.load' +
'}]' +
'}'));
// init garbage collector
cleanup();
}
});
// hide the chart while benchmarks are running
ui.on('start', function() {
setMessage(ui.browserscope.texts.wait);
})
.on('abort', function() {
ui.browserscope.render({ 'force': true });
});
}(this, document));