(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'; 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}