Index: perf/dashboard/ui/js/graph.js |
=================================================================== |
--- perf/dashboard/ui/js/graph.js (revision 298504) |
+++ perf/dashboard/ui/js/graph.js (working copy) |
@@ -1,645 +0,0 @@ |
-/* |
- Copyright (c) 2012 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. |
-*/ |
- |
-/* |
- Fetch all graph data files, prepare the data, and create Plotter() to |
- generate a graph. |
- |
- To use: |
- var graph = new Graph('output_div', graphList) |
- graph.setTitle('Title'); |
- graph.graph(); |
-*/ |
- |
-function JsonToJs(data) { |
- return eval('(' + data + ')'); |
-} |
- |
-/** |
- * Insert element a after element b. |
- */ |
-function AppendChild(a, b) { |
- var elementA = (typeof(a) == 'object') ? a : document.getElementById(a); |
- var elementB = (typeof(b) == 'object') ? b : document.getElementById(b); |
- elementB.appendChild(elementA); |
-} |
- |
-/** |
- * Insert element a before element b. |
- */ |
-function InsertBefore(a, b) { |
- var elementA = (typeof(a) == 'object') ? a : document.getElementById(a); |
- var elementB = (typeof(b) == 'object') ? b : document.getElementById(b); |
- elementB.insertBefore(elementA); |
-} |
- |
- |
-/** |
- * Graph class. |
- * @constructor |
- * |
- * Fetch each graph in |graphList| and create Plotter(). Create Graph() |
- * and call graph() to display graph. |
- * |
- * @param div {String|DOMElement} The container that the graph should be |
- * rendered to. |
- * @param graphList {Array} List of graphs to be plotted. |
- * @param options {Object} Options to configure graph. |
- * - width {int} Width of graph. |
- * - height {int} Height of graph. |
- * - history {int} Number of row to show. |
- * - showDetail {Boolean} Specifies whether or not to display detail. |
- * Default false. |
- * - showTabs {Boolean} Specifies whether or not to show tabs. |
- * Default false. |
- * - enableMouseScroll {Boolean} Specifies whether or not to enable |
- * mouse wheel zooming. Default false. |
- * - channels {Array} Display graph by channels. |
- * - orderDataByVersion {Boolean} Order plot data by version number. |
- * Default false. |
- * |
- * Example of the graphList: |
- * [ |
- * {"units": "ms", "important": false, "name": "SunSpider-individual", |
- * "SunSpider-individual-summary.dat"}, |
- * ] |
- */ |
-function Graph(div, graphList, opt_options) { |
- this.graphList_ = graphList; |
- this.options_ = (opt_options) ? opt_options : {}; |
- this.history_ = (this.options_.history) ? this.options_.history : 150; |
- this.rev_ = (this.options_.rev) ? this.options_.rev : -1; |
- this.channels_ = (this.options_.channels) ? this.options_.channels : []; |
- this.firstTrace_ = ''; |
- this.rows_ = []; |
- this.isDetailViewAdded_ = false; |
- this.selectedGraph_ = null; |
- this.plotterDiv_ = null; |
- this.tabs_ = []; |
- this.width = this.options_.width; |
- this.height = this.options_.height; |
- |
- this.graphContainer = document.createElement('div'); |
- this.graphContainer.setAttribute( |
- 'style', 'display: block; overflow: hidden; ' + |
- 'margin: 5px; padding: 5px; width:' + this.width); |
- AppendChild(this.graphContainer, div); |
- |
- this.title = document.createElement('div'); |
- this.title.setAttribute('style', 'text-align: center'); |
- AppendChild(this.title, this.graphContainer); |
-} |
- |
-/** |
- * Start fetching graph data. |
- */ |
-Graph.prototype.graph = function() { |
- this.fetchSummary_(); |
- |
- if (this.options_.showTabs) |
- this.addTabs_(); |
-} |
- |
-/** |
- * Set graph title. |
- */ |
-Graph.prototype.setTitle = function(title) { |
- this.title.innerHTML = title; |
-} |
- |
-/** |
- * Display tabs for each graph. |
- */ |
-Graph.prototype.addTabs_ = function() { |
- this.tabs_ = []; |
- var tabPane = document.createElement('div'); |
- tabPane.setAttribute('class', 'switcher'); |
- AppendChild(tabPane, this.graphContainer); |
- |
- var graphNames = [] |
- var inserted = {}; |
- for (var i = 0; i < this.graphList_.length; i++) { |
- if (!inserted[this.graphList_[i].name]) { |
- graphNames.push(this.graphList_[i].name); |
- inserted[this.graphList_[i].name] = 1; |
- } |
- } |
- |
- var obj = this; |
- for (var i = 0; i < graphNames.length; i++) { |
- var name = graphNames[i]; |
- var tab = document.createElement('span'); |
- if (name != this.selectedGraph_.name) { |
- tab.setAttribute('class', 'select'); |
- } |
- tab.addEventListener( |
- "click", |
- (function(){ |
- var cur = name; return function() {obj.switchGraph_(cur)} |
- })(), |
- false); |
- tab.appendChild(document.createTextNode(name + " ")); |
- AppendChild(tab, tabPane); |
- this.tabs_.push(tab); |
- } |
-} |
- |
-/** |
- * Fetch graph summary data files. |
- */ |
-Graph.prototype.fetchSummary_ = function() { |
- this.rows_ = []; |
- if (!this.selectedGraph_) { |
- this.selectedGraph_ = this.graphList_[0]; |
- } |
- var graphFiles = []; |
- this.selectedGraphList_ = []; |
- for (var i = 0; i < this.graphList_.length; i++) { |
- if (this.graphList_[i].name == this.selectedGraph_.name) { |
- graphFiles.push(this.graphList_[i].loc); |
- this.selectedGraphList_.push(this.graphList_[i]); |
- } |
- } |
- var obj = this; |
- new FetchList(graphFiles, function(data) {obj.onSummaryReceived_(data)}); |
-} |
- |
-/** |
- * Call addPlot_ once all graph summary data are received. |
- */ |
-Graph.prototype.onSummaryReceived_ = function(data) { |
- // Parse the summary data file. |
- for (var i = 0; i < data.length; i++) { |
- if (data[i]) { |
- var rows = new Rows(data[i]); |
- this.rows_[i] = rows; |
- } |
- } |
- this.addPlot_(); |
-} |
- |
-/** |
- * Merge all data rows by channel and version. This is use in platform |
- * comparison graph. |
- * |
- * Example: |
- * Two rows: |
- * {"traces": {"score": ["777", "0.0"]}, "rev": "9", |
- * "ver": "17.1.963.19", "chan": "stable"} |
- * {"traces": {"score": ["888", "0.0"]}, "rev": "10", |
- * "ver": "17.1.963.19", "chan": "stable"} |
- * Become: |
- * {"traces": {"score_windows": ["777", "0.0"], |
- * "score_linux": ["888", "0.0"]}, |
- * "rev": "9", "ver": "17.1.963.19", "chan": "stable"} |
- * |
- * @return {Array} Array of rows. |
- */ |
-Graph.prototype.getMergedRowsByVersion_ = function() { |
- var channels = {}; |
- for (var i = 0; i < this.channels_.length; i++) |
- channels[this.channels_[i]] = 1; |
- var allRows = []; |
- // Combind all rows to one list. |
- for (var i = 0; i < this.rows_.length; i++) { |
- if (this.rows_[i]) { |
- for (var j = 0; j < this.rows_[i].length; j++) { |
- var row = this.rows_[i].get(j); |
- if (row && row.chan in channels) { |
- row.machine = this.selectedGraphList_[i].machine; |
- allRows.push(row); |
- } |
- } |
- } |
- } |
- |
- // Sort by version number. |
- allRows.sort( |
- function(a, b) { |
- var a_arr = a.version.split('.'); |
- var b_arr = b.version.split('.'); |
- var len = Math.min(b_arr.length, b_arr.length); |
- for (var i = 0; i < len; i++) { |
- if (parseInt(a_arr[i], 10) > parseInt(b_arr[i], 10)) |
- return 1; |
- else if (parseInt(a_arr[i], 10) < parseInt(b_arr[i], 10)) |
- return -1; |
- } |
- return a_arr.length - b_arr.length; |
- }); |
- |
- // Merge all rows by version number. |
- var combindedRows = []; |
- var index = 0; |
- while (index < allRows.length) { |
- var currentRow = allRows[index]; |
- var traces = currentRow['traces']; |
- for (var traceName in traces) { |
- var traceRenamed = traceName + '_' + currentRow.machine.toLowerCase(); |
- traces[traceRenamed] = traces[traceName]; |
- delete(traces[traceName]); |
- } |
- while (index < allRows.length - 1 && |
- allRows[index + 1].version == currentRow.version) { |
- var row = allRows[index + 1]; |
- var traces = row['traces']; |
- for (var traceName in traces) { |
- var traceRenamed = traceName + '_' + row.machine.toLowerCase(); |
- currentRow['traces'][traceRenamed] = traces[traceName]; |
- } |
- index++; |
- } |
- combindedRows.push(currentRow); |
- index++; |
- } |
- return combindedRows; |
-} |
- |
-/** |
- * Merge all channel data by their index in file. This is use in channel |
- * comparison graph. |
- * |
- * @return {Array} Array of rows. |
- */ |
-Graph.prototype.getMergedRowByIndex_ = function() { |
- var rowByChannel = {}; |
- for (var i = 0; i < this.channels_.length; i++) |
- rowByChannel[this.channels_[i]] = []; |
- |
- // Order by channel. |
- for (var i = 0; i < this.rows_.length; i++) { |
- if (this.rows_[i]) { |
- for (var j = 0; j < this.rows_[i].length; j++) { |
- var row = this.rows_[i].get(j); |
- if (row && row.chan in rowByChannel) { |
- rowByChannel[row.chan].push(row); |
- } |
- } |
- } |
- } |
- |
- var max = 0; |
- for (var channel in rowByChannel) |
- max = Math.max(rowByChannel[channel].length, max); |
- |
- // Merge data. |
- var combindedRows = []; |
- for (var i = 0; i < max; i++) { |
- var currentRow = null; |
- for (var channel in rowByChannel) { |
- if (rowByChannel[channel].length > i) { |
- var row = rowByChannel[channel][i]; |
- var traces = row['traces']; |
- for (var traceName in traces) { |
- traces[traceName + '_' + channel] = traces[traceName]; |
- delete(traces[traceName]); |
- } |
- if (!currentRow) { |
- currentRow = row; |
- } else { |
- for (var traceName in traces) |
- currentRow['traces'][traceName] = traces[traceName]; |
- currentRow.version += ', ' + row.version; |
- } |
- } |
- } |
- combindedRows.push(currentRow); |
- } |
- return combindedRows; |
-} |
- |
-/** |
- * Get rows for a specific channel. |
- * |
- * @return {Array} Array of rows. |
- */ |
-Graph.prototype.getRowByChannel_ = function() { |
- // Combind channel data. |
- var rows = []; |
- for (var i = 0; i < this.rows_.length; i++) { |
- if (this.rows_[i]) { |
- for (var j = 0; j < this.rows_[i].length; j++) { |
- var row = this.rows_[i].get(j); |
- if (row && row.chan == this.channels_[0]) |
- rows.push(row); |
- } |
- } |
- } |
- return rows; |
-} |
- |
-/** |
- * Prepare the data and create Plotter() to generate a graph. |
- */ |
-Graph.prototype.addPlot_ = function() { |
- var rows = []; |
- if (this.options_.orderDataByVersion) |
- rows = this.getMergedRowsByVersion_(); |
- else if (this.channels_.length > 1) |
- rows = this.getMergedRowByIndex_(); |
- else |
- rows = this.getRowByChannel_(); |
- |
- var maxRows = rows.length; |
- if (maxRows > this.history_) |
- maxRows = this.history_; |
- |
- // Find the start and end of the data slice we will focus on. |
- var startRow = 0; |
- if (this.rev_ > 0) { |
- var i = 0; |
- while (i < rows.length) { |
- var row = rows[i]; |
- // If the current row's revision is higher than the desired revision, |
- // continue searching. |
- if (row.revision > this.rev_) { |
- i++; |
- continue; |
- } |
- // We're either just under or at the desired revision. |
- startRow = i; |
- // If the desired revision does not exist, use the row before it. |
- if (row.revision < this.rev_ && startRow > 0) |
- startRow -= 1; |
- break; |
- } |
- } |
- |
- // Some summary files contain data not listed in rev-descending order. For |
- // those cases, it is possible we will find a start row in the middle of the |
- // data whose neighboring data is not nearby. See xp-release-dual-core |
- // moz rev 265 => no graph. |
- var endRow = startRow + maxRows; |
- |
- // Build and order a list of revision numbers. |
- var allTraces = {}; |
- var revisionNumbers = []; |
- var versionMap = {}; |
- var hasNumericRevisions = true; |
- // graphData[rev] = {trace1:[value, stddev], trace2:[value, stddev], ...} |
- var graphData = {}; |
- for (var i = startRow; i < endRow; ++i) { |
- var row = rows[i]; |
- if (!row) |
- continue; |
- var traces = row['traces']; |
- for (var j = 0; j < traces.length; ++j) |
- traces[j] = parseFloat(traces[j]); |
- |
- graphData[row.revision] = traces; |
- if (isNaN(row.revision)) { |
- hasNumericRevisions = false; |
- } |
- revisionNumbers.push(row.revision); |
- |
- versionMap[row.revision] = row.version; |
- |
- // Collect unique trace names. If traces are explicitly specified in |
- // params, delete unspecified trace data. |
- for (var traceName in traces) { |
- if (typeof(params['trace']) != 'undefined' && |
- params['trace'][traceName] != 1) { |
- delete(traces[traceName]); |
- } |
- allTraces[traceName] = 1; |
- } |
- } |
- |
- // Build a list of all the trace names we've seen, in the order in which |
- // they appear in the data file. Although JS objects are not required by |
- // the spec to iterate their properties in order, in practice they do, |
- // because it causes compatibility problems otherwise. |
- var traceNames = []; |
- for (var traceName in allTraces) |
- traceNames.push(traceName); |
- this.firstTrace_ = traceNames[0]; |
- |
- // If the revisions are numeric (svn), sort them numerically to ensure they |
- // are in ascending order. Otherwise, if the revisions aren't numeric (git), |
- // reverse them under the assumption the rows were prepended to the file. |
- if (hasNumericRevisions) { |
- revisionNumbers.sort( |
- function(a, b) { return parseInt(a, 10) - parseInt(b, 10) }); |
- } else { |
- revisionNumbers.reverse(); |
- } |
- |
- // Build separate ordered lists of trace data. |
- var traceData = {}; |
- var versionList = []; |
- for (var revIndex = 0; revIndex < revisionNumbers.length; ++revIndex) { |
- var rev = revisionNumbers[revIndex]; |
- var revisionData = graphData[rev]; |
- for (var nameIndex = 0; nameIndex < traceNames.length; ++nameIndex) { |
- var traceName = traceNames[nameIndex]; |
- if (!traceData[traceName]) |
- traceData[traceName] = []; |
- if (!revisionData[traceName]) |
- traceData[traceName].push([NaN, NaN]); |
- else |
- traceData[traceName].push([parseFloat(revisionData[traceName][0]), |
- parseFloat(revisionData[traceName][1])]); |
- } |
- versionList.push(versionMap[rev]); |
- } |
- |
- var plotData = []; |
- for (var traceName in traceData) |
- plotData.push(traceData[traceName]); |
- |
- var plotterDiv = document.createElement('div'); |
- if (!this.plotterDiv_) |
- AppendChild(plotterDiv, this.graphContainer) |
- else |
- this.graphContainer.replaceChild(plotterDiv, this.plotterDiv_); |
- this.plotterDiv_ = plotterDiv; |
- |
- var plotter = new Plotter( |
- versionList, plotData, traceNames, this.selectedGraph_.units, |
- this.plotterDiv_, this.width, this.height); |
- |
- var obj = this; |
- plotter.onclick = function(){obj.onPlotClicked.apply(obj, arguments)}; |
- plotter.enableMouseScroll = this.options_.enableMouseScroll; |
- plotter.plot(); |
-} |
- |
-/** |
- * Handle switching graph when tab is clicked. |
- */ |
-Graph.prototype.switchGraph_ = function(graphName) { |
- if (graphName == this.selectedGraph_.name) |
- return; |
- |
- for (var i = 0; i < this.tabs_.length; i++) { |
- var name = this.tabs_[i].innerHTML; |
- if (graphName + ' ' == name) { |
- this.tabs_[i].removeAttribute('class'); |
- } else { |
- this.tabs_[i].setAttribute('class', 'select'); |
- } |
- } |
- |
- for (var i = 0; i < this.graphList_.length; i++) { |
- if (this.graphList_[i].name == graphName) { |
- this.selectedGraph_ = this.graphList_[i]; |
- break; |
- } |
- } |
- |
- this.fetchSummary_(); |
-} |
- |
-/** |
- * On plot clicked, display detail view. |
- */ |
-Graph.prototype.onPlotClicked = function(prev_cl, cl) { |
- if (!this.options_.showDetail) |
- return; |
- this.addDetailView_(); |
- |
- var getChildByName = function(div, name) { |
- var children = div.childNodes; |
- for (var i = 0; i < children.length; i++) |
- if (children[i].getAttribute('name') == name) |
- return children[i]; |
- } |
- // Define sources for detail tabs |
- if ('view-change' in Config.detailTabs) { |
- // If the changeLinkPrefix has {PREV_CL}/{CL} markers, replace them. |
- // Otherwise, append to the URL. |
- var url = Config.changeLinkPrefix; |
- if (url.indexOf('{PREV_CL}') >= 0 || url.indexOf('{CL}') >= 0) { |
- url = url.replace('{PREV_CL}', prev_cl); |
- url = url.replace('{CL}', cl); |
- } else { |
- url += prev_cl + ':' + cl; |
- } |
- getChildByName(this.detailPane, 'view-change').setAttribute('src', url); |
- } |
- |
- if ('view-pages' in Config.detailTabs) { |
- getChildByName(this.detailPane, 'view-pages'). |
- setAttribute('src', 'details.html?cl=' + cl + |
- '&graph=' + this.milestone + '-' + this.selectedGraph_.name + |
- '&trace=' + this.firstTrace_); |
- } |
- if ('view-coverage' in Config.detailTabs) { |
- getChildByName(this.detailPane, 'view-coverage').setAttribute( |
- 'src',Config.coverageLinkPrefix + cl); |
- } |
- |
- if (!this.didPositionDetail) { |
- this.positionDetails_(); |
- this.didPositionDetail = true; |
- } |
-} |
- |
-/** |
- * Display detail view. |
- */ |
-Graph.prototype.addDetailView_ = function() { |
- if (this.isDetailViewAdded_) |
- return; |
- this.isDetailViewAdded_ = true; |
- // Add detail page. |
- this.detailPane = document.createElement('div'); |
- AppendChild(this.detailPane, this.graphContainer); |
- |
- for (var tab in Config.detailTabs) { |
- var detail = document.createElement('iframe'); |
- detail.setAttribute('class', 'detail'); |
- detail.setAttribute('name', tab); |
- AppendChild(detail, this.detailPane); |
- } |
- |
- this.selectorPane = document.createElement('div'); |
- this.selectorPane.setAttribute('class', 'selectors'); |
- this.selectorPane.setAttribute( |
- 'style', 'display: block; 1px solid black; position: absolute;'); |
- AppendChild(this.selectorPane, this.graphContainer); |
- |
- var firstTab = true; |
- for (var tab in Config.detailTabs) { |
- var selector = document.createElement('div'); |
- selector.setAttribute('class', 'selector'); |
- var obj = this; |
- selector.onclick = ( |
- function(){ |
- var cur = tab; return function() {obj.changeDetailTab(cur)}})(); |
- if (firstTab) |
- firstTab = false; |
- else |
- selector.setAttribute('style', 'border-top: none'); |
- selector.innerHTML = Config.detailTabs[tab]; |
- AppendChild(selector, this.selectorPane); |
- } |
-} |
- |
-Graph.prototype.positionDetails_ = function() { |
- var win_height = window.innerHeight; |
- |
- var views_width = this.graphContainer.offsetWidth - |
- this.selectorPane.offsetWidth; |
- |
- this.detailPane.style.width = views_width + "px"; |
- this.detailPane.style.height = ( |
- win_height - this.graphContainer.offsetHeight - |
- this.graphContainer.offsetTop - 30) + "px"; |
- |
- this.selectorPane.style.left = ( |
- this.detailPane.offsetLeft + views_width + 1) + "px"; |
- this.selectorPane.style.top = this.detailPane.offsetTop + "px"; |
- |
- // Change to the first detail tab |
- for (var tab in Config.detailTabs) { |
- this.changeDetailTab_(tab); |
- break; |
- } |
-} |
- |
-Graph.prototype.changeDetailTab_ = function(target) { |
- var detailArr = this.detailPane.getElementsByTagName('iframe'); |
- var i = 0; |
- for (var tab in Config.detailTabs) { |
- detailArr[i++].style.display = (tab == target ? 'block' : 'none'); |
- } |
-} |
- |
-Graph.prototype.goTo = function(graph) { |
- params.graph = graph; |
- if (params.graph == '') |
- delete params.graph; |
- window.location.href = MakeURL(params); |
-} |
- |
-Graph.prototype.getURL = function() { |
- new_url = window.location.href; |
- new_url = new_url.replace(/50/, "150"); |
- new_url = new_url.replace(/\&lookout=1/, ""); |
- return new_url; |
-} |
- |
- |
-/** |
- * Encapsulates a *-summary.dat file. |
- * @constructor |
- */ |
-function Rows(data) { |
- this.rows_ = (data) ? data.split('\n') : []; |
- this.length = this.rows_.length; |
-} |
- |
-/** |
- * Returns the row at the given index. |
- */ |
-Rows.prototype.get = function(i) { |
- if (!this.rows_[i].length) return null; |
- var row = JsonToJs(this.rows_[i]); |
- row.revision = isNaN(row['rev']) ? row['rev'] : parseInt(row['rev']); |
- row.version = row['ver']; |
- return row; |
-}; |