OLD | NEW |
| (Empty) |
1 /* | |
2 Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 Use of this source code is governed by a BSD-style license that can be | |
4 found in the LICENSE file. | |
5 */ | |
6 | |
7 /* | |
8 Fetch all graph data files, prepare the data, and create Plotter() to | |
9 generate a graph. | |
10 | |
11 To use: | |
12 var graph = new Graph('output_div', graphList) | |
13 graph.setTitle('Title'); | |
14 graph.graph(); | |
15 */ | |
16 | |
17 function JsonToJs(data) { | |
18 return eval('(' + data + ')'); | |
19 } | |
20 | |
21 /** | |
22 * Insert element a after element b. | |
23 */ | |
24 function AppendChild(a, b) { | |
25 var elementA = (typeof(a) == 'object') ? a : document.getElementById(a); | |
26 var elementB = (typeof(b) == 'object') ? b : document.getElementById(b); | |
27 elementB.appendChild(elementA); | |
28 } | |
29 | |
30 /** | |
31 * Insert element a before element b. | |
32 */ | |
33 function InsertBefore(a, b) { | |
34 var elementA = (typeof(a) == 'object') ? a : document.getElementById(a); | |
35 var elementB = (typeof(b) == 'object') ? b : document.getElementById(b); | |
36 elementB.insertBefore(elementA); | |
37 } | |
38 | |
39 | |
40 /** | |
41 * Graph class. | |
42 * @constructor | |
43 * | |
44 * Fetch each graph in |graphList| and create Plotter(). Create Graph() | |
45 * and call graph() to display graph. | |
46 * | |
47 * @param div {String|DOMElement} The container that the graph should be | |
48 * rendered to. | |
49 * @param graphList {Array} List of graphs to be plotted. | |
50 * @param options {Object} Options to configure graph. | |
51 * - width {int} Width of graph. | |
52 * - height {int} Height of graph. | |
53 * - history {int} Number of row to show. | |
54 * - showDetail {Boolean} Specifies whether or not to display detail. | |
55 * Default false. | |
56 * - showTabs {Boolean} Specifies whether or not to show tabs. | |
57 * Default false. | |
58 * - enableMouseScroll {Boolean} Specifies whether or not to enable | |
59 * mouse wheel zooming. Default false. | |
60 * - channels {Array} Display graph by channels. | |
61 * - orderDataByVersion {Boolean} Order plot data by version number. | |
62 * Default false. | |
63 * | |
64 * Example of the graphList: | |
65 * [ | |
66 * {"units": "ms", "important": false, "name": "SunSpider-individual", | |
67 * "SunSpider-individual-summary.dat"}, | |
68 * ] | |
69 */ | |
70 function Graph(div, graphList, opt_options) { | |
71 this.graphList_ = graphList; | |
72 this.options_ = (opt_options) ? opt_options : {}; | |
73 this.history_ = (this.options_.history) ? this.options_.history : 150; | |
74 this.rev_ = (this.options_.rev) ? this.options_.rev : -1; | |
75 this.channels_ = (this.options_.channels) ? this.options_.channels : []; | |
76 this.firstTrace_ = ''; | |
77 this.rows_ = []; | |
78 this.isDetailViewAdded_ = false; | |
79 this.selectedGraph_ = null; | |
80 this.plotterDiv_ = null; | |
81 this.tabs_ = []; | |
82 this.width = this.options_.width; | |
83 this.height = this.options_.height; | |
84 | |
85 this.graphContainer = document.createElement('div'); | |
86 this.graphContainer.setAttribute( | |
87 'style', 'display: block; overflow: hidden; ' + | |
88 'margin: 5px; padding: 5px; width:' + this.width); | |
89 AppendChild(this.graphContainer, div); | |
90 | |
91 this.title = document.createElement('div'); | |
92 this.title.setAttribute('style', 'text-align: center'); | |
93 AppendChild(this.title, this.graphContainer); | |
94 } | |
95 | |
96 /** | |
97 * Start fetching graph data. | |
98 */ | |
99 Graph.prototype.graph = function() { | |
100 this.fetchSummary_(); | |
101 | |
102 if (this.options_.showTabs) | |
103 this.addTabs_(); | |
104 } | |
105 | |
106 /** | |
107 * Set graph title. | |
108 */ | |
109 Graph.prototype.setTitle = function(title) { | |
110 this.title.innerHTML = title; | |
111 } | |
112 | |
113 /** | |
114 * Display tabs for each graph. | |
115 */ | |
116 Graph.prototype.addTabs_ = function() { | |
117 this.tabs_ = []; | |
118 var tabPane = document.createElement('div'); | |
119 tabPane.setAttribute('class', 'switcher'); | |
120 AppendChild(tabPane, this.graphContainer); | |
121 | |
122 var graphNames = [] | |
123 var inserted = {}; | |
124 for (var i = 0; i < this.graphList_.length; i++) { | |
125 if (!inserted[this.graphList_[i].name]) { | |
126 graphNames.push(this.graphList_[i].name); | |
127 inserted[this.graphList_[i].name] = 1; | |
128 } | |
129 } | |
130 | |
131 var obj = this; | |
132 for (var i = 0; i < graphNames.length; i++) { | |
133 var name = graphNames[i]; | |
134 var tab = document.createElement('span'); | |
135 if (name != this.selectedGraph_.name) { | |
136 tab.setAttribute('class', 'select'); | |
137 } | |
138 tab.addEventListener( | |
139 "click", | |
140 (function(){ | |
141 var cur = name; return function() {obj.switchGraph_(cur)} | |
142 })(), | |
143 false); | |
144 tab.appendChild(document.createTextNode(name + " ")); | |
145 AppendChild(tab, tabPane); | |
146 this.tabs_.push(tab); | |
147 } | |
148 } | |
149 | |
150 /** | |
151 * Fetch graph summary data files. | |
152 */ | |
153 Graph.prototype.fetchSummary_ = function() { | |
154 this.rows_ = []; | |
155 if (!this.selectedGraph_) { | |
156 this.selectedGraph_ = this.graphList_[0]; | |
157 } | |
158 var graphFiles = []; | |
159 this.selectedGraphList_ = []; | |
160 for (var i = 0; i < this.graphList_.length; i++) { | |
161 if (this.graphList_[i].name == this.selectedGraph_.name) { | |
162 graphFiles.push(this.graphList_[i].loc); | |
163 this.selectedGraphList_.push(this.graphList_[i]); | |
164 } | |
165 } | |
166 var obj = this; | |
167 new FetchList(graphFiles, function(data) {obj.onSummaryReceived_(data)}); | |
168 } | |
169 | |
170 /** | |
171 * Call addPlot_ once all graph summary data are received. | |
172 */ | |
173 Graph.prototype.onSummaryReceived_ = function(data) { | |
174 // Parse the summary data file. | |
175 for (var i = 0; i < data.length; i++) { | |
176 if (data[i]) { | |
177 var rows = new Rows(data[i]); | |
178 this.rows_[i] = rows; | |
179 } | |
180 } | |
181 this.addPlot_(); | |
182 } | |
183 | |
184 /** | |
185 * Merge all data rows by channel and version. This is use in platform | |
186 * comparison graph. | |
187 * | |
188 * Example: | |
189 * Two rows: | |
190 * {"traces": {"score": ["777", "0.0"]}, "rev": "9", | |
191 * "ver": "17.1.963.19", "chan": "stable"} | |
192 * {"traces": {"score": ["888", "0.0"]}, "rev": "10", | |
193 * "ver": "17.1.963.19", "chan": "stable"} | |
194 * Become: | |
195 * {"traces": {"score_windows": ["777", "0.0"], | |
196 * "score_linux": ["888", "0.0"]}, | |
197 * "rev": "9", "ver": "17.1.963.19", "chan": "stable"} | |
198 * | |
199 * @return {Array} Array of rows. | |
200 */ | |
201 Graph.prototype.getMergedRowsByVersion_ = function() { | |
202 var channels = {}; | |
203 for (var i = 0; i < this.channels_.length; i++) | |
204 channels[this.channels_[i]] = 1; | |
205 var allRows = []; | |
206 // Combind all rows to one list. | |
207 for (var i = 0; i < this.rows_.length; i++) { | |
208 if (this.rows_[i]) { | |
209 for (var j = 0; j < this.rows_[i].length; j++) { | |
210 var row = this.rows_[i].get(j); | |
211 if (row && row.chan in channels) { | |
212 row.machine = this.selectedGraphList_[i].machine; | |
213 allRows.push(row); | |
214 } | |
215 } | |
216 } | |
217 } | |
218 | |
219 // Sort by version number. | |
220 allRows.sort( | |
221 function(a, b) { | |
222 var a_arr = a.version.split('.'); | |
223 var b_arr = b.version.split('.'); | |
224 var len = Math.min(b_arr.length, b_arr.length); | |
225 for (var i = 0; i < len; i++) { | |
226 if (parseInt(a_arr[i], 10) > parseInt(b_arr[i], 10)) | |
227 return 1; | |
228 else if (parseInt(a_arr[i], 10) < parseInt(b_arr[i], 10)) | |
229 return -1; | |
230 } | |
231 return a_arr.length - b_arr.length; | |
232 }); | |
233 | |
234 // Merge all rows by version number. | |
235 var combindedRows = []; | |
236 var index = 0; | |
237 while (index < allRows.length) { | |
238 var currentRow = allRows[index]; | |
239 var traces = currentRow['traces']; | |
240 for (var traceName in traces) { | |
241 var traceRenamed = traceName + '_' + currentRow.machine.toLowerCase(); | |
242 traces[traceRenamed] = traces[traceName]; | |
243 delete(traces[traceName]); | |
244 } | |
245 while (index < allRows.length - 1 && | |
246 allRows[index + 1].version == currentRow.version) { | |
247 var row = allRows[index + 1]; | |
248 var traces = row['traces']; | |
249 for (var traceName in traces) { | |
250 var traceRenamed = traceName + '_' + row.machine.toLowerCase(); | |
251 currentRow['traces'][traceRenamed] = traces[traceName]; | |
252 } | |
253 index++; | |
254 } | |
255 combindedRows.push(currentRow); | |
256 index++; | |
257 } | |
258 return combindedRows; | |
259 } | |
260 | |
261 /** | |
262 * Merge all channel data by their index in file. This is use in channel | |
263 * comparison graph. | |
264 * | |
265 * @return {Array} Array of rows. | |
266 */ | |
267 Graph.prototype.getMergedRowByIndex_ = function() { | |
268 var rowByChannel = {}; | |
269 for (var i = 0; i < this.channels_.length; i++) | |
270 rowByChannel[this.channels_[i]] = []; | |
271 | |
272 // Order by channel. | |
273 for (var i = 0; i < this.rows_.length; i++) { | |
274 if (this.rows_[i]) { | |
275 for (var j = 0; j < this.rows_[i].length; j++) { | |
276 var row = this.rows_[i].get(j); | |
277 if (row && row.chan in rowByChannel) { | |
278 rowByChannel[row.chan].push(row); | |
279 } | |
280 } | |
281 } | |
282 } | |
283 | |
284 var max = 0; | |
285 for (var channel in rowByChannel) | |
286 max = Math.max(rowByChannel[channel].length, max); | |
287 | |
288 // Merge data. | |
289 var combindedRows = []; | |
290 for (var i = 0; i < max; i++) { | |
291 var currentRow = null; | |
292 for (var channel in rowByChannel) { | |
293 if (rowByChannel[channel].length > i) { | |
294 var row = rowByChannel[channel][i]; | |
295 var traces = row['traces']; | |
296 for (var traceName in traces) { | |
297 traces[traceName + '_' + channel] = traces[traceName]; | |
298 delete(traces[traceName]); | |
299 } | |
300 if (!currentRow) { | |
301 currentRow = row; | |
302 } else { | |
303 for (var traceName in traces) | |
304 currentRow['traces'][traceName] = traces[traceName]; | |
305 currentRow.version += ', ' + row.version; | |
306 } | |
307 } | |
308 } | |
309 combindedRows.push(currentRow); | |
310 } | |
311 return combindedRows; | |
312 } | |
313 | |
314 /** | |
315 * Get rows for a specific channel. | |
316 * | |
317 * @return {Array} Array of rows. | |
318 */ | |
319 Graph.prototype.getRowByChannel_ = function() { | |
320 // Combind channel data. | |
321 var rows = []; | |
322 for (var i = 0; i < this.rows_.length; i++) { | |
323 if (this.rows_[i]) { | |
324 for (var j = 0; j < this.rows_[i].length; j++) { | |
325 var row = this.rows_[i].get(j); | |
326 if (row && row.chan == this.channels_[0]) | |
327 rows.push(row); | |
328 } | |
329 } | |
330 } | |
331 return rows; | |
332 } | |
333 | |
334 /** | |
335 * Prepare the data and create Plotter() to generate a graph. | |
336 */ | |
337 Graph.prototype.addPlot_ = function() { | |
338 var rows = []; | |
339 if (this.options_.orderDataByVersion) | |
340 rows = this.getMergedRowsByVersion_(); | |
341 else if (this.channels_.length > 1) | |
342 rows = this.getMergedRowByIndex_(); | |
343 else | |
344 rows = this.getRowByChannel_(); | |
345 | |
346 var maxRows = rows.length; | |
347 if (maxRows > this.history_) | |
348 maxRows = this.history_; | |
349 | |
350 // Find the start and end of the data slice we will focus on. | |
351 var startRow = 0; | |
352 if (this.rev_ > 0) { | |
353 var i = 0; | |
354 while (i < rows.length) { | |
355 var row = rows[i]; | |
356 // If the current row's revision is higher than the desired revision, | |
357 // continue searching. | |
358 if (row.revision > this.rev_) { | |
359 i++; | |
360 continue; | |
361 } | |
362 // We're either just under or at the desired revision. | |
363 startRow = i; | |
364 // If the desired revision does not exist, use the row before it. | |
365 if (row.revision < this.rev_ && startRow > 0) | |
366 startRow -= 1; | |
367 break; | |
368 } | |
369 } | |
370 | |
371 // Some summary files contain data not listed in rev-descending order. For | |
372 // those cases, it is possible we will find a start row in the middle of the | |
373 // data whose neighboring data is not nearby. See xp-release-dual-core | |
374 // moz rev 265 => no graph. | |
375 var endRow = startRow + maxRows; | |
376 | |
377 // Build and order a list of revision numbers. | |
378 var allTraces = {}; | |
379 var revisionNumbers = []; | |
380 var versionMap = {}; | |
381 var hasNumericRevisions = true; | |
382 // graphData[rev] = {trace1:[value, stddev], trace2:[value, stddev], ...} | |
383 var graphData = {}; | |
384 for (var i = startRow; i < endRow; ++i) { | |
385 var row = rows[i]; | |
386 if (!row) | |
387 continue; | |
388 var traces = row['traces']; | |
389 for (var j = 0; j < traces.length; ++j) | |
390 traces[j] = parseFloat(traces[j]); | |
391 | |
392 graphData[row.revision] = traces; | |
393 if (isNaN(row.revision)) { | |
394 hasNumericRevisions = false; | |
395 } | |
396 revisionNumbers.push(row.revision); | |
397 | |
398 versionMap[row.revision] = row.version; | |
399 | |
400 // Collect unique trace names. If traces are explicitly specified in | |
401 // params, delete unspecified trace data. | |
402 for (var traceName in traces) { | |
403 if (typeof(params['trace']) != 'undefined' && | |
404 params['trace'][traceName] != 1) { | |
405 delete(traces[traceName]); | |
406 } | |
407 allTraces[traceName] = 1; | |
408 } | |
409 } | |
410 | |
411 // Build a list of all the trace names we've seen, in the order in which | |
412 // they appear in the data file. Although JS objects are not required by | |
413 // the spec to iterate their properties in order, in practice they do, | |
414 // because it causes compatibility problems otherwise. | |
415 var traceNames = []; | |
416 for (var traceName in allTraces) | |
417 traceNames.push(traceName); | |
418 this.firstTrace_ = traceNames[0]; | |
419 | |
420 // If the revisions are numeric (svn), sort them numerically to ensure they | |
421 // are in ascending order. Otherwise, if the revisions aren't numeric (git), | |
422 // reverse them under the assumption the rows were prepended to the file. | |
423 if (hasNumericRevisions) { | |
424 revisionNumbers.sort( | |
425 function(a, b) { return parseInt(a, 10) - parseInt(b, 10) }); | |
426 } else { | |
427 revisionNumbers.reverse(); | |
428 } | |
429 | |
430 // Build separate ordered lists of trace data. | |
431 var traceData = {}; | |
432 var versionList = []; | |
433 for (var revIndex = 0; revIndex < revisionNumbers.length; ++revIndex) { | |
434 var rev = revisionNumbers[revIndex]; | |
435 var revisionData = graphData[rev]; | |
436 for (var nameIndex = 0; nameIndex < traceNames.length; ++nameIndex) { | |
437 var traceName = traceNames[nameIndex]; | |
438 if (!traceData[traceName]) | |
439 traceData[traceName] = []; | |
440 if (!revisionData[traceName]) | |
441 traceData[traceName].push([NaN, NaN]); | |
442 else | |
443 traceData[traceName].push([parseFloat(revisionData[traceName][0]), | |
444 parseFloat(revisionData[traceName][1])]); | |
445 } | |
446 versionList.push(versionMap[rev]); | |
447 } | |
448 | |
449 var plotData = []; | |
450 for (var traceName in traceData) | |
451 plotData.push(traceData[traceName]); | |
452 | |
453 var plotterDiv = document.createElement('div'); | |
454 if (!this.plotterDiv_) | |
455 AppendChild(plotterDiv, this.graphContainer) | |
456 else | |
457 this.graphContainer.replaceChild(plotterDiv, this.plotterDiv_); | |
458 this.plotterDiv_ = plotterDiv; | |
459 | |
460 var plotter = new Plotter( | |
461 versionList, plotData, traceNames, this.selectedGraph_.units, | |
462 this.plotterDiv_, this.width, this.height); | |
463 | |
464 var obj = this; | |
465 plotter.onclick = function(){obj.onPlotClicked.apply(obj, arguments)}; | |
466 plotter.enableMouseScroll = this.options_.enableMouseScroll; | |
467 plotter.plot(); | |
468 } | |
469 | |
470 /** | |
471 * Handle switching graph when tab is clicked. | |
472 */ | |
473 Graph.prototype.switchGraph_ = function(graphName) { | |
474 if (graphName == this.selectedGraph_.name) | |
475 return; | |
476 | |
477 for (var i = 0; i < this.tabs_.length; i++) { | |
478 var name = this.tabs_[i].innerHTML; | |
479 if (graphName + ' ' == name) { | |
480 this.tabs_[i].removeAttribute('class'); | |
481 } else { | |
482 this.tabs_[i].setAttribute('class', 'select'); | |
483 } | |
484 } | |
485 | |
486 for (var i = 0; i < this.graphList_.length; i++) { | |
487 if (this.graphList_[i].name == graphName) { | |
488 this.selectedGraph_ = this.graphList_[i]; | |
489 break; | |
490 } | |
491 } | |
492 | |
493 this.fetchSummary_(); | |
494 } | |
495 | |
496 /** | |
497 * On plot clicked, display detail view. | |
498 */ | |
499 Graph.prototype.onPlotClicked = function(prev_cl, cl) { | |
500 if (!this.options_.showDetail) | |
501 return; | |
502 this.addDetailView_(); | |
503 | |
504 var getChildByName = function(div, name) { | |
505 var children = div.childNodes; | |
506 for (var i = 0; i < children.length; i++) | |
507 if (children[i].getAttribute('name') == name) | |
508 return children[i]; | |
509 } | |
510 // Define sources for detail tabs | |
511 if ('view-change' in Config.detailTabs) { | |
512 // If the changeLinkPrefix has {PREV_CL}/{CL} markers, replace them. | |
513 // Otherwise, append to the URL. | |
514 var url = Config.changeLinkPrefix; | |
515 if (url.indexOf('{PREV_CL}') >= 0 || url.indexOf('{CL}') >= 0) { | |
516 url = url.replace('{PREV_CL}', prev_cl); | |
517 url = url.replace('{CL}', cl); | |
518 } else { | |
519 url += prev_cl + ':' + cl; | |
520 } | |
521 getChildByName(this.detailPane, 'view-change').setAttribute('src', url); | |
522 } | |
523 | |
524 if ('view-pages' in Config.detailTabs) { | |
525 getChildByName(this.detailPane, 'view-pages'). | |
526 setAttribute('src', 'details.html?cl=' + cl + | |
527 '&graph=' + this.milestone + '-' + this.selectedGraph_.name + | |
528 '&trace=' + this.firstTrace_); | |
529 } | |
530 if ('view-coverage' in Config.detailTabs) { | |
531 getChildByName(this.detailPane, 'view-coverage').setAttribute( | |
532 'src',Config.coverageLinkPrefix + cl); | |
533 } | |
534 | |
535 if (!this.didPositionDetail) { | |
536 this.positionDetails_(); | |
537 this.didPositionDetail = true; | |
538 } | |
539 } | |
540 | |
541 /** | |
542 * Display detail view. | |
543 */ | |
544 Graph.prototype.addDetailView_ = function() { | |
545 if (this.isDetailViewAdded_) | |
546 return; | |
547 this.isDetailViewAdded_ = true; | |
548 // Add detail page. | |
549 this.detailPane = document.createElement('div'); | |
550 AppendChild(this.detailPane, this.graphContainer); | |
551 | |
552 for (var tab in Config.detailTabs) { | |
553 var detail = document.createElement('iframe'); | |
554 detail.setAttribute('class', 'detail'); | |
555 detail.setAttribute('name', tab); | |
556 AppendChild(detail, this.detailPane); | |
557 } | |
558 | |
559 this.selectorPane = document.createElement('div'); | |
560 this.selectorPane.setAttribute('class', 'selectors'); | |
561 this.selectorPane.setAttribute( | |
562 'style', 'display: block; 1px solid black; position: absolute;'); | |
563 AppendChild(this.selectorPane, this.graphContainer); | |
564 | |
565 var firstTab = true; | |
566 for (var tab in Config.detailTabs) { | |
567 var selector = document.createElement('div'); | |
568 selector.setAttribute('class', 'selector'); | |
569 var obj = this; | |
570 selector.onclick = ( | |
571 function(){ | |
572 var cur = tab; return function() {obj.changeDetailTab(cur)}})(); | |
573 if (firstTab) | |
574 firstTab = false; | |
575 else | |
576 selector.setAttribute('style', 'border-top: none'); | |
577 selector.innerHTML = Config.detailTabs[tab]; | |
578 AppendChild(selector, this.selectorPane); | |
579 } | |
580 } | |
581 | |
582 Graph.prototype.positionDetails_ = function() { | |
583 var win_height = window.innerHeight; | |
584 | |
585 var views_width = this.graphContainer.offsetWidth - | |
586 this.selectorPane.offsetWidth; | |
587 | |
588 this.detailPane.style.width = views_width + "px"; | |
589 this.detailPane.style.height = ( | |
590 win_height - this.graphContainer.offsetHeight - | |
591 this.graphContainer.offsetTop - 30) + "px"; | |
592 | |
593 this.selectorPane.style.left = ( | |
594 this.detailPane.offsetLeft + views_width + 1) + "px"; | |
595 this.selectorPane.style.top = this.detailPane.offsetTop + "px"; | |
596 | |
597 // Change to the first detail tab | |
598 for (var tab in Config.detailTabs) { | |
599 this.changeDetailTab_(tab); | |
600 break; | |
601 } | |
602 } | |
603 | |
604 Graph.prototype.changeDetailTab_ = function(target) { | |
605 var detailArr = this.detailPane.getElementsByTagName('iframe'); | |
606 var i = 0; | |
607 for (var tab in Config.detailTabs) { | |
608 detailArr[i++].style.display = (tab == target ? 'block' : 'none'); | |
609 } | |
610 } | |
611 | |
612 Graph.prototype.goTo = function(graph) { | |
613 params.graph = graph; | |
614 if (params.graph == '') | |
615 delete params.graph; | |
616 window.location.href = MakeURL(params); | |
617 } | |
618 | |
619 Graph.prototype.getURL = function() { | |
620 new_url = window.location.href; | |
621 new_url = new_url.replace(/50/, "150"); | |
622 new_url = new_url.replace(/\&lookout=1/, ""); | |
623 return new_url; | |
624 } | |
625 | |
626 | |
627 /** | |
628 * Encapsulates a *-summary.dat file. | |
629 * @constructor | |
630 */ | |
631 function Rows(data) { | |
632 this.rows_ = (data) ? data.split('\n') : []; | |
633 this.length = this.rows_.length; | |
634 } | |
635 | |
636 /** | |
637 * Returns the row at the given index. | |
638 */ | |
639 Rows.prototype.get = function(i) { | |
640 if (!this.rows_[i].length) return null; | |
641 var row = JsonToJs(this.rows_[i]); | |
642 row.revision = isNaN(row['rev']) ? row['rev'] : parseInt(row['rev']); | |
643 row.version = row['ver']; | |
644 return row; | |
645 }; | |
OLD | NEW |