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