// vim: set ts=4 sw=4 tw=99 et: "use strict"; var AWFY = { }; AWFY.refreshTime = 60 * 5; AWFY.machineId = 0; AWFY.hasLegend = false; AWFY.panes = []; AWFY.queryParams = null; AWFY.aggregate = null; AWFY.xhr = []; AWFY.view = 'none'; AWFY.suiteName = null; AWFY.lastHash = null; AWFY.lastRefresh = 0; AWFY.request = function (files, callback) { var url = window.location.protocol + '//' + window.location.host + window.location.pathname; if (url[url.length - 1] != '/') url += '/'; url += 'data/'; var count = 0; var received = new Array(files.length); var done = (function (jqXHR, textStatus) { count++; if (count == files.length) callback(received); this.xhr.splice(this.xhr.lastIndexOf(jqXHR), 1); }).bind(this); for (var i = 0; i < files.length; i++) { var success = (function (index) { return function (data, textStatus, jqXHR) { received[index] = data; }; })(i); var req = { async: true, complete: done, success: success, cache: false }; this.xhr.push($.ajax(url + files[i] + '.json', req)); } } AWFY.pushState = function () { // Build URL query. var vars = [] vars.push('machine=' + this.machineId); if (this.view == 'breakdown') { vars.push('view=breakdown'); vars.push('suite=' + this.suiteName); } if ($('#about').is(':visible')) vars.push('about=1'); var text = '#' + vars.join('&'); this.lastHash = text; if (window.history.pushState) window.history.pushState(null, 'AreWeFastYet', text); else window.location.hash = '#' + text; } AWFY.loadAggregateGraph = function (blobgraph) { var lines = []; var info = []; for (var i = 0; i < blobgraph.lines.length; i++) { var blobline = blobgraph.lines[i]; var points = []; for (var j = 0; j < blobline.data.length; j++) { var point = blobline.data[j]; var score = point && point[0] ? point[0] : null; points.push([j, score]); } var mode = AWFYMaster.modes[blobline.modeid]; if (!mode) continue; var line = { color: mode.color, data: points }; lines.push(line); info.push(blobline); } var graph = { lines: lines, direction: blobgraph.direction, aggregate: blobgraph.aggregate, timelist: blobgraph.timelist, earliest: blobgraph.earliest, info: info }; return graph; } AWFY.displayNewGraph = function (name, graph) { var elt = $('#' + name + '-graph'); if (!elt.length) return; var display = elt.data('awfy-display'); if (!display) { display = new Display(this, name, elt); elt.data('awfy-display', display); } if (display.shouldRefresh()) { display.setup(graph); display.draw(); } this.aggregate[name] = graph; } AWFY.drawLegend = function () { // Draw the legend if needed. if (this.hasLegend) return; var legend = $("#legend"); legend.empty(); var modes = []; for (var modename in AWFYMaster.modes) { var mode = AWFYMaster.modes[modename]; // hack - strip jm+ti, bc if (modename == 12 || modename == 15) continue; if (AWFY.machineId != 14 && modename == 16) continue; if (!mode.used) continue; modes.push(mode); } for (var i = 0; i < modes.length; i++) { var mode = modes[i]; var vendor = AWFYMaster.vendors[mode.vendor_id]; if (!vendor) continue; var item = $('
  • '); var link = $('' + vendor.browser + ' (' + mode.name + ')'); var onClick = (function (awfy, mode, link) { return (function () { if (mode.hidden) { mode.hidden = false; link.css('color', '#000000'); } else { mode.hidden = true; link.css('color', '#cccccc'); } for (var i = 0; i < this.panes.length; i++) { var elt = this.panes[i]; var display = elt.data('awfy-display'); if (!display) continue; display.draw(); } return false; }).bind(awfy); })(this, mode, link); link.click(onClick); if (mode.hidden) link.css('color', '#cccccc'); else link.css('color', '#000000'); link.appendTo(item); item.appendTo(legend); } this.hasLegend = true; } AWFY.computeBreakdown = function (data, id) { var blob = typeof data == "string" ? JSON.parse(data) : data; // Should we handle version changes better? if (blob.version != AWFYMaster.version) { window.location.reload(); return; } var graph = this.loadAggregateGraph(blob['graph']); this.displayNewGraph(id, graph); } AWFY.computeAggregate = function (received) { var blob = typeof received[0] == "string" ? JSON.parse(received[0]) : received[0]; // Should we handle version changes better? if (blob.version != AWFYMaster.version) { window.location.reload(); return; } var graphs = { }; for (var name in blob.graphs) { var blobgraph = blob.graphs[name]; if (!blobgraph) continue; graphs[name] = this.loadAggregateGraph(blobgraph); } // Save this for if/when we need to zoom out. this.aggregate = graphs; for (var id in graphs) this.displayNewGraph(id, graphs[id]); this.drawLegend(); } AWFY.mergeJSON = function (blobs) { var lines = { }; var timelist = []; // We're guaranteed the blobs are in sorted order, which makes this simpler. for (var i = 0; i < blobs.length; i++) { var blob = blobs[i]; // Should we handle version changes better? if (blob.version != AWFYMaster.version) { window.location.reload(); return; } for (var j = 0; j < blob.graph.lines.length; j++) { var blobline = blob.graph.lines[j]; var line = lines[blobline.modeid]; if (!line) { var points = []; var info = []; // We have to pre-fill the array with slots for each blob // we may have missed. for (var k = 0; k < timelist.length; k++) { points.push(null); info.push(null); } line = { points: points, info: info }; lines[blobline.modeid] = line; } var points = line.points; var info = line.info; for (var k = 0; k < blobline.data.length; k++) { var point = blobline.data[k]; var score = point && point[0] ? point[0] : null; points.push([timelist.length + k, score]); info.push(point); } } for (var j = 0; j < blob.graph.timelist.length; j++) timelist.push(blob.graph.timelist[j]); // If we missed updating any line, pre-fill it with null points. for (var modeid in lines) { var line = lines[modeid]; if (line.points.length == timelist.length) continue; for (var j = line.points.length; j < timelist.length; j++) { line.points.push(null); line.info.push(null); } } } var actual = []; var info = []; for (var modename in lines) { if (!(modename in AWFYMaster.modes)) continue; var line = { data: lines[modename].points, color: AWFYMaster.modes[modename].color }; actual.push(line); info.push({ 'modeid': parseInt(modename), 'data': lines[modename].info }); } var graph = { lines: actual, aggregate: false, timelist: timelist, info: info }; return graph; } AWFY.condense = function (graph, max) { if (graph.timelist.length <= max) return graph; var slice = graph.timelist.length / max; var timelist = []; var lines = []; var info = []; // Setup the new structures. for (var i = 0; i < graph.lines.length; i++) { var newline = { 'color': graph.lines[i].color, 'data': [] }; var newinfo = { 'modeid': graph.info[i].modeid, 'data': [] }; lines.push(newline); info.push(newinfo); } var pos = 0; for (var i = 0; i < max; i++) { var start = Math.round(pos); for (var lineno = 0; lineno < lines.length; lineno++) { var oldinfo = graph.info[lineno]; var newline = lines[lineno]; var newinfo = info[lineno]; var average = 0; var count = 0; var first = null; var last = null; for (var j = start; j < pos + slice && j < oldinfo.data.length; j++) { var point = oldinfo.data[j]; if (!point || !point[0]) continue; if (!first) first = point[1]; if (point.length > 1 && point[2]) last = point[2]; else last = first average = ((average * count) + point[0]) / (count + 1); count += 1; } var score = average ? average : null; newline.data.push([timelist.length, score]); if (count) newinfo.data.push([average, first, last]) else newinfo.data.push([average, first]) } timelist.push(graph.timelist[start]); pos += slice; } return { info: info, lines: lines, timelist: timelist, direction: graph.direction }; } AWFY.trim = function (graph, start, end) { var timelist = []; var lines = []; var infos = []; // Setup the new structures. for (var i = 0; i < graph.lines.length; i++) { var newline = { 'color': graph.lines[i].color, 'data': [] }; var newinfo = { 'modeid': graph.info[i].modeid, 'data': [] }; lines.push(newline); infos.push(newinfo); } // Whether |end| is inclusive is not really clear, actually. for (var i = start; i < end; i++) timelist.push(graph.timelist[i]); for (var i = 0; i < graph.lines.length; i++) { var oldline = graph.lines[i]; var oldinfo = graph.info[i]; var line = lines[i]; var info = infos[i]; for (var j = start; j < end; j++) { var point = oldline.data[j]; line.data.push([j - start, point ? point[1] : null]); info.data.push(oldinfo.data[j]); } } return { lines: lines, info: infos, timelist: timelist, direction: graph.direction }; } AWFY.computeZoom = function (display, received, start, end) { // Get JSON blobs for each received text. var blobs = []; for (var i = 0; i < received.length; i++) { if (!received[i]) continue; if (typeof received[i] == "string") blobs.push(JSON.parse(received[i])); else blobs.push(received[i]); } if (!blobs.length) { display.cancelZoom(); return; } var graph = this.mergeJSON(blobs); display.completeZoom(graph, start, end); } AWFY.findX = function (graph, time) { for (var i = 0; i < graph.timelist.length; i++) { if (graph.timelist[i] >= time) break; } return i; } AWFY.requestZoom = function (display, kind, start_t, end_t) { // Figure out the list of dates we'll need to query. var files = []; var start = new Date(start_t * 1000); var end = new Date(end_t * 1000); for (var year = start.getUTCFullYear(); year <= end.getUTCFullYear(); year++) { var firstMonth = (year == start.getUTCFullYear()) ? start.getUTCMonth() + 1 : 1; var lastMonth = (year == end.getUTCFullYear()) ? end.getUTCMonth() + 1 : 12; for (var month = firstMonth; month <= lastMonth; month++) { var name = kind + '-' + display.id + '-' + this.machineId + '-' + year + '-' + month; if (this.view == 'breakdown') name = 'bk-' + name; files.push(name); } } var zoom = function (received) { this.computeZoom(display, received, start_t, end_t); } this.request(files, zoom.bind(this)); } AWFY.showOverview = function () { this.reset('overview'); $('#breakdown').empty(); $('#breakdown').hide(); $('.graph-container').show(); this.suiteName = null; this.panes = [$('#ss-graph'), /*$('#v8real-graph'),*/ $('#kraken-graph'), $('#octane-graph') ]; this.request(['aggregate-' + this.machineId], this.computeAggregate.bind(this)); this.lastRefresh = Date.now(); } AWFY.showBreakdown = function (name) { this.reset('breakdown'); // Clear the breakdown view. var breakdown = $('#breakdown'); breakdown.empty() $('.graph-container').hide(); breakdown.show(); this.suiteName = name; this.panes = []; var total = 0; // Create a div for each sub-test. var suite = AWFYMaster.suites[name]; for (var i = 0; i < suite.tests.length; i++) { var test = suite.tests[i]; var id = name + '-' + test; $('
    ').html('' + id + '').appendTo(breakdown); var div = $('
    '); div.appendTo(breakdown); $('

    ').appendTo(breakdown); this.panes.push(div); var callback = (function (id) { return (function (received) { if (received[0]) this.computeBreakdown(received[0], id); if (++total == suite.tests.length) this.drawLegend(); }).bind(this); }).bind(this)(id); // Fire off an XHR request for each test. var file = 'bk-aggregate-' + id + '-' + this.machineId; this.request([file], callback); } this.lastRefresh = Date.now(); } AWFY.requestRedraw = function () { if (this.view == 'overview') { this.request(['aggregate-' + this.machineId], this.computeAggregate.bind(this)); } else if (this.view == 'breakdown') { var suite = AWFYMaster.suites[this.suiteName]; var total = 0; for (var i = 0; i < suite.tests.length; i++) { var id = this.suiteName + '-' + suite.tests[i]; var callback = (function (id) { return (function (received) { if (received[0]) this.computeBreakdown(received[0], id); if (++total == suite.tests.length) this.drawLegend(); }).bind(this); }).bind(this)(id); var file = 'bk-aggregate-' + id + '-' + this.machineId; this.request([file], callback); } } this.lastRefresh = Date.now(); } AWFY.changeMachine = function (machine_id) { this.reset(this.view); this.machineId = machine_id; this.requestRedraw(); } AWFY.reset = function (view) { // Delete everything from the existing panes. for (var i = 0; i < this.panes.length; i++) { var elt = this.panes[i]; var display = elt.data('awfy-display'); if (!display) continue; display.shutdown(); // If we're changing views, remove the display. if (this.view != view) { elt.data('awfy-display', null); elt.empty(); } } for (var mode in AWFYMaster.modes) AWFYMaster.modes[mode].used = false; this.hasLegend = false; this.aggregate = { }; this.view = view; // Drop all outstanding XHR requests. for (var i = 0; i < this.xhr.length; i++) this.xhr[i].abort(); this.xhr = []; } AWFY.onHashChange = function () { this.parseURL(); } AWFY.refresh = function () { if (Date.now() - this.lastRefresh >= this.refreshTime) this.requestRedraw(); window.setTimeout(this.refresh.bind(this), this.refreshTime * 1000); } AWFY.parseURL = function () { if (this.lastHash == window.location.hash) return; var query = window.location.hash.substring(1); var items = query.split('&'); this.queryParams = {}; for (var i = 0; i < items.length; i++) { var item = items[i].split('='); this.queryParams[item[0]] = item[1]; } var machineId; if ('machine' in this.queryParams) machineId = parseInt(this.queryParams['machine']); else machineId = 11; var view = this.queryParams['view']; if (!view || (view != 'overview' && view != 'breakdown')) view = 'overview'; if (view == 'breakdown') { var suiteName = this.queryParams['suite']; if (!suiteName || !AWFYMaster.suites[suiteName]) view = 'overview'; } // Make sure the menus are up to date. if (this.view != 'none') { if (this.machineId != machineId) { $('#machinelist .clicked').removeClass('clicked'); $('#machine' + machineId).addClass('clicked'); } $('#breakdownlist .clicked').removeClass('clicked'); if (view == 'overview') $('#suite-overview').addClass('clicked'); else if (view == 'breakdown') $('#suite-' + suiteName).addClass('clicked'); } if (view == this.view) { // See if we just need to change the current machine. if (view == 'overview') { if (machineId != this.machineId) this.changeMachine(machineId); return; } else if (view == 'breakdown') { if (suiteName == this.suiteName) { if (machineId != this.machineId) this.changeMachine(machineId); return; } } } // Nope. this.machineId = machineId; if (view == 'overview') this.showOverview(); else if (view == 'breakdown') this.showBreakdown(suiteName); this.lastHash = window.location.hash; } AWFY.startup = function () { this.panes = [$('#ss-graph'), $('#kraken-graph'), /*$('#v8real-graph'),*/ $('#octane-graph')]; this.parseURL(); // Add machine information to the menu. var menu = $('#machinelist'); for (var id in AWFYMaster.machines) { var machine = AWFYMaster.machines[id]; var li = $('
  • '); var a = $(''); a.click((function (id) { return (function (event) { this.changeMachine(parseInt(id)); $('#machinelist .clicked').removeClass('clicked'); $(event.target).addClass('clicked'); this.pushState(); return false; }).bind(this); }).bind(this)(id)); if (parseInt(id) == this.machineId) a.addClass('clicked'); a.html(machine.description); a.appendTo(li); li.appendTo(menu); } // Hide it by default. $('#machinedrop').click((function (event) { if (!menu.is(':visible') && !$('#about').is(':visible')) { menu.show(); } else { menu.hide(); } return false; }).bind(this)); menu.hide(); var breakdown = $('#breakdownlist'); var home = $('').click( (function (event) { $('#breakdownlist .clicked').removeClass('clicked'); $(event.target).addClass('clicked'); this.showOverview(); this.pushState(); return false; }).bind(this)) .html('Overview') .appendTo($('
  • ').appendTo(breakdown)); if (this.view == 'overview') home.addClass('clicked'); var suites = []; for (var name in AWFYMaster.suites) suites.push([name, AWFYMaster.suites[name]]); suites.sort(function (a, b) { return a[1].sort_order - b[1].sort_order; }); for (var i = 0; i < suites.length; i++) { var name = suites[i][0]; var suite = suites[i][1]; var li = $('
  • '); var a = $(''); a.click((function (name) { return (function(event) { $('#breakdownlist .clicked').removeClass('clicked'); $(event.target).addClass('clicked'); this.showBreakdown(name); this.pushState(); return false; }).bind(this); }).bind(this)(name)); if (this.view == 'breakdown' && this.suiteName == name) a.addClass('clicked'); a.html(suite.description); a.appendTo(li); li.appendTo(breakdown); } $('#bkdrop').click((function (event) { if (!breakdown.is(':visible') && !$('#about').is(':visible')) { breakdown.show(); } else { breakdown.hide(); } return false; }).bind(this)); breakdown.hide(); var about = $('#aboutdrop'); about.click((function (event) { var help = $('#about'); if (!help.is(':visible')) { $('.graph-container').hide(); $('#breakdown').hide(); help.show(); about.text('Home'); $('#breakdownhook').hide(); $('#machinehook').hide(); for (var i = 0; i < this.panes.length; i++) { var elt = this.panes[i]; var display = elt.data('awfy-display'); if (display) display.hideToolTips(); } } else { help.hide(); if (this.view == 'breakdown') $('#breakdown').show(); else $('.graph-container').show(); about.text('About'); $('#breakdownhook').show(); $('#machinehook').show(); for (var i = 0; i < this.panes.length; i++) { var elt = this.panes[i]; var display = elt.data('awfy-display'); if (display) display.showToolTips(); } } menu.hide(); breakdown.hide(); this.pushState(); return false; }).bind(this)); if (this.queryParams['help']) about.trigger('click'); $(window).hashchange(this.onHashChange.bind(this)); $(window).bind('popstate', this.onHashChange.bind(this)); window.setTimeout(this.refresh.bind(this), this.refreshTime * 1000); }