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