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