Perf Dashboard v3 confuses better and worse on A/B task page
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / components / analysis-results-viewer.js
1
2 class AnalysisResultsViewer extends ResultsTable {
3     constructor()
4     {
5         super('analysis-results-viewer');
6         this._startPoint = null;
7         this._endPoint = null;
8         this._testGroups = null;
9         this._currentTestGroup = null;
10         this._renderedCurrentTestGroup = null;
11         this._shouldRenderTable = true;
12         this._additionalHeading = null;
13         this._testGroupCallback = null;
14         this._expandedPoints = new Set;
15     }
16
17     setTestGroupCallback(callback) { this._testGroupCallback = callback; }
18
19     setCurrentTestGroup(testGroup)
20     {
21         this._currentTestGroup = testGroup;
22     }
23
24     setPoints(startPoint, endPoint)
25     {
26         this._startPoint = startPoint;
27         this._endPoint = endPoint;
28         this._shouldRenderTable = true;
29         this._expandedPoints.clear();
30         this._expandedPoints.add(startPoint);
31         this._expandedPoints.add(endPoint);
32     }
33
34     setTestGroups(testGroups)
35     {
36         this._testGroups = testGroups;
37         this._shouldRenderTable = true;
38     }
39
40     didUpdateResults()
41     {
42         this._shouldRenderTable = true;
43     }
44
45     render()
46     {
47         if (!this._valueFormatter || !this._startPoint)
48             return;
49
50         Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'render');
51
52         if (this._shouldRenderTable) {
53             this._shouldRenderTable = false;
54             this._renderedCurrentTestGroup = null;
55
56             Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'renderTable');
57             super.render();
58             Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'renderTable');
59         }
60
61         if (this._currentTestGroup != this._renderedCurrentTestGroup) {
62             if (this._renderedCurrentTestGroup) {
63                 var className = this._classForTestGroup(this._renderedCurrentTestGroup);
64                 var element = this.content().querySelector('.' + className);
65                 if (element)
66                     element.classList.remove('selected');
67             }
68             if (this._currentTestGroup) {
69                 var className = this._classForTestGroup(this._currentTestGroup);
70                 var element = this.content().querySelector('.' + className);
71                 if (element)
72                     element.classList.add('selected');
73             }
74             this._renderedCurrentTestGroup = this._currentTestGroup;
75         }
76
77         Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'render');
78     }
79
80     heading() { return [ComponentBase.createElement('th', 'Point')]; }
81     additionalHeading() { return this._additionalHeading; }
82
83     buildRowGroups()
84     {
85         Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'buildRowGroups');
86
87         var testGroups = this._testGroups || [];
88         var rootSetsInTestGroups = this._collectRootSetsInTestGroups(testGroups);
89
90         var rowToMatchingRootSets = new Map;
91         var rowList = this._buildRowsForPointsAndTestGroups(rootSetsInTestGroups, rowToMatchingRootSets);
92
93         var testGroupLayoutMap = new Map;
94         var self = this;
95         rowList.forEach(function (row, rowIndex) {
96             var matchingRootSets = rowToMatchingRootSets.get(row);
97             if (!matchingRootSets) {
98                 console.assert(row instanceof AnalysisResultsViewer.ExpandableRow);
99                 return;
100             }
101
102             for (var entry of matchingRootSets) {
103                 var testGroup = entry.testGroup();
104
105                 var block = testGroupLayoutMap.get(testGroup);
106                 if (!block) {
107                     block = new AnalysisResultsViewer.TestGroupStackingBlock(
108                         testGroup, self._classForTestGroup(testGroup), self._openStackingBlock.bind(self, testGroup));
109                     testGroupLayoutMap.set(testGroup, block);
110                 }
111                 block.addRowIndex(entry, rowIndex);
112             }
113         });
114
115         var grid = new AnalysisResultsViewer.TestGroupStackingGrid(rowList.length);
116         for (var testGroup of testGroups) {
117             var block = testGroupLayoutMap.get(testGroup);
118             if (block)
119                 grid.insertBlockToColumn(block);
120         }
121
122         grid.layout();
123         for (var rowIndex = 0; rowIndex < rowList.length; rowIndex++)
124             rowList[rowIndex].setAdditionalColumns(grid.createCellsForRow(rowIndex));
125
126         this._additionalHeading = grid._columns ? ComponentBase.createElement('td', {colspan: grid._columns.length + 1, class: 'stacking-block'}) : [];
127
128         Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'buildRowGroups');
129
130         return [{rows: rowList}];
131     }
132
133     _collectRootSetsInTestGroups(testGroups)
134     {
135         if (!this._testGroups)
136             return [];
137
138         var rootSetsInTestGroups = [];
139         for (var group of this._testGroups) {
140             var sortedSets = group.requestedRootSets();
141             for (var i = 0; i < sortedSets.length; i++)
142                 rootSetsInTestGroups.push(new AnalysisResultsViewer.RootSetInTestGroup(group, sortedSets[i], sortedSets[i + 1]));
143         }
144
145         return rootSetsInTestGroups;
146     }
147
148     _buildRowsForPointsAndTestGroups(rootSetsInTestGroups, rowToMatchingRootSets)
149     {
150         console.assert(this._startPoint.series == this._endPoint.series);
151         var rowList = [];
152         var pointAfterEnd = this._endPoint.series.nextPoint(this._endPoint);
153         var rootSetsWithPoints = new Set;
154         var pointIndex = 0;
155         var previousPoint;
156         for (var point = this._startPoint; point && point != pointAfterEnd; point = point.series.nextPoint(point), pointIndex++) {
157             var rootSetInPoint = point.rootSet();
158             var matchingRootSets = [];
159             for (var entry of rootSetsInTestGroups) {
160                 if (rootSetInPoint.equals(entry.rootSet()) && !rootSetsWithPoints.has(entry)) {
161                     matchingRootSets.push(entry);
162                     rootSetsWithPoints.add(entry);
163                 }
164             }
165
166             var hasMatchingTestGroup = !!matchingRootSets.length;
167             if (!hasMatchingTestGroup && !this._expandedPoints.has(point))
168                 continue;
169
170             var row = new ResultsTableRow(pointIndex.toString(), rootSetInPoint);
171             row.setResult(point);
172
173             if (previousPoint && previousPoint.series.nextPoint(previousPoint) != point)
174                 rowList.push(new AnalysisResultsViewer.ExpandableRow(this._expandBetween.bind(this, previousPoint, point)));
175             previousPoint = point;
176
177             rowToMatchingRootSets.set(row, matchingRootSets);
178             rowList.push(row);
179         }
180
181         rootSetsInTestGroups.forEach(function (entry) {
182             if (rootSetsWithPoints.has(entry))
183                 return;
184
185             for (var i = 0; i < rowList.length; i++) {
186                 var row = rowList[i];
187                 if (!(row instanceof AnalysisResultsViewer.ExpandableRow) && row.rootSet().equals(entry.rootSet())) {
188                     rowToMatchingRootSets.get(row).push(entry);
189                     return;
190                 }
191             }
192
193             var groupTime = entry.rootSet().latestCommitTime();
194             var newRow = new ResultsTableRow(null, entry.rootSet());
195             rowToMatchingRootSets.set(newRow, [entry]);
196
197             for (var i = 0; i < rowList.length; i++) {
198                 if (rowList[i] instanceof AnalysisResultsViewer.ExpandableRow)
199                     continue;
200
201                 if (rowList[i].rootSet().equals(entry.succeedingRootSet())) {
202                     rowList.splice(i, 0, newRow);
203                     return;
204                 }
205
206                 var rowTime = rowList[i].rootSet().latestCommitTime();
207                 if (rowTime > groupTime) {
208                     rowList.splice(i, 0, newRow);
209                     return;
210                 }
211
212                 if (rowTime == groupTime) {
213                     // Missing some commits. Do as best as we can to avoid going backwards in time.
214                     var repositoriesInNewRow = entry.rootSet().repositories();
215                     for (var j = i; j < rowList.length; j++) {
216                         if (rowList[j] instanceof AnalysisResultsViewer.ExpandableRow)
217                             continue;
218                         for (var repository of repositoriesInNewRow) {
219                             var newCommit = entry.rootSet().commitForRepository(repository);
220                             var rowCommit = rowList[j].rootSet().commitForRepository(repository);
221                             if (!rowCommit || newCommit.time() < rowCommit.time()) {
222                                 rowList.splice(j, 0, newRow);
223                                 return;
224                             }
225                         }
226                     }
227                 }
228             }
229
230             var newRow = new ResultsTableRow(null, entry.rootSet());
231             rowToMatchingRootSets.set(newRow, [entry]);
232             rowList.push(newRow);
233         });
234
235         return rowList;
236     }
237
238     _classForTestGroup(testGroup)
239     {
240         return 'stacked-test-group-' + testGroup.id();
241     }
242
243     _openStackingBlock(testGroup)
244     {
245         if (this._testGroupCallback)
246             this._testGroupCallback(testGroup);
247     }
248     
249     _expandBetween(pointBeforeExpansion, pointAfterExpansion)
250     {
251         console.assert(pointBeforeExpansion.series == pointAfterExpansion.series);
252         var indexBeforeStart = pointBeforeExpansion.seriesIndex;
253         var indexAfterEnd = pointAfterExpansion.seriesIndex;
254         console.assert(indexBeforeStart + 1 < indexAfterEnd);
255
256         var series = pointAfterExpansion.series;
257         var increment = Math.ceil((indexAfterEnd - indexBeforeStart) / 5);
258         if (increment < 3)
259             increment = 1;
260         for (var i = indexBeforeStart + 1; i < indexAfterEnd; i += increment)
261             this._expandedPoints.add(series.findPointByIndex(i));
262         this._shouldRenderTable = true;
263         this.render();
264     }
265
266     static htmlTemplate()
267     {
268         return `<section class="analysis-view">${ResultsTable.htmlTemplate()}</section>`;
269     }
270
271     static cssTemplate()
272     {
273         return ResultsTable.cssTemplate() + `
274             .analysis-view .stacking-block {
275                 position: relative;
276                 border: solid 1px #fff;
277                 cursor: pointer;
278             }
279
280             .analysis-view .stacking-block a {
281                 display: block;
282                 text-decoration: none;
283                 color: inherit;
284                 font-size: 0.8rem;
285                 padding: 0 0.1rem;
286                 max-width: 3rem;
287             }
288
289             .analysis-view .stacking-block:not(.failed) {
290                 color: black;
291                 opacity: 1;
292             }
293
294             .analysis-view .stacking-block.selected,
295             .analysis-view .stacking-block:hover {
296                 text-decoration: underline;
297             }
298
299             .analysis-view .stacking-block.selected:before {
300                 content: '';
301                 position: absolute;
302                 left: 0px;
303                 top: 0px;
304                 width: calc(100% - 2px);
305                 height: calc(100% - 2px);
306                 border: solid 1px #333;
307             }
308
309             .analysis-view .stacking-block.failed {
310                 background: rgba(128, 51, 128, 0.5);
311             }
312             .analysis-view .stacking-block.unchanged {
313                 background: rgba(128, 128, 128, 0.5);
314             }
315             .analysis-view .stacking-block.pending {
316                 background: rgba(204, 204, 51, 0.2);
317             }
318             .analysis-view .stacking-block.running {
319                 background: rgba(204, 204, 51, 0.5);
320             }
321             .analysis-view .stacking-block.worse {
322                 background: rgba(255, 102, 102, 0.5);
323             }
324             .analysis-view .stacking-block.better {
325                 background: rgba(102, 102, 255, 0.5);
326             }
327
328             .analysis-view .point-label-with-expansion-link {
329                 font-size: 0.7rem;
330             }
331             .analysis-view .point-label-with-expansion-link a {
332                 color: #999;
333                 text-decoration: none;
334             }
335         `;
336     }
337 }
338
339 ComponentBase.defineElement('analysis-results-viewer', AnalysisResultsViewer);
340
341 AnalysisResultsViewer.ExpandableRow = class extends ResultsTableRow {
342     constructor(callback)
343     {
344         super(null, null);
345         this._callback = callback;
346     }
347
348     resultContent() { return ''; }
349
350     heading()
351     {
352         return ComponentBase.createElement('span', {class: 'point-label-with-expansion-link'}, [
353             ComponentBase.createLink('(Expand)', 'Expand', this._callback),
354         ]);
355     }
356 }
357
358 AnalysisResultsViewer.RootSetInTestGroup = class {
359     constructor(testGroup, rootSet, succeedingRootSet)
360     {
361         console.assert(testGroup instanceof TestGroup);
362         console.assert(rootSet instanceof RootSet);
363         this._testGroup = testGroup;
364         this._rootSet = rootSet;
365         this._succeedingRootSet = succeedingRootSet;
366     }
367
368     testGroup() { return this._testGroup; }
369     rootSet() { return this._rootSet; }
370     succeedingRootSet() { return this._succeedingRootSet; }
371 }
372
373 AnalysisResultsViewer.TestGroupStackingBlock = class {
374     constructor(testGroup, className, callback)
375     {
376         this._testGroup = testGroup;
377         this._rootSetIndexRowIndexMap = [];
378         this._className = className;
379         this._label = null;
380         this._title = null;
381         this._status = null;
382         this._callback = callback;
383     }
384
385     addRowIndex(rootSetInTestGroup, rowIndex)
386     {
387         console.assert(rootSetInTestGroup instanceof AnalysisResultsViewer.RootSetInTestGroup);
388         this._rootSetIndexRowIndexMap.push({rootSet: rootSetInTestGroup.rootSet(), rowIndex: rowIndex});
389     }
390
391     testGroup() { return this._testGroup; }
392
393     createStackingCell()
394     {
395         this._computeTestGroupStatus();
396
397         return ComponentBase.createElement('td', {
398             rowspan: this.endRowIndex() - this.startRowIndex() + 1,
399             title: this._title,
400             class: 'stacking-block ' + this._className + ' ' + this._status,
401             onclick: this._callback,
402         }, ComponentBase.createLink(this._label, this._title, this._callback));
403     }
404
405     isComplete() { return this._rootSetIndexRowIndexMap.length >= 2; }
406
407     startRowIndex() { return this._rootSetIndexRowIndexMap[0].rowIndex; }
408     endRowIndex() { return this._rootSetIndexRowIndexMap[this._rootSetIndexRowIndexMap.length - 1].rowIndex; }
409     isThin()
410     {
411         this._computeTestGroupStatus();
412         return this._status == 'failed';
413     }
414
415     _computeTestGroupStatus()
416     {
417         if (this._status || !this.isComplete())
418             return;
419
420         console.assert(this._rootSetIndexRowIndexMap.length <= 2); // FIXME: Support having more root sets.
421
422         var result = this._testGroup.compareTestResults(
423             this._rootSetIndexRowIndexMap[0].rootSet, this._rootSetIndexRowIndexMap[1].rootSet);
424
425         this._label = result.label;
426         this._title = result.fullLabel;
427         this._status = result.status;
428     }
429 }
430
431 AnalysisResultsViewer.TestGroupStackingGrid = class {
432     constructor(rowCount)
433     {
434         this._blocks = [];
435         this._columns = null;
436         this._rowCount = rowCount;
437     }
438
439     insertBlockToColumn(newBlock)
440     {
441         console.assert(newBlock instanceof AnalysisResultsViewer.TestGroupStackingBlock);
442         for (var i = this._blocks.length - 1; i >= 0; i--) {
443             var currentBlock = this._blocks[i];
444             if (currentBlock.startRowIndex() == newBlock.startRowIndex()
445                 && currentBlock.endRowIndex() == newBlock.endRowIndex()) {
446                 this._blocks.splice(i + 1, 0, newBlock);
447                 return;
448             }
449         }
450         this._blocks.push(newBlock);
451     }
452
453     layout()
454     {
455         this._columns = [];
456         for (var block of this._blocks)
457             this._layoutBlock(block);
458     }
459
460     _layoutBlock(newBlock)
461     {
462         for (var columnIndex = 0; columnIndex < this._columns.length; columnIndex++) {
463             var existingColumn = this._columns[columnIndex];
464             if (newBlock.isThin() != existingColumn[0].isThin())
465                 continue;
466
467             for (var i = 0; i < existingColumn.length; i++) {
468                 var currentBlock = existingColumn[i];
469                 if ((!i || existingColumn[i - 1].endRowIndex() < newBlock.startRowIndex())
470                     && newBlock.endRowIndex() < currentBlock.startRowIndex()) {
471                     existingColumn.splice(i, 0, newBlock);
472                     return;
473                 }
474             }
475
476             var lastBlock = existingColumn[existingColumn.length - 1];
477             if (lastBlock.endRowIndex() < newBlock.startRowIndex()) {
478                 existingColumn.push(newBlock);
479                 return;
480             }
481         }
482         this._columns.push([newBlock]);
483     }
484
485     createCellsForRow(rowIndex)
486     {
487         var element = ComponentBase.createElement;
488         var link = ComponentBase.createLink;
489
490         var cells = [element('td', {class: 'stacking-block'}, '')];
491         for (var columnIndex = 0; columnIndex < this._columns.length; columnIndex++) {
492             var blocksInColumn = this._columns[columnIndex];
493             if (!rowIndex && blocksInColumn[0].startRowIndex()) {
494                 cells.push(this._createEmptyStackingCell(blocksInColumn[0].startRowIndex()));
495                 continue;
496             }
497             for (var i = 0; i < blocksInColumn.length; i++) {
498                 var block = blocksInColumn[i];
499                 if (block.startRowIndex() == rowIndex) {
500                     cells.push(block.createStackingCell());
501                     break;
502                 }
503                 var rowCount = i + 1 < blocksInColumn.length ? blocksInColumn[i + 1].startRowIndex() : this._rowCount;
504                 var remainingRows = rowCount - block.endRowIndex() - 1;
505                 if (rowIndex == block.endRowIndex() + 1 && rowIndex < rowCount)
506                     cells.push(this._createEmptyStackingCell(remainingRows));
507             }
508         }
509
510         return cells;
511     }
512
513     _createEmptyStackingCell(rowspan, content)
514     {
515         return ComponentBase.createElement('td', {rowspan: rowspan, class: 'stacking-block'}, '');
516     }
517
518 }