| /** |
| * Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file */ |
| /** Provides the logic behind the performance visualization webpage. */ |
| |
| function assert_(cond, msg) { |
| if(!cond) { |
| throw msg || "Assertion failed"; |
| } |
| } |
| |
| |
| function last(ary) { |
| return ary[ary.length - 1]; |
| } |
| |
| |
| function getKeys(dict) { |
| var keys = []; |
| for(var key in dict) { |
| if(dict.hasOwnProperty(key)) { |
| keys.push(key); |
| } |
| } |
| return keys; |
| } |
| |
| |
| function getArg(key) { |
| var keypairs = window.location.hash.slice(1).split('&').map(function(p) { |
| return p.split('=', 2);}); |
| for(var i = 0; i < keypairs.length; i++) { |
| if(keypairs[i][0] == key && keypairs[i][1].length > 0) { |
| return decodeURIComponent(keypairs[i][1]); |
| } |
| } |
| return null; |
| } |
| |
| |
| var id = function(e) { return e; }; |
| |
| |
| function $$(query, par) { |
| if(!par) { |
| return Array.prototype.map.call(document.querySelectorAll(query), id); |
| } else { |
| return Array.prototype.map.call(par.querySelectorAll(query), id); |
| } |
| } |
| |
| function $$$(query, par) { |
| return par ? par.querySelector(query) : document.querySelector(query); |
| } |
| |
| |
| /** A safe wrapper around a dictionaryish object */ |
| function PagedDictionary(attrs) { |
| var dict = {}; |
| var index = []; |
| var current = null; |
| // Adds extra attributes to extend functionality as needed |
| for(var attr in attrs) { |
| if(attrs.hasOwnProperty(attr)) { |
| this[attr] = attrs[attr]; |
| } |
| } |
| /* Returns true if the dictionary has something with that index. */ |
| this.has = function(id) { |
| return index.indexOf(id) != -1; |
| }; |
| /* Returns the value currently being pointed to. */ |
| this.cur = function() { |
| if(current) { |
| return dict[current]; |
| } else { |
| return null; |
| } |
| }; |
| /* Returns the current key being used as a pointer. */ |
| this.currentId = function() { |
| return current; |
| }; |
| /* Returns the value matching the key if it exists, null otherwise. */ |
| this.get = function(id) { |
| if(this.has(id)) { |
| return dict[id]; |
| } else { |
| return null; |
| } |
| }; |
| /* Adds a value to the dictionary. */ |
| this.add = function(id, val) { |
| if(!this.has(id)) { |
| index.push(id); |
| } |
| dict[id] = val; |
| }; |
| /* Adds a value to the dictionary, and has the current pointer point to it. */ |
| this.push = function(id, val) { |
| this.add(id, val); |
| this.makeCurrent(id); |
| }; |
| /* Points current at a particular value. */ |
| this.makeCurrent = function(id) { |
| if(this.has(id)) { |
| current = id; |
| return true; |
| } else { |
| return false; |
| } |
| }; |
| /* Returns a list of existing keys. */ |
| this.index = function() { |
| return index; |
| }; |
| // TODO: Make map function |
| /* Removes a value from the dictionary. */ |
| this.remove = function(id) { |
| if(!this.has(id)) { |
| dict[id] = null; |
| index.splice(index.indexOf(id),1); |
| } |
| }; |
| /* Sets a value for a given key, returns false if it fails. */ |
| this.set = function(id, val) { |
| if(this.has(id)) { |
| dict[id] = val; |
| return true; |
| } else { |
| return false; |
| } |
| }; |
| /* Returns the position of a key in the key list. */ |
| this.indexOf = function(idx) { |
| return index.indexOf(idx); |
| } |
| /* Looks up the index for a given item. */ |
| this.rlookup = function(val) { |
| for(var i = 0; i < dict.length; i++) { |
| if(this.dict[index[i]] == val) { |
| return index[i]; |
| } else { |
| return null; |
| } |
| } |
| }; |
| /* Returns the private variables for debugging purposes. */ |
| this.debug = function() { |
| return {dict: dict, index: index, current: current}; |
| }; |
| } |
| |
| /* Unordered data structure with O(1) add, remove, and index.*/ |
| // Benchmarked remarkably well against vector.splice()/push() |
| function Vector() { |
| var data = []; |
| var firstEmpty = 0; |
| if(arguments) { |
| data = Array.prototype.map.call(arguments, id); |
| firstEmpty = data.length; |
| } |
| this.get = function(idx) { |
| assert_(idx < firstEmpty); |
| return data[idx]; |
| }, |
| this.push = function(elem) { |
| data[firstEmpty] = elem; |
| firstEmpty++; |
| }; |
| this.pop = function(idx) { |
| assert_(idx < firstEmpty && firstEmpty > 0); |
| var result = data[idx]; |
| data[idx] = data[firstEmpty - 1]; |
| data[firstEmpty - 1] = null; |
| firstEmpty--; |
| return result; |
| }; |
| this.all = function() { |
| return data.slice(0, firstEmpty); |
| }; |
| this.remove = function(elem) { |
| var i = 0; |
| while(i < firstEmpty && data[i] != elem) { i++; } |
| this.pop(i); |
| }; |
| this.map = function() { |
| return Array.prototype.map.apply(this.all(), arguments); |
| }; |
| this.has = function(val) { |
| for(var i = 0; i < firstEmpty; i++) { |
| if(data[i] == val) { return true; } |
| } |
| return false; |
| }; |
| } |
| |
| |
| function loadJSON(uri, success, fail) { |
| var req = new XMLHttpRequest(); |
| document.body.classList.add('waiting'); |
| req.addEventListener('load', function() { |
| console.log(req); |
| if(req.response && req.status == 200) { |
| if(req.responseType == 'json') { |
| success(req.response); |
| } else { |
| success(JSON.parse(req.response)); |
| } |
| } else { |
| fail(); |
| } |
| }); |
| req.addEventListener('loadend', function() { |
| document.body.classList.remove('waiting'); |
| }); |
| req.addEventListener('error', function() { |
| notifyUser('Unable to retrieve' + uri); |
| fail(); |
| }); |
| req.open('GET', uri, true); |
| req.send(); |
| } |
| |
| |
| /** Sends the user a notification on the bottom bar. */ |
| function notifyUser(text, replace) { |
| if (!$('#notification').is(':visible') && !replace) { |
| $('#notification-text').html(text); |
| $('#notification').show().delay(5000).fadeOut(1000).hide(10); |
| // If you find a way to convert this to non-jQuery, I'll replace it. |
| } else { |
| window.setTimeout(notifyUser, 400, text); |
| } |
| } |
| |
| |
| /** Splits a particular component out of a list of objects.*/ |
| function getComponent(ary, name) { |
| return ary.map(function(e) { return e[name]; }); |
| } |
| |
| |
| /** Removes all the children owned by the element.*/ |
| function killChildren(e) { |
| while(e.hasChildNodes()) { |
| e.removeChild(e.children[0]); |
| } |
| } |
| |
| |
| // FUTURE(kelvinly): Try using a dirty flag instead of two representations |
| // to see if it's more efficient. |
| /** Encapsulates the legend state.*/ |
| var legend = (function() { |
| var internalLegend = []; |
| var externalLegend = []; |
| // Each legend marker has two values, a key and a color |
| // The external legend also has a pointer to its DOM element called elem. |
| var legendBody = null; |
| |
| function addToDOM(e) { |
| console.log('addToDOM called'); |
| // console.log(e); |
| assert_(e.key && e.color); |
| assert_(legendBody); |
| var container = document.createElement('tr'); |
| var checkContainer = document.createElement('td'); |
| var checkbox = document.createElement('input'); |
| checkbox.type = 'checkbox'; |
| checkbox.checked = true; |
| checkbox.id = e.key; |
| var outerColor = document.createElement('div'); |
| outerColor.classList.add('legend-box-outer'); |
| var innerColor = document.createElement('div'); |
| innerColor.classList.add('legend-box-inner'); |
| innerColor.style.border = '5px solid ' + e.color; |
| var text = document.createTextNode(e.key); |
| var linkContainer = document.createElement('td'); |
| var link = document.createElement('a'); |
| link.href = '#'; |
| link.innerText = 'Remove'; |
| link.id = e.key + '_remove'; |
| |
| container.appendChild(checkContainer); |
| container.appendChild(linkContainer); |
| |
| checkContainer.appendChild(checkbox); |
| checkContainer.appendChild(outerColor); |
| checkContainer.appendChild(text); |
| |
| linkContainer.appendChild(link); |
| outerColor.appendChild(innerColor); |
| |
| legendBody.appendChild(container); |
| return {key: e.key, color: e.color, elem: container}; |
| } |
| |
| function rawAdd(key, color) { |
| assert_(getComponent(internalLegend, 'key').indexOf(key) == -1); |
| |
| var newPair = {key:key, color:color}; |
| internalLegend.push(newPair); |
| externalLegend.push(addToDOM(newPair)); |
| } |
| return { |
| /** Updates the colors for the elements.*/ |
| updateColors: function(plotRef) { |
| var dataRef = plotRef.getData(); |
| internalLegend.forEach(function(legendMarker) { |
| legendMarker.color = legendMarker.color || 'white'; |
| dataRef.forEach(function(series) { |
| if(legendMarker.key == series.label) { |
| legendMarker.color = series.color; |
| } |
| }); |
| }); |
| }, |
| /** Refreshes the DOM to match the internal state.*/ |
| refresh: function() { |
| var synced, color_synced = true; |
| synced = internalLegend.length == externalLegend.length; |
| // NOTE: Restructure control flow? |
| internalLegend.forEach(function(elem, idx) { |
| if(!synced) { |
| return; |
| } else { |
| if(elem.key != externalLegend[idx].key) { |
| synced = false; |
| color_synced = false; |
| } |
| if(elem.color != externalLegend[idx].color) { |
| color_synced = false; |
| } |
| } |
| }); |
| if(synced && color_synced) { |
| console.log('legend.refresh: legends synced'); |
| return; |
| } else if(synced && !color_synced) { |
| console.log('legend.refresh: fixing colors'); |
| // Fix the colors |
| $$('tr', legendBody).forEach(function(e, idx) { |
| assert_($$$('input', e).id == internalLegend[idx].key); |
| |
| $$$('.legend-box-inner', e).style. |
| border = '5px solid ' + internalLegend[idx].color; |
| }); |
| } else { |
| killChildren($$$('#legend table tbody')); |
| externalLegend = []; |
| // Regenerate a new legend |
| var _this = this; |
| internalLegend.forEach(function(e) { |
| externalLegend.push(addToDOM(e, _this)); |
| }); |
| } |
| }, |
| remove: function(key) { |
| var children = []; |
| externalLegend.forEach(function(e, idx) { |
| if(e.key == key) { |
| children.push(e.elem); |
| } |
| }); |
| children.forEach(function(c) { |
| assert_(c.parentNode); |
| c.parentNode.removeChild(c); |
| }); |
| internalLegend = internalLegend.filter(function(e) { |
| return e.key != key; |
| }); |
| externalLegend = externalLegend.filter(function(e) { |
| return e.key != key; |
| }); |
| }, |
| /* Sets up the private variables, and a few of the relevant UI controls.*/ |
| init: function(showHandler, hideHandler, drawHandler) { |
| console.log('Initializing legend'); |
| assert_(showHandler && hideHandler); |
| legendBody = $$$('#legend table tbody'); |
| var _this = this; |
| $$$('#nuke-plot').addEventListener('click', function(e) { |
| internalLegend.forEach(function(keypair) { |
| plotData.hide(keypair.key); |
| }); |
| killChildren($$$('#legend table tbody')); |
| internalLegend = []; |
| externalLegend = []; |
| drawHandler(); |
| e.preventDefault(); |
| }); |
| legendBody.addEventListener('click', function(e) { |
| if('INPUT' == e.target.nodeName) { |
| if(e.target.checked) { |
| console.log(e.target.id + ' checked'); |
| showHandler(e.target.id); |
| } else { |
| console.log(e.target.id + ' unchecked'); |
| hideHandler(e.target.id); |
| } |
| drawHandler(); |
| } else if('A' == e.target.nodeName) { |
| console.log(e.target.id + ' removed'); |
| if(document.getElementById(e.target.id.slice(0,-'_remove'.length)).checked) { |
| hideHandler(e.target.id.slice(0,-'_remove'.length)); |
| } |
| _this.remove(e.target.id.slice(0,-'_remove'.length)); |
| drawHandler(); |
| } |
| }); |
| /* Adds a key to the legend, and makes it visible on the chart.*/ |
| this.add = function(key, color, nodraw) { |
| if(getComponent(internalLegend, 'key').indexOf(key) != -1) { |
| return; |
| } |
| rawAdd(key, color); |
| showHandler(key); |
| if(!nodraw) { |
| drawHandler(); |
| } |
| }; |
| /* Adds an array of keys to the legend, and makes them visible |
| * on the chart.*/ |
| this.addMany = function(ary) { |
| ary.forEach(function(a) { _this.add(a.key, a.color, true); }); |
| drawHandler(); |
| }; |
| }, |
| debug: function() { |
| return [internalLegend, externalLegend, legendBody]; |
| } |
| }; |
| })(); |
| |
| |
| /** Attempts to merge requests for the same resource, waits 50 ms before sending |
| * a request */ |
| var jsonRequest = (function() { |
| var waitingHandlers = []; |
| var freshFiles = []; |
| |
| function makeRequest(uri, callback) { |
| var ref = {uri: uri, callbacks: [callback]}; |
| waitingHandlers.push(ref); |
| |
| var removeSelf = function() { |
| var idx = waitingHandlers.indexOf(ref); |
| assert_(idx != -1); |
| waitingHandlers.splice(idx, 1); |
| }; |
| |
| loadJSON(ref.uri, function(data) { |
| console.log('jsonRequest: ' + ref.uri + ' received'); |
| var freshIdx = getComponent(freshFiles, 'uri').indexOf(uri); |
| if(freshIdx == -1) { |
| freshFiles.push({uri: uri, time: Date.now(), data: data}); |
| } else { |
| freshFiles[freshIdx].data = data; |
| freshFiles[freshIdx].time = Date.now(); |
| } |
| ref.callbacks.forEach(function(callback) { |
| assert_(callback); |
| callback(data, true); |
| }); |
| removeSelf(); |
| }, function() { |
| ref.callbacks.forEach(function(callback) { |
| assert_(callback); |
| callback(data, false); |
| }); |
| removeSelf(); |
| }); |
| } |
| return { |
| askFor: function(uri, callback) { |
| var idx = getComponent(waitingHandlers, 'uri').indexOf(uri); |
| var freshIdx = getComponent(freshFiles, 'uri').indexOf(uri); |
| if(idx != -1) { |
| // Add to to that handler's callbacks. |
| waitingHandlers[idx].callbacks.push(callback); |
| } else if(freshIdx != -1) { |
| if(Date.now() - freshFiles[freshIdx].time < 5*60*1000) { |
| // Good enough |
| callback(freshFiles[freshIdx].data, true); |
| return; |
| } else { |
| // Not fresh enough any more |
| freshFiles.splice(freshIdx, 1); |
| makeRequest(uri, callback); |
| } |
| } else { |
| makeRequest(uri, callback); |
| } |
| }, |
| forceReload: function(uri, callback) { |
| var freshIdx = getComponent(freshFiles, 'uri').indexOf(uri); |
| while(freshIdx != -1) { |
| freshFiles.splice(freshIdx, 1); |
| freshIdx = getComponent(freshFiles, 'uri').indexOf(uri); |
| } |
| makeRequest(uri, callback); |
| }, |
| askForTile: function(scale, tileNumber, dataset, |
| options, callback, forcerefresh) { |
| if(!forcerefresh) { |
| // FUTURE: Use other passed in data |
| this.askFor('json/' + dataset /* + makeArgs(options) */, callback); |
| } else { |
| // FUTURE: Use other passed in data |
| this.forceRefresh('json/' + dataset /* + makeArgs(options) */, callback); |
| } |
| }, |
| debug: function() { |
| return [freshFiles, waitingHandlers]; |
| } |
| }; |
| })(); |
| |
| |
| // Stores a set of traces for a single key, over all tiles and ranges. The |
| // constuctor takes in a set of data to start it off. |
| function Trace(newData, newTileID, newScale) { |
| var data = []; |
| data[parseInt(newScale)] = []; |
| data[parseInt(newScale)][parseInt(newTileID)] = newData.slice(); |
| // Will replace with shallow copy if not performant |
| // FUTURE: Assumes input data is sorted |
| |
| function getscales() { |
| var result = []; |
| for(var key in data) { |
| if(data.hasOwnProperty(key)) { |
| result.push(parseInt(key)); |
| } |
| } |
| return result; |
| } |
| |
| function getTiles(scale) { |
| if(!data[scale]) { |
| return []; |
| } |
| var result = []; |
| for(var key in data[scale]) { |
| if(data[scale].hasOwnProperty(key)) { |
| result.push(parseInt(key)); |
| } |
| } |
| return result; |
| } |
| |
| /* Adds a set of data to the trace.*/ |
| this.add = function(newData, tileId, scale) { |
| assert_(newData.length > 0); |
| if(!data[scale]) { |
| data[scale] = []; |
| } |
| if(!data[scale][tileId] || (newData.length >= data[scale][tileId].length)) { |
| data[scale][tileId] = newData.slice(); // FUTURE: If too slow, replace with |
| // shallow copy |
| } |
| }; |
| |
| this.get = function(tileId, scale) { |
| return (data[scale] && data[scale][tileId]) || []; |
| }; |
| |
| this.getRange = function(start, end, scale) { |
| // FUTURE: Add support for downsampling on scale mismatch |
| assert_(start <= end); |
| var results = []; |
| var tiles = data[scale]; |
| getTiles(scale).forEach(function(tileIdx) { |
| if(tiles[tileIdx][0][0] <= end && last(tiles[tileIdx])[0] >= start) { |
| var result = []; |
| var i = 0; |
| while(tiles[tileIdx][i][0] < start) { |
| assert_(i < tiles[tileIdx].length); |
| i++; |
| } |
| if(i > 0) { |
| // Add one just before the range if possible |
| i--; |
| } |
| while(i < tiles[tileIdx].length && tiles[tileIdx][i][0] <= end) { |
| result.push(tiles[tileIdx][i]); |
| i++; |
| } |
| if(i < tiles[tileIdx].length) { |
| // Also add one just after |
| result.push(tiles[tileIdx][i]); |
| } |
| results.push(result); |
| } |
| }); |
| return results; |
| } |
| |
| /* Returns true if the trace has data in that tile and scale.*/ |
| this.contains = function(tileid, scale) { |
| return !!(data[scale] && data[scale][tileid]); |
| }; |
| |
| this.debug = function() { |
| return data; |
| }; |
| } |
| |
| |
| var traceDict = (function() { |
| var cache = new PagedDictionary(); |
| // A dictionary of keys to Trace objects |
| |
| function loadData(data, tileId, dataset, scale) { |
| console.log('traceDict: loadData called. tileId = ' + tileId + |
| ', scale = ' + scale); |
| // Look for the key in the data, and store that. If no key specified, store |
| // as much data as possible. |
| assert_(data['traces'] && data['commits']); |
| |
| var commitAry = getComponent(data['commits'], 'commit_time'); |
| data['traces'].forEach(function(trace) { |
| var newKey = schema.makeLegendKey(trace); |
| var processedData = []; |
| for(var i = 0; i < trace['values'].length; i++) { |
| if(trace['values'][i] < 1e+99) { |
| processedData.push([commitAry[i], trace['values'][i]]); |
| } |
| } |
| if(cache.has(newKey)) { |
| cache.get(newKey).add(processedData, tileId, scale); |
| } else { |
| cache.add(newKey, new Trace(processedData, tileId, scale)); |
| } |
| }); |
| } |
| return { |
| getTraces: function(toGet, callback) { |
| var result = {}; |
| var count = toGet.length; |
| var writeResults = function(key, data) { |
| result[key] = data; |
| count--; |
| if(count <= 0) { |
| callback(result); |
| } |
| }; |
| var failResults = function() { |
| count--; |
| if(count <= 0) { |
| callback(result); |
| } |
| }; |
| toGet.forEach(function(metadata) { |
| // NOTE: Assumes getTileNumbers returns in numeric order |
| commitDict.getTileNumbers(metadata.range, function(tileData) { |
| var tileNums = tileData[0]; |
| var scale = tileData[1]; |
| var tileCount = tileNums.length; |
| var tileData = []; |
| var writeSegment = function(tileId, data) { |
| tileData.push.apply(tileData, data); |
| tileCount--; |
| if(tileCount <= 0) { |
| writeResults(metadata.key, tileData); |
| } |
| }; |
| var writeFromCache = function(tileId) { |
| writeSegment(tileId, cache.get(metadata.key).get(tileId, scale)); |
| }; |
| tileNums.forEach(function(tileId) { |
| if(cache.has(metadata.key) && |
| cache.get(metadata.key).contains(tileId, scale)) { |
| //console.log('trace segment ' + metadata.key + ':' + |
| //tileId + ':' + scale + ' found in cache'); |
| writeFromCache(tileId); |
| } else { |
| //console.log('traceDict.getTraces: line ' + metadata.key + ' not cached.'); |
| var dataset = metadata.key.split(':')[0]; |
| jsonRequest.askForTile(scale, tileId, dataset, {} /* individual trace set here */, |
| function(data) { |
| loadData(data, tileId, dataset, scale); |
| // FUTURE: Check to see if this causes race conditions? |
| if(cache.has(metadata.key) && |
| cache.get(metadata.key).contains(tileId, scale)) { |
| //console.log('trace segement ' + metadata.key + ':' + |
| //tileId + ':' + scale + ' found after loading'); |
| writeFromCache(tileId); |
| } else { |
| tileCount--; |
| console.log('This line appears to not be in selected tile.'); |
| } |
| }); |
| } |
| }); |
| }); |
| }); |
| }, |
| /* Returns the number of traces that are cached from the array of lines |
| * passed in.*/ |
| countCached: function(keyArray) { |
| var count = 0; |
| keyArray.forEach(function(key) { |
| if(cache.has(key)) { count ++; } |
| }); |
| return count; |
| }, |
| init: function() { |
| console.log('Initializing traceDict'); |
| // Nothing here; data's loaded when requested |
| }, |
| debug: function() { |
| return cache; |
| } |
| }; |
| })(); |
| |
| |
| var commitDict = (function() { |
| var dataDict = new PagedDictionary(); |
| // Uses timestamp plus scale as keys, {hash, commit_msg, blamelist}, etc as values |
| var callbacks = new Vector(); |
| |
| /* Calls a function for each non empty element of callbackentries. |
| * If the function returns true, then after iterating, remove that |
| * element. */ |
| function iterateAndPopCallback(fun) { |
| var toRemove = callbacks.map(function(e, idx) { |
| if(fun(e)) { |
| return idx; |
| } else { |
| return null; |
| } |
| }).filter(function(e) {return e != null;}); |
| // Iterate in reverse, since removing an element will only disturb the |
| // ones with a higher index than it |
| for(var i = toRemove.length - 1; i >= 0; i--) { |
| callbacks.pop(toRemove[i]); |
| } |
| } |
| |
| return { |
| /* Gets all the data associated with a timestamp.*/ |
| getAssociatedData: function(timestamp, scale, callback) { |
| if(dataDict.has(scale) && dataDict.get(scale).has(timestamp)) { |
| return dataDict.get(scale).get(timestamp); |
| } else if(callback) { |
| callbacks.push({lookup: timestamp, scale: scale, callback: callback}); |
| return null; |
| } |
| }, |
| /* Call the callback with the hash for the given timestamp.*/ |
| timestampToHash: function(timestamp, scale, callback) { |
| var res = this.getAssociatedData(timestamp, scale, function(res) { |
| assert_(res && res.hash); |
| callback(res.hash); |
| }); |
| return res && res.hash; |
| }, |
| /* Updates the dictionaries with the JSON data.*/ |
| update: function(data, isManifest) { |
| console.log('commitDict.update called: isManifest=' + isManifest); |
| console.log(data); |
| // Load new data, then see if any of the callbacks are now valid |
| if(isManifest) { |
| // it's a manifest JSON |
| // FUTURE |
| assert_(false, "Unimplemented"); |
| } else { |
| // it's a tile JSON |
| // FUTURE: Remove hack when the JSON has the right format |
| data['scale'] = data['scale'] || 0; |
| |
| assert_(data && data['commits']); // FUTURE: add: && data['scale']); |
| var commits = data['commits']; |
| var scale = 0; // FUTURE: Replace with: parseInt(data['scale']); |
| commits.forEach(function(commit) { |
| assert_(commit['commit_time']); |
| if(!dataDict.has(scale)) { |
| dataDict.add(scale, new PagedDictionary()); |
| } |
| if(!dataDict.get(scale).has(commit['commit_time'])) { |
| dataDict.get(scale).add(parseInt(commit['commit_time']), commit); |
| } |
| }); |
| } |
| iterateAndPopCallback(function(entry) { |
| assert_(entry.callback); |
| if(callbackObject.hasOwnProperty('lookup')) { // Then it's a timestamp look up |
| var res = getAssociatedData(entry.timestamp, entry.scale, null); |
| if(res != null) { |
| entry.callback(res); |
| } |
| return res != null; // Remove the entry if the get was successful |
| } |
| return false; |
| }); |
| }, |
| /* Looks only through the available commit data, returning the array |
| * of values foundIt returns true on.*/ |
| lazySearch: function(foundIt) { |
| var searchSpace = dataDict.index(); |
| var results = []; |
| searchSpace.forEach(function(key) { |
| var maybeResult = dataDict.get(key); |
| if(foundIt(maybeResult)) { |
| results.push(maybeResult); |
| } |
| }); |
| return results; |
| }, |
| /* It'll call the callback when it can pass the tile numbers and scale |
| * for the range.*/ |
| getTileNumbers: function(range, callback) { |
| // FUTURE: Actually make work when we have tiles |
| callback([[0], 0]); |
| }, |
| /* Called on start up.*/ |
| init: function() { |
| console.log('Initializing commitDict'); |
| var _this = this; |
| // FUTURE: Also load manifest |
| jsonRequest.askForTile(0, -1, 'skps', {'use_commit_data': true}, |
| function(data, success) { |
| if(success) { |
| _this.update(data); |
| } else { |
| console.log('traceDict.init failed to retrieve data'); |
| } |
| }); |
| }, |
| debug: function() { |
| return [dataDict, callbacks]; |
| } |
| }; |
| })(); |
| |
| |
| var schema = (function() { |
| var KEY_DELIMITER = ':'; |
| var currentDataset; |
| var schemaDict = new PagedDictionary(); |
| var hiddenChildren = new PagedDictionary(); |
| var oldKeyToLegendKey = {}; |
| // Contains a dictionary of dictionary of config key-values |
| var validKeyParts = { |
| 'micro': ['dataset', 'builderName', 'system', 'testName', 'gpuConfig', |
| 'measurementType'], |
| 'skps': ['dataset', 'builderName', 'benchName', 'config', |
| 'scale', 'measurementType'] |
| }; |
| var keysWithEmpty = ['config']; |
| var lineList = []; |
| // List of all lines keys |
| |
| // Makes sure the children of root match the given model |
| // Model has the format |
| // [ |
| // { |
| // nodeType: <string>, // Required |
| // id: <string>, |
| // style: { ... }, |
| // attributes: { ... }, |
| // text: <string>, |
| // children: [models] |
| // } |
| // ] |
| function diffReplace(root, model) { |
| var checkSet = function(obj, fieldName, value) { |
| if(obj[fieldName] != value) { |
| obj[fieldName] = value; |
| } |
| } |
| var checkSetAttr = function(node, fieldName, value) { |
| if(node.getAttribute(fieldName) != value) { |
| node.setAttribute(fieldName, value); |
| } |
| } |
| var specialCases = ['children', 'text', 'attributes', 'style']; |
| for(var i = 0; i < model.length; i++) { |
| assert_(model[i].nodeType); |
| if(i >= root.children.length || |
| root.children[i].nodeName != model[i].nodeType.toUpperCase()) { |
| root.appendChild(document.createElement(model[i].nodeType)); |
| } |
| var curChild = root.children[i]; |
| for(var name in model[i]) { |
| if(model[i].hasOwnProperty(name) && specialCases.indexOf(name) == -1) { |
| checkSet(curChild, name, model[i][name]); |
| } |
| } |
| if(model[i].text) { checkSet(curChild, 'innerText', model[i].text); } |
| for(var styleName in model[i].style) { |
| if(model[i].style.hasOwnProperty(styleName)) { |
| checkSet(curChild.style, styleName, model[i].style[styleName]); |
| } |
| } |
| for(var attrName in model[i].attributes) { |
| if(model[i].attributes.hasOwnProperty(attrName)) { |
| checkSetAttr(curChild, attrName, model[i].attributes[attrName]); |
| } |
| } |
| if(model[i].children) { |
| diffReplace(curChild, model[i].children); |
| } |
| } |
| // Get rid of extras |
| while(root.length > model.length) { |
| root.removeChild(last(root.children)); |
| } |
| } |
| |
| function getOptions() { |
| var keyParts = $$('#line-table select'); |
| var options = {}; |
| keyParts.forEach(function(keyPart) { |
| partName = keyPart.id.slice(0, -'-results'.length); |
| $$('option:checked', keyPart).forEach(function(selectedOption) { |
| if(!options[partName]) { options[partName] = []; } |
| options[partName].push(selectedOption.value); |
| }); |
| }); |
| options['dataset'] = currentDataset; |
| return options; |
| } |
| |
| return { |
| oldToLegend: function(key) { |
| return oldKeyToLegendKey[key]; |
| }, |
| /* Returns a string given the key elements in trace.*/ |
| makeLegendKey: function(trace, dataset) { |
| assert_(trace['params']); |
| if(!dataset) { |
| assert_(trace['params']['dataset']); |
| dataset = trace['params']['dataset']; |
| } else if(!trace['params']['dataset']) { |
| trace['params']['dataset'] = dataset; |
| } |
| assert_(validKeyParts[dataset]); |
| return validKeyParts[dataset].map(function(part) { |
| return trace['params'][part] || ''; |
| }).join(KEY_DELIMITER); |
| }, |
| /* Updates the schema given data in the JSON input.*/ |
| update: function(data, datasetName) { |
| console.log('schema.update called: datasetName=' + datasetName); |
| console.log(data); |
| assert_(data['param_set']); |
| // Update internal structure |
| if(!schemaDict.has(datasetName)) { |
| schemaDict.add(datasetName, new PagedDictionary()); |
| } |
| var keys = []; |
| for(var key in data['param_set']) { |
| if(data['param_set'].hasOwnProperty(key) && |
| validKeyParts[datasetName].indexOf(key) != -1) { |
| keys.push(key); |
| } |
| } |
| keys.forEach(function(key) { |
| if(schemaDict.get(datasetName).has(key)) { |
| var newParams = data['param_set'][key].filter(function(param) { |
| return schemaDict.get(datasetName).get(key).indexOf(param) == -1; |
| }); |
| schemaDict.get(datasetName).get(key).push.apply(newParams); |
| } else { |
| schemaDict.get(datasetName).add(key, data['param_set'][key]); |
| } |
| if(keysWithEmpty.indexOf(key) != -1 && |
| schemaDict.get(datasetName).get(key).indexOf('') == -1) { |
| schemaDict.get(datasetName).get(key).push(''); |
| } |
| }); |
| |
| if(data['traces']) { |
| data['traces'].forEach(function(trace) { |
| lineList.push(this.makeLegendKey(trace, datasetName)); |
| oldKeyToLegendKey[trace['key']] = this.makeLegendKey(trace, datasetName); |
| }, this); |
| } |
| loadShortcut(); |
| this.updateDOM(); |
| }, |
| /* Given the input options, returns the ones that define real traces and |
| * their number. |
| * options is a dictionary keyed with the key part name and has the |
| * selected values as its value.*/ |
| getValidOptions: function(options, dataset) { |
| // FUTURE: Replace with tree if not performing well enough |
| assert_(validKeyParts[dataset]); |
| var mapOptions = function(key) { |
| // Return the split string if it's valid, false otherwise. |
| var parts = key.split(KEY_DELIMITER); |
| return key.split(KEY_DELIMITER).every(function(part, idx) { |
| return !options[validKeyParts[dataset][idx]] || |
| options[validKeyParts[dataset][idx]].indexOf(part) != -1; |
| }) && parts; |
| }; |
| var validLines = lineList.map(mapOptions); |
| var optionDicts = {}; |
| var count = 0; |
| // Get all the valid options |
| for(var i = 1; i < validKeyParts.length; i++) { |
| var tmpDict = {}; |
| for(var j = 0; j < validLines.length; j++) { |
| if(validLines) { |
| //O(1) set addition! There's a little latency on the microbench |
| // options bar, hopefully this helps with that.. |
| tmpDict[validLines[j][i]] = true; |
| count++; |
| } |
| } |
| var tmpOptions = []; |
| for(var k in tmpDict) { |
| if(tmpDict.hasOwnProperty(k)) { |
| tmpOptions.push(k); |
| } |
| } |
| optionDicts[validKeyParts[i]] = tmpOptions; |
| } |
| return [optionDicts, count]; |
| }, |
| /* Returns a list of valid lines given the selected options.*/ |
| getValidLines: function(options, dataset) { |
| // FUTURE: Replace with tree if not performing well enough |
| assert_(validKeyParts[dataset]); |
| var mapOptions = function(key) { |
| // Return the split string if it's valid, false otherwise. |
| var parts = key.split(KEY_DELIMITER); |
| return parts.every(function(part, idx) { |
| return !options[validKeyParts[dataset][idx]] || |
| options[validKeyParts[dataset][idx]].indexOf(part) != -1; |
| }) && key; |
| }; |
| var validLines = lineList.map(mapOptions); |
| // console.log(validLines); |
| return validLines.filter(id); |
| }, |
| /* Updates the selection boxes to match the ones currently in the schema.*/ |
| updateDOM: function() { |
| assert_(currentDataset); |
| console.log('schema.updateDOM: start'); |
| if(!schemaDict.has(currentDataset)) { |
| console.log('schema.updateDOM: Schema for selected dataset not ' + |
| 'currently loaded; sending request.'); |
| var _this = this; |
| jsonRequest.askForTile(0, -1, currentDataset, {'get_params_data': true}, |
| function(data, success) { |
| if(success) { |
| console.log('schema.updateDOM: received data.'); |
| _this.update(data, currentDataset); |
| } |
| }); |
| return; |
| } |
| assert_($$$('#line-table')); |
| var inputRoot; |
| if($$$('#' + currentDataset + '-set')) { |
| inputRoot = $$$('#' + currentDataset + '-set'); |
| assert_(inputRoot.parentElement == $$$('#line-table')); |
| } else { |
| inputRoot = document.createElement('tr'); |
| inputRoot.id = currentDataset + '-set'; |
| } |
| var curDict = schemaDict.get(currentDataset); |
| curDict.index().forEach(function(part) { |
| curDict.get(part).sort(); |
| }); |
| var getWidth = function(part) { |
| var longestLine = Math.max.apply(null, |
| curDict.get(part).map(function(names) { |
| return names.length; |
| })); |
| return 0.75 * longestLine + 0.5; |
| } |
| var selectedValues = {}; |
| $$('select', inputRoot).forEach(function(opt) { |
| var partName = opt.id.slice(0, -'-results'.length); |
| selectedValues[partName] = {}; |
| $$('option', opt).forEach(function(maybeSelected) { |
| if(maybeSelected.selected) { |
| selectedValues[partName][maybeSelected.value] = true; |
| } |
| }); |
| }); |
| var makeSelectModel = function(part) { |
| return curDict.get(part).map(function(option) { |
| return { |
| nodeType: 'option', |
| value: option, |
| text: option.length > 0 ? option : '(none)', |
| selected: !!(selectedValues[part] && selectedValues[part][option]) |
| }; |
| }); |
| }; |
| diffReplace(inputRoot, validKeyParts[currentDataset].slice(1).map( |
| function(part) { |
| return { |
| nodeType: 'td', |
| children: [ |
| { |
| nodeType: 'input', |
| id: part + '-input', |
| style: { |
| width: getWidth(part) + 'em' |
| } |
| }, |
| { |
| nodeType: 'select', |
| id: part + '-results', |
| attributes: { |
| multiple: 'yes' |
| }, |
| style: { |
| width: getWidth(part) + 'em', |
| overflow: 'auto' |
| }, |
| children: makeSelectModel(part) |
| } |
| ] |
| }; |
| })); |
| // Hide the other ones |
| $$('tr', $$$('#line-table')).forEach(function(e) { |
| e.style.display = 'none'; |
| }); |
| $$$('#line-table').appendChild(inputRoot); |
| inputRoot.style.display = ''; |
| }, |
| |
| /* Greys out elements as needed. Doesn't grey out any more in the currentRow.*/ |
| updateDisabledDOM: function(currentRow) { |
| // Currently unimplemented because it seems like people found it confusing |
| // TODO: Fix greyed out areas |
| }, |
| |
| /* Grabs all the possible line names.*/ |
| init: function() { |
| console.log('Initializing schema'); |
| // Load line metadata, update client controls |
| currentDataset = getArg('set') || 'skps'; |
| jsonRequest.askForTile(0, -1, currentDataset, {include_commit_data: true}, |
| function(data, success) { |
| if(success) {_this.update(data, currentDataset);} |
| }); |
| var _this = this; |
| var updateLineCount = function() { |
| var lines = _this.getValidLines(getOptions(), currentDataset); |
| var count = lines.length; |
| var cacheCount = traceDict.countCached(lines); |
| $$$('#line-num').innerHTML = count + ' valid lines, ' + |
| cacheCount + ' cached lines'; |
| }; |
| $$('input[name=\'schema-type\']').forEach(function(e) { |
| console.log('schema: Adding event listener for ' + e.value); |
| console.log(e); |
| if(e.value == currentDataset) { e.checked = true; } |
| e.addEventListener('change', function() { |
| console.log('schema change'); |
| var newSchema = this.value; |
| console.log(newSchema); |
| currentDataset = newSchema; |
| _this.updateDOM(); |
| updateLineCount(); |
| }); |
| }); |
| $$$('#add-lines').addEventListener('click', function(e) { |
| // Find relevant lines, add to legend and plot |
| var options = getOptions(); |
| var lines = _this.getValidLines(options, currentDataset).map(function(l) { |
| return {key: l, color: 'white'}; |
| }); |
| legend.addMany(lines); |
| // console.log(lines); |
| /* |
| lines.forEach(function(line) { |
| legend.add(line, 'white'); |
| }); |
| */ |
| plotData.data(plotData.makePlotCallback()); |
| }); |
| // Attach event listeners to parent of all schema nodes |
| $$$('#line-table').addEventListener('input', function(e) { |
| console.log('line-table: input event listener called'); |
| // console.log(e); |
| var inputId = e.target.id.slice(0,-'-input'.length); |
| console.log('called for ' + e.target.id); |
| if(e.target.nodeName == 'INPUT') { |
| if(!currentDataset) { |
| console.log('line-table.input: no schema currently loaded. Ignoring.'); |
| return; |
| } |
| var query = e.target.value; |
| var dataset = schemaDict.get(currentDataset).get(inputId); |
| assert_(dataset != null); |
| var results = dataset.filter(function(candidate) { |
| return candidate.indexOf(query) != -1; |
| }); // FUTURE: If this is too slow, swap with binary search |
| if (results.length < 1) { |
| matchLengths = dataset.map(function(candidate) { |
| var maxMatch = 0; |
| for(var start = 0; start < candidate.length; start++) { |
| var i = 0; |
| for (; start + i < candidate.length && i < query.length; i++) { |
| if (candidate[start + i] != query[i]) { |
| break; |
| } |
| } |
| if(i > maxMatch) { |
| maxMatch = i; |
| } |
| } |
| return maxMatch; |
| }); |
| maxMatch = Math.max.apply(null, matchLengths); |
| results = dataset.filter(function(_, idx) { |
| return matchLengths[idx] >= maxMatch; |
| }); |
| } |
| console.log('search results: '); |
| console.log(results); |
| if(!hiddenChildren.has(currentDataset)) { |
| hiddenChildren.add(currentDataset, new PagedDictionary()); |
| } |
| if(!hiddenChildren.get(currentDataset).has(inputId)) { |
| hiddenChildren.get(currentDataset).add(inputId, []); |
| } |
| var hiddenResults = hiddenChildren.get(currentDataset). |
| get(inputId); |
| var removed = []; |
| // Insert appropriate nodes back into the array |
| hiddenResults.forEach(function(e, idx) { |
| if(results.indexOf(e.value) != -1) { |
| var resultsChildren = $$$('#' + inputId + '-results').children; |
| for(var i = 0; i < resultsChildren.length; i++) { |
| if(resultsChildren[i].value > e.value) { |
| $$$('#' + inputId + '-results').insertBefore( |
| e, resultsChildren[i]); |
| removed.push(idx); |
| return; |
| } |
| } |
| $$$('#' + inputId + '-results').insertBefore(e, null); |
| removed.push(idx); |
| } |
| }); |
| for(var i = removed.length - 1; i >= 0; i--) { |
| hiddenResults.splice(removed[i], 1); |
| } |
| $$('#' + inputId + '-results option').forEach(function(e) { |
| if(results.indexOf(e.value) != -1) { |
| e.style.display = ''; |
| } else { |
| hiddenResults.push(e); |
| e.parentNode.removeChild(e); |
| } |
| }); |
| console.log('hidden nodes:'); |
| console.log(hiddenResults); |
| } |
| }); |
| $$$('#line-table').addEventListener('click', function(e) { |
| console.log('#line-table.click called'); |
| //console.log(e); |
| if(e.target.nodeName == 'OPTION') { |
| console.log('#line-table select.click event listener called'); |
| // Update the line count |
| updateLineCount(); |
| } |
| }); |
| }, |
| debug: function() { |
| return [schemaDict, currentDataset, lineList]; |
| } |
| }; |
| })(); |
| |
| |
| var history = (function() { |
| // TODO: Re-add history features, etc. |
| })(); |
| |
| |
| /** Returns the datetime compatible version of a POSIX timestamp.*/ |
| function toRFC(timestamp) { |
| // Slice off the ending 'Z' |
| return new Date(timestamp*1000).toISOString().slice(0, -1); |
| } |
| |
| |
| var plotData = (function() { |
| var plotRef; |
| var isLogPlot; |
| var visibleKeys = new Vector(); |
| function getVisibleKeys() { |
| return visibleKeys.all(); |
| } |
| function toZoomValue(min, max) { |
| return 60*60/(max - min); |
| } |
| function fromZoomValue(curMin, curMax, zoomValue) { |
| if(zoomValue < 0.005) { |
| zoomValue = 0.005; |
| } |
| var range = 60*60/zoomValue; |
| var midpoint = 0.5*(curMin + curMax); |
| return [midpoint - range/2, midpoint + range/2]; |
| } |
| function plotClickHandler(evt, pos, item) { |
| var notePad = $('#note'); |
| if(!item) { |
| notePad.hide(); |
| return; |
| } |
| notePad.hide(); |
| notePad.css({'top': item.pageY + 10, 'left': item.pageX}); |
| commitDict.getTileNumbers(getCurRange(), function(tileData) { |
| var postNote = function(commitData) { |
| console.log(commitData); |
| var hashMsg = ''; |
| var commitMsg = ''; |
| var authorMsg = ''; |
| if(commitData['hash']) { |
| var hash = commitData['hash']; |
| hashMsg = 'hash: <a href=https://github.com/google/skia/commit/' + |
| hash + ' target=_blank>' + hash + '</a><br />'; |
| } |
| if(commitData['commit_msg']) { |
| commitMsg = 'commit message: ' + commitData['commit_msg'] + |
| '<br />'; |
| } |
| if(commitData['author']) { |
| authorMsg = 'author: ' + commitData['author'] + '<br />'; |
| } |
| $$('#note #data')[0].innerHTML = ( |
| hashMsg + |
| 'timestamp: ' + item.datapoint[0] + '<br />' + |
| 'value: ' + item.datapoint[1] + '<br />' + |
| authorMsg + commitMsg |
| ); |
| notePad.show(); |
| }; |
| var relatedData = commitDict.getAssociatedData( |
| item.datapoint[0], tileData[1], postNote); |
| if(relatedData) { |
| postNote(relatedData); |
| } |
| }); |
| } |
| function plotZoomHandler(evt, pos, item) { |
| var range = getCurRange(); |
| $$$('#zoom').value = toZoomValue(range[0], range[1]); |
| $$$('#start').value = toRFC(range[0]); |
| $$$('#end').value = toRFC(range[1]); |
| // TODO: Get more plot data as needed; see if the new range |
| // requires new tiles, and if so call data(makePlotCallback()) |
| } |
| function plotPanHandler(evt, pos, item) { |
| var range = getCurRange(); |
| $$$('#start').value = toRFC(range[0]); |
| $$$('#end').value = toRFC(range[1]); |
| // TODO: Get more plot data as needed; see if the new range |
| // requires new tiles, and if so call data(makePlotCallback()) |
| } |
| /* Returns the current visible range. Null to load the latest tile.*/ |
| function getCurRange() { |
| var data = plotRef.getData(); |
| var xaxis = plotRef.getOptions().xaxes[0]; |
| var min = null; |
| var max = null; |
| if(xaxis.min != null && xaxis.max != null) { |
| min = xaxis.min; |
| max = xaxis.max; |
| } else if(plotRef.getData().length > 0) { |
| min = Math.min.apply(null, data.map(function(set) { |
| return Math.min.apply(null, set.data.map(function(point) { |
| return point[0]; |
| })); |
| })); |
| max = Math.max.apply(null, data.map(function(set) { |
| return Math.max.apply(null, set.data.map(function(point) { |
| return point[0]; |
| })); |
| })); |
| } |
| return [min, max]; |
| } |
| |
| return { |
| /* Initializes the plot.*/ |
| init: function() { |
| console.log('Initializing plotData'); |
| isLogPlot = false; |
| plotRef = $('#chart').plot([], |
| { |
| legend: { |
| show: false |
| }, |
| grid: { |
| hoverable: true, |
| autoHighlight: true, |
| mouseActiveRadius: 16, |
| clickable: true, |
| markings: this.getMarkings |
| }, |
| xaxis: { |
| ticks: function(axis) { |
| var range = axis.max - axis.min; |
| // Different possible tick intervals, ranging from a second to |
| // about a year |
| var scaleFactors = [1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 2*60, |
| 4*60, 5*60, 15*60, 20*60, 30*60, |
| 60*60, 2*60*60, 3*60*60, 4*60*60, |
| 5*60*60, 6*60*60, 12*60*60, 24*60*60, |
| 7*24*60*60, 30*24*60*60, 2*30*24*60*60, |
| 4*30*24*60*60, 6*30*24*60*60, 365*24*60*60]; |
| var MAX_TICKS = 5; |
| var i = 0; |
| while(range/scaleFactors[i] > MAX_TICKS && i < scaleFactors.length) { |
| i++; |
| } |
| var scaleFactor = scaleFactors[i]; |
| var cur = scaleFactor*Math.ceil(axis.min/scaleFactor); |
| var ticks = []; |
| do { |
| var tickDate = new Date(cur*1000); |
| var formattedTime = tickDate.toString(); |
| if(scaleFactor >= 24*60*60) { |
| formattedTime = tickDate.toDateString(); |
| } else { |
| // FUTURE: Find a way to make a string with only the hour or minute |
| formattedTime = tickDate.toDateString() + '<br \\>' + |
| tickDate.toTimeString(); |
| } |
| ticks.push([cur, formattedTime]); |
| cur += scaleFactor; |
| } while(cur < axis.max); |
| return ticks; |
| } |
| }, |
| yaxis: { |
| /* zoomRange: false */ |
| transform: function(v) { return isLogPlot? Math.log(v) : v; }, |
| inverseTransform: function(v) { return isLogPlot? Math.exp(v) : v; } |
| }, |
| crosshair: { |
| mode: 'xy' |
| }, |
| zoom: { |
| interactive: true |
| }, |
| pan: { |
| interactive: true, |
| frameRate: 60 |
| } |
| }).data('plot'); |
| $('#chart').bind('plotclick', plotClickHandler); |
| $('#chart').bind('plotzoom', plotZoomHandler); |
| $('#chart').bind('plotpan', plotPanHandler); |
| |
| // Initialize zoom ranges |
| $$$('#zoom').setAttribute('min', 0); |
| $$$('#zoom').setAttribute('max', 1); |
| $$$('#zoom').setAttribute('step', 0.001); |
| |
| // Zoom binding |
| $$$('#zoom').addEventListener('input', function() { |
| var curRange = getCurRange(); |
| var newRange = fromZoomValue(curRange[0], curRange[1], |
| $$$('#zoom').value); |
| var xaxis = plotRef.getOptions().xaxes[0]; |
| xaxis.min = newRange[0]; |
| xaxis.max = newRange[1]; |
| plotRef.setupGrid(); |
| plotRef.draw(); |
| }); |
| |
| // Set up go button binding |
| $$$('#back-to-the-future').addEventListener('click', function(e) { |
| var newMin = Date.parse($$$('#start').value)/1000; |
| var newMax = Date.parse($$$('#end').value)/1000; |
| if(isNaN(newMin) || isNaN(newMax)) { |
| console.log('#back-to-the-future.click: invalid input'); |
| } else { |
| var realMin = Math.min(newMin, newMax); |
| var realMax = Math.max(newMin, newMax); |
| var xaxis = plotRef.getOptions().xaxes[0]; |
| xaxis.min = realMin; |
| xaxis.max = realMax; |
| plotRef.setupGrid(); |
| plotRef.draw(); |
| } |
| }); |
| |
| $$$('#islog').addEventListener('click', function(e) { |
| var willLogPlot = $$$('#islog').checked; |
| if(isLogPlot != willLogPlot) { |
| isLogPlot = willLogPlot; |
| plotRef.setupGrid(); |
| plotRef.draw(); |
| } |
| }); |
| }, |
| /* Returns usable plot data.*/ |
| data: function(callback) { |
| var processAndCall = function(dataDict) { |
| console.log('data.get callback:'); |
| console.log(dataDict); |
| var results = []; |
| for(var trace in dataDict) { |
| if(dataDict.hasOwnProperty(trace)) { |
| // Convert data to Flot readable format |
| var curTrace = dataDict[trace]; |
| results.push({ |
| label: trace, |
| data: curTrace |
| }); |
| } |
| } |
| //console.log(results); |
| callback(results); |
| }; |
| //console.log('visible keys: '); |
| //console.log(visibleKeys.all()); |
| var currentRange = getCurRange(); |
| // Process keys before passing to traceDict |
| var processedKeys = visibleKeys.all().map(function(key) { |
| return { |
| key: key, |
| range: currentRange |
| }; |
| }); |
| traceDict.getTraces(processedKeys, processAndCall); |
| }, |
| getMarkings: function() { |
| if(visibleKeys.all().length <= 0) { return []; } |
| return []; |
| var skpPhrase = 'Update SKP version to '; |
| var updates = commitDict.lazySearch(function(commitData) { |
| return commitData['commit_msg'] && |
| commitData['commit_msg'].slice(0, skpPhrase) == skpPhrase; |
| }).map(function(commitData) { |
| return [parseInt(commitData['commit_msg'].substr(skpPhrase.length)), |
| commitData['commit_time']]; |
| }); |
| var markings = []; |
| for(var i = 1; i < updates.length; i++) { |
| if(updates[i][0] % 2 == 0) { |
| markings.push([updates[i-1][1], updates[i][1]]); |
| } |
| } |
| return markings.map(function(pair) { |
| return { xaxis: {from: pair[0], to: pair[1]}, color: '#cccccc'}; |
| }); |
| }, |
| /* Plots the given data.*/ |
| plot: function(data) { |
| // Plot the given data |
| plotRef.setData(data); |
| var options = plotRef.getOptions(); |
| options.xaxes.forEach(function(axis) { |
| axis.max = null; |
| axis.min = null; |
| }); |
| options.yaxes.forEach(function(axis) { |
| axis.max = null; |
| axis.min = null; |
| }); |
| plotRef.setupGrid(); |
| plotRef.draw(); |
| // Push changes to legend |
| legend.updateColors(plotRef); |
| legend.refresh(); |
| // Then push changes to zoom control and date controls |
| var range = this.getCurrentRange(); |
| $$$('#zoom').value = toZoomValue(range[0], range[1]); |
| $$$('#start').value = toRFC(range[0]); |
| $$$('#end').value = toRFC(range[1]); |
| }, |
| /* Produces a wrapper that can be used in any context.*/ |
| makePlotCallback: function() { |
| var _this = this; |
| return function(data) { |
| _this.plot(data); |
| }; |
| }, |
| /* Returns the current visible range. Null to load the latest tile.*/ |
| getCurrentRange: function() { |
| return getCurRange(); |
| }, |
| /* Sets the plot to display the key next time it is plotted.*/ |
| show: function(key) { |
| if(!visibleKeys.has(key)) { |
| visibleKeys.push(key); |
| } |
| }, |
| /* Hides a trace from the plot the next time it is plotted.*/ |
| hide: function(key) { |
| visibleKeys.remove(key); |
| }, |
| debug: function() { |
| return [plotRef, visibleKeys]; |
| } |
| }; |
| })(); |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| console.log('DOM loaded, init()ing components'); |
| commitDict.init(); |
| plotData.init(); |
| traceDict.init(); |
| schema.init(); |
| legend.init(function(key) { |
| plotData.show(key); |
| }, function(key) { |
| plotData.hide(key); |
| }, function() { |
| plotData.data(plotData.makePlotCallback()); |
| }); |
| |
| document.body.addEventListener('click', function(e) { |
| if(!$(e.target).parents().is('#note,#chart')) { |
| $('#note').hide(); |
| } |
| }); |
| |
| |
| $('#note').hide(); |
| $('#notification').hide(); |
| }); |
| |
| // Very, very hackish, since this is a temporary fix until the new world order is established |
| var loadShortcut = (function() { |
| var alreadyDone = false; |
| return function(doAnyways) { |
| if(getArg('shortcut')) { |
| |
| if(alreadyDone && !doAnyways) { |
| return; |
| } else { |
| alreadyDone = true; |
| } |
| |
| var shortcutId = parseInt(getArg('shortcut')); |
| loadJSON('/shortcuts/' + shortcutId, function(data) { |
| console.log(data); |
| if(!data['keys']) { |
| console.log('Invalid shortcut: ' + shortcutId); |
| return; |
| } |
| if(!doAnyways && data['keys'].indexOf('MICROBENCHMICROBENCHMICROBENCH') != -1) { |
| jsonRequest.askFor('json/micro', function(data) { |
| schema.update(data, 'micro'); |
| loadShortcut(true); |
| }); |
| } |
| console.log('Shortcut keys:'); |
| console.log(data); |
| var legendKeys = data['keys'].map(function(oldKey) { |
| return { |
| key: schema.oldToLegend(oldKey), |
| color: 'white' |
| }; |
| }); |
| legend.addMany(legendKeys.filter(function(k) {return !!k.key;})); |
| }); |
| } |
| }; |
| }()); |