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