bbca73dbd7df54002f21024f7b689a4a9c07d2dc
[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]));
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             for (var i = 0; i < rowList.length; i++) {
195                 if (rowList[i] instanceof AnalysisResultsViewer.ExpandableRow)
196                     continue;
197
198                 var rowTime = rowList[i].rootSet().latestCommitTime();
199                 if (rowTime > groupTime) {
200                     var newRow = new ResultsTableRow(null, entry.rootSet());
201                     rowToMatchingRootSets.set(newRow, [entry]);
202                     rowList.splice(i, 0, newRow);
203                     return;
204                 }
205
206                 if (rowTime == groupTime) {
207                     // Missing some commits. Do as best as we can to avoid going backwards in time.
208                     var repositoriesInNewRow = entry.rootSet().repositories();
209                     for (var j = i; j < rowList.length; j++) {
210                         if (rowList[j] instanceof AnalysisResultsViewer.ExpandableRow)
211                             continue;
212                         for (var repository of repositoriesInNewRow) {
213                             var newCommit = entry.rootSet().commitForRepository(repository);
214                             var rowCommit = rowList[j].rootSet().commitForRepository(repository);
215                             if (!rowCommit || newCommit.time() < rowCommit.time()) {
216                                 var row = new ResultsTableRow(null, entry.rootSet());
217                                 rowToMatchingRootSets.set(row, [entry]);
218                                 rowList.splice(j, 0, row);
219                                 return;
220                             }
221                         }
222                     }
223                 }
224             }
225
226             var newRow = new ResultsTableRow(null, entry.rootSet());
227             rowToMatchingRootSets.set(newRow, [entry]);
228             rowList.push(newRow);
229         });
230
231         return rowList;
232     }
233
234     _classForTestGroup(testGroup)
235     {
236         return 'stacked-test-group-' + testGroup.id();
237     }
238
239     _openStackingBlock(testGroup)
240     {
241         if (this._testGroupCallback)
242             this._testGroupCallback(testGroup);
243     }
244     
245     _expandBetween(pointBeforeExpansion, pointAfterExpansion)
246     {
247         console.assert(pointBeforeExpansion.series == pointAfterExpansion.series);
248         var indexBeforeStart = pointBeforeExpansion.seriesIndex;
249         var indexAfterEnd = pointAfterExpansion.seriesIndex;
250         console.assert(indexBeforeStart + 1 < indexAfterEnd);
251
252         var series = pointAfterExpansion.series;
253         var increment = Math.ceil((indexAfterEnd - indexBeforeStart) / 5);
254         if (increment < 3)
255             increment = 1;
256         for (var i = indexBeforeStart + 1; i < indexAfterEnd; i += increment)
257             this._expandedPoints.add(series.findPointByIndex(i));
258         this._shouldRenderTable = true;
259         this.render();
260     }
261
262     static htmlTemplate()
263     {
264         return `<section class="analysis-view">${ResultsTable.htmlTemplate()}</section>`;
265     }
266
267     static cssTemplate()
268     {
269         return ResultsTable.cssTemplate() + `
270             .analysis-view .stacking-block {
271                 position: relative;
272                 border: solid 1px #fff;
273                 cursor: pointer;
274             }
275
276             .analysis-view .stacking-block a {
277                 display: block;
278                 text-decoration: none;
279                 color: inherit;
280                 font-size: 0.8rem;
281                 padding: 0 0.1rem;
282                 max-width: 3rem;
283             }
284
285             .analysis-view .stacking-block:not(.failed) {
286                 color: black;
287                 opacity: 1;
288             }
289
290             .analysis-view .stacking-block.selected,
291             .analysis-view .stacking-block:hover {
292                 text-decoration: underline;
293             }
294
295             .analysis-view .stacking-block.selected:before {
296                 content: '';
297                 position: absolute;
298                 left: 0px;
299                 top: 0px;
300                 width: calc(100% - 2px);
301                 height: calc(100% - 2px);
302                 border: solid 1px #333;
303             }
304
305             .analysis-view .stacking-block.failed {
306                 background: rgba(128, 51, 128, 0.5);
307             }
308             .analysis-view .stacking-block.unchanged {
309                 background: rgba(128, 128, 128, 0.5);
310             }
311             .analysis-view .stacking-block.pending {
312                 background: rgba(204, 204, 51, 0.2);
313             }
314             .analysis-view .stacking-block.running {
315                 background: rgba(204, 204, 51, 0.5);
316             }
317             .analysis-view .stacking-block.worse {
318                 background: rgba(255, 102, 102, 0.5);
319             }
320             .analysis-view .stacking-block.better {
321                 background: rgba(102, 102, 255, 0.5);
322             }
323
324             .analysis-view .point-label-with-expansion-link {
325                 font-size: 0.7rem;
326             }
327             .analysis-view .point-label-with-expansion-link a {
328                 color: #999;
329                 text-decoration: none;
330             }
331         `;
332     }
333 }
334
335 ComponentBase.defineElement('analysis-results-viewer', AnalysisResultsViewer);
336
337 AnalysisResultsViewer.ExpandableRow = class extends ResultsTableRow {
338     constructor(callback)
339     {
340         super(null, null);
341         this._callback = callback;
342     }
343
344     resultContent() { return ''; }
345
346     heading()
347     {
348         return ComponentBase.createElement('span', {class: 'point-label-with-expansion-link'}, [
349             ComponentBase.createLink('(Expand)', 'Expand', this._callback),
350         ]);
351     }
352 }
353
354 AnalysisResultsViewer.RootSetInTestGroup = class {
355     constructor(testGroup, rootSet)
356     {
357         console.assert(testGroup instanceof TestGroup);
358         console.assert(rootSet instanceof RootSet);
359         this._testGroup = testGroup;
360         this._rootSet = rootSet;
361     }
362
363     testGroup() { return this._testGroup; }
364     rootSet() { return this._rootSet; }
365 }
366
367 AnalysisResultsViewer.TestGroupStackingBlock = class {
368     constructor(testGroup, className, callback)
369     {
370         this._testGroup = testGroup;
371         this._rootSetIndexRowIndexMap = [];
372         this._className = className;
373         this._label = null;
374         this._title = null;
375         this._status = null;
376         this._callback = callback;
377     }
378
379     addRowIndex(rootSetInTestGroup, rowIndex)
380     {
381         console.assert(rootSetInTestGroup instanceof AnalysisResultsViewer.RootSetInTestGroup);
382         this._rootSetIndexRowIndexMap.push({rootSet: rootSetInTestGroup.rootSet(), rowIndex: rowIndex});
383     }
384
385     testGroup() { return this._testGroup; }
386
387     createStackingCell()
388     {
389         this._computeTestGroupStatus();
390
391         return ComponentBase.createElement('td', {
392             rowspan: this.endRowIndex() - this.startRowIndex() + 1,
393             title: this._title,
394             class: 'stacking-block ' + this._className + ' ' + this._status,
395             onclick: this._callback,
396         }, ComponentBase.createLink(this._label, this._title, this._callback));
397     }
398
399     isComplete() { return this._rootSetIndexRowIndexMap.length >= 2; }
400
401     startRowIndex() { return this._rootSetIndexRowIndexMap[0].rowIndex; }
402     endRowIndex() { return this._rootSetIndexRowIndexMap[this._rootSetIndexRowIndexMap.length - 1].rowIndex; }
403     isThin()
404     {
405         this._computeTestGroupStatus();
406         return this._status == 'failed';
407     }
408
409     _computeTestGroupStatus()
410     {
411         if (this._status || !this.isComplete())
412             return;
413
414         console.assert(this._rootSetIndexRowIndexMap.length <= 2); // FIXME: Support having more root sets.
415
416         var result = this._testGroup.compareTestResults(
417             this._rootSetIndexRowIndexMap[0].rootSet, this._rootSetIndexRowIndexMap[1].rootSet);
418
419         this._label = result.label;
420         this._title = result.fullLabel;
421         this._status = result.status;
422     }
423 }
424
425 AnalysisResultsViewer.TestGroupStackingGrid = class {
426     constructor(rowCount)
427     {
428         this._blocks = [];
429         this._columns = null;
430         this._rowCount = rowCount;
431     }
432
433     insertBlockToColumn(newBlock)
434     {
435         console.assert(newBlock instanceof AnalysisResultsViewer.TestGroupStackingBlock);
436         for (var i = this._blocks.length - 1; i >= 0; i--) {
437             var currentBlock = this._blocks[i];
438             if (currentBlock.startRowIndex() == newBlock.startRowIndex()
439                 && currentBlock.endRowIndex() == newBlock.endRowIndex()) {
440                 this._blocks.splice(i + 1, 0, newBlock);
441                 return;
442             }
443         }
444         this._blocks.push(newBlock);
445     }
446
447     layout()
448     {
449         this._columns = [];
450         for (var block of this._blocks)
451             this._layoutBlock(block);
452     }
453
454     _layoutBlock(newBlock)
455     {
456         for (var columnIndex = 0; columnIndex < this._columns.length; columnIndex++) {
457             var existingColumn = this._columns[columnIndex];
458             if (newBlock.isThin() != existingColumn[0].isThin())
459                 continue;
460
461             for (var i = 0; i < existingColumn.length; i++) {
462                 var currentBlock = existingColumn[i];
463                 if ((!i || existingColumn[i - 1].endRowIndex() < newBlock.startRowIndex())
464                     && newBlock.endRowIndex() < currentBlock.startRowIndex()) {
465                     existingColumn.splice(i, 0, newBlock);
466                     return;
467                 }
468             }
469
470             var lastBlock = existingColumn[existingColumn.length - 1];
471             if (lastBlock.endRowIndex() < newBlock.startRowIndex()) {
472                 existingColumn.push(newBlock);
473                 return;
474             }
475         }
476         this._columns.push([newBlock]);
477     }
478
479     createCellsForRow(rowIndex)
480     {
481         var element = ComponentBase.createElement;
482         var link = ComponentBase.createLink;
483
484         var cells = [element('td', {class: 'stacking-block'}, '')];
485         for (var columnIndex = 0; columnIndex < this._columns.length; columnIndex++) {
486             var blocksInColumn = this._columns[columnIndex];
487             if (!rowIndex && blocksInColumn[0].startRowIndex()) {
488                 cells.push(this._createEmptyStackingCell(blocksInColumn[0].startRowIndex()));
489                 continue;
490             }
491             for (var i = 0; i < blocksInColumn.length; i++) {
492                 var block = blocksInColumn[i];
493                 if (block.startRowIndex() == rowIndex) {
494                     cells.push(block.createStackingCell());
495                     break;
496                 }
497                 var rowCount = i + 1 < blocksInColumn.length ? blocksInColumn[i + 1].startRowIndex() : this._rowCount;
498                 var remainingRows = rowCount - block.endRowIndex() - 1;
499                 if (rowIndex == block.endRowIndex() + 1 && rowIndex < rowCount)
500                     cells.push(this._createEmptyStackingCell(remainingRows));
501             }
502         }
503
504         return cells;
505     }
506
507     _createEmptyStackingCell(rowspan, content)
508     {
509         return ComponentBase.createElement('td', {rowspan: rowspan, class: 'stacking-block'}, '');
510     }
511
512 }