A/B testing results should be visualized intuitively on v3 UI
[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())) {
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                 border: solid 1px #fff;
243                 cursor: pointer;
244             }
245
246             .analysis-view .stacking-block a {
247                 display: block;
248                 text-decoration: none;
249                 color: inherit;
250                 font-size: 0.8rem;
251                 padding: 0 0.1rem;
252             }
253
254             .analysis-view .stacking-block:not(.failed) {
255                 color: white;
256                 opacity: 1;
257             }
258
259             .analysis-view .stacking-block.selected {
260
261             }
262
263             .analysis-view .stacking-block.failed {
264                 background: rgba(128, 51, 128, 0.4);
265             }
266             .analysis-view .stacking-block.failed:hover {
267                 background: rgba(128, 51, 128, 0.6);
268             }
269             .analysis-view .stacking-block.failed.selected {
270                 background: rgba(128, 51, 128, 1);
271                 color: white;
272             }
273
274             .analysis-view .stacking-block.unchanged {
275                 background: rgba(128, 128, 128, 0.3);
276                 color: black;
277             }
278             .analysis-view .stacking-block.unchanged:hover {
279                 background: rgba(128, 128, 128, 0.6);
280             }
281             .analysis-view .stacking-block.unchanged.selected {
282                 background: rgba(128, 128, 128, 1);
283                 color: white;
284             }
285
286             .analysis-view .stacking-block.worse {
287                 background: rgba(255, 102, 102, 0.4);
288                 color: black;
289             }
290             .analysis-view .stacking-block.worse:hover {
291                 background: rgba(255, 102, 102, 0.6);
292             }
293             .analysis-view .stacking-block.worse.selected {
294                 background: rgba(255, 102, 102, 1);
295                 color: white;
296             }
297
298             .analysis-view .stacking-block.better {
299                 background: rgba(102, 102, 255, 0.4);
300                 color: black;
301             }
302             .analysis-view .stacking-block.better:hover {
303                 background: rgba(102, 102, 255, 0.6);
304             }
305             .analysis-view .stacking-block.better.selected {
306                 background: rgba(102, 102, 255, 1);
307                 color: white;
308             }
309         `;
310     }
311 }
312
313 ComponentBase.defineElement('analysis-results-viewer', AnalysisResultsViewer);
314
315 AnalysisResultsViewer.RootSetInTestGroup = class {
316     constructor(testGroup, rootSet)
317     {
318         console.assert(testGroup instanceof TestGroup);
319         console.assert(rootSet instanceof RootSet);
320         this._testGroup = testGroup;
321         this._rootSet = rootSet;
322     }
323
324     testGroup() { return this._testGroup; }
325     rootSet() { return this._rootSet; }
326 }
327
328 AnalysisResultsViewer.TestGroupStackingBlock = class {
329     constructor(testGroup, smallerIsBetter, className, callback)
330     {
331         this._testGroup = testGroup;
332         this._smallerIsBetter = smallerIsBetter;
333         this._rootSetIndexRowIndexMap = [];
334         this._className = className;
335         this._label = '-';
336         this._status = null;
337         this._callback = callback;
338     }
339
340     addRowIndex(rootSetInTestGroup, rowIndex)
341     {
342         console.assert(rootSetInTestGroup instanceof AnalysisResultsViewer.RootSetInTestGroup);
343         this._rootSetIndexRowIndexMap.push({rootSet: rootSetInTestGroup.rootSet(), rowIndex: rowIndex});
344     }
345
346     testGroup() { return this._testGroup; }
347
348     createStackingCell()
349     {
350         this._computeTestGroupStatus();
351
352         var title = this._testGroup.label();
353         return ComponentBase.createElement('td', {
354             rowspan: this.endRowIndex() - this.startRowIndex() + 1,
355             title: title,
356             class: 'stacking-block ' + this._className + ' ' + this._status,
357             onclick: this._callback,
358         }, ComponentBase.createLink(this._label, title, this._callback));
359     }
360
361     isComplete() { return this._rootSetIndexRowIndexMap.length >= 2; }
362
363     startRowIndex() { return this._rootSetIndexRowIndexMap[0].rowIndex; }
364     endRowIndex() { return this._rootSetIndexRowIndexMap[this._rootSetIndexRowIndexMap.length - 1].rowIndex; }
365     isThin()
366     {
367         this._computeTestGroupStatus();
368         return this._status == 'failed';
369     }
370
371     _computeTestGroupStatus()
372     {
373         if (this._status || !this.isComplete())
374             return;
375
376         console.assert(this._rootSetIndexRowIndexMap.length <= 2); // FIXME: Support having more root sets.
377
378         var beforeValues = this._valuesForRootSet(this._rootSetIndexRowIndexMap[0].rootSet);
379         var afterValues = this._valuesForRootSet(this._rootSetIndexRowIndexMap[1].rootSet);
380
381         var status = 'failed';
382         var beforeMean = Statistics.sum(beforeValues) / beforeValues.length;
383         var afterMean = Statistics.sum(afterValues) / afterValues.length;
384         if (beforeValues.length && afterValues.length) {
385             var diff = afterMean - beforeMean;
386             this._label = (diff / beforeMean * 100).toFixed(2) + '%';
387             status = 'unchanged';
388             if (Statistics.testWelchsT(beforeValues, afterValues))
389                 status = diff < 0 == this._smallerIsBetter ? 'better' : 'worse';
390         }
391
392         this._status = status;
393     }
394
395     _valuesForRootSet(rootSet)
396     {
397         var requests = this._testGroup.requestsForRootSet(rootSet);
398         var values = [];
399         for (var request of requests) {
400             if (request.result())
401                 values.push(request.result().value);
402         }
403         return values;
404     }
405 }
406
407 AnalysisResultsViewer.TestGroupStackingGrid = class {
408     constructor(rowCount)
409     {
410         this._blocks = [];
411         this._columns = null;
412         this._rowCount = rowCount;
413     }
414
415     insertBlockToColumn(newBlock)
416     {
417         console.assert(newBlock instanceof AnalysisResultsViewer.TestGroupStackingBlock);
418         for (var i = this._blocks.length - 1; i >= 0; i--) {
419             var currentBlock = this._blocks[i];
420             if (currentBlock.startRowIndex() == newBlock.startRowIndex()
421                 && currentBlock.endRowIndex() == newBlock.endRowIndex()) {
422                 this._blocks.splice(i + 1, 0, newBlock);
423                 return;
424             }
425         }
426         this._blocks.push(newBlock);
427     }
428
429     layout()
430     {
431         this._columns = [];
432         for (var block of this._blocks)
433             this._layoutBlock(block);
434     }
435
436     _layoutBlock(newBlock)
437     {
438         for (var columnIndex = 0; columnIndex < this._columns.length; columnIndex++) {
439             var existingColumn = this._columns[columnIndex];
440             if (newBlock.isThin() != existingColumn[0].isThin())
441                 continue;
442
443             for (var i = 0; i < existingColumn.length; i++) {
444                 var currentBlock = existingColumn[i];
445                 if ((!i || existingColumn[i - 1].endRowIndex() < newBlock.startRowIndex())
446                     && newBlock.endRowIndex() < currentBlock.startRowIndex()) {
447                     existingColumn.splice(i, 0, newBlock);
448                     return;
449                 }
450             }
451
452             var lastBlock = existingColumn[existingColumn.length - 1];
453             if (lastBlock.endRowIndex() < newBlock.startRowIndex()) {
454                 existingColumn.push(newBlock);
455                 return;
456             }
457         }
458         this._columns.push([newBlock]);
459     }
460
461     createCellsForRow(rowIndex)
462     {
463         var element = ComponentBase.createElement;
464         var link = ComponentBase.createLink;
465
466         var cells = [element('td', {class: 'stacking-block'}, '')];
467         for (var columnIndex = 0; columnIndex < this._columns.length; columnIndex++) {
468             var blocksInColumn = this._columns[columnIndex];
469             if (!rowIndex && blocksInColumn[0].startRowIndex()) {
470                 cells.push(this._createEmptyStackingCell(blocksInColumn[0].startRowIndex()));
471                 continue;
472             }
473             for (var i = 0; i < blocksInColumn.length; i++) {
474                 var block = blocksInColumn[i];
475                 if (block.startRowIndex() == rowIndex) {
476                     cells.push(block.createStackingCell());
477                     break;
478                 }
479                 var rowCount = i + 1 < blocksInColumn.length ? blocksInColumn[i + 1].startRowIndex() : this._rowCount;
480                 var remainingRows = rowCount - block.endRowIndex() - 1;
481                 if (rowIndex == block.endRowIndex() + 1 && rowIndex < rowCount)
482                     cells.push(this._createEmptyStackingCell(remainingRows));
483             }
484         }
485
486         return cells;
487     }
488
489     _createEmptyStackingCell(rowspan, content)
490     {
491         return ComponentBase.createElement('td', {rowspan: rowspan, class: 'stacking-block'}, '');
492     }
493
494 }