Make it possible to view results for sub tests and metrics in A/B testing
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / components / test-group-results-viewer.js
1
2 class TestGroupResultsViewer extends ComponentBase {
3     constructor()
4     {
5         super('test-group-results-table');
6         this._analysisResults = null;
7         this._testGroup = null;
8         this._startPoint = null;
9         this._endPoint = null;
10         this._currentMetric = null;
11         this._expandedTests = new Set;
12         this._barGraphCellMap = new Map;
13         this._renderResultsTableLazily = new LazilyEvaluatedFunction(this._renderResultsTable.bind(this));
14         this._renderCurrentMetricsLazily = new LazilyEvaluatedFunction(this._renderCurrentMetrics.bind(this));
15     }
16
17     setTestGroup(currentTestGroup)
18     {
19         this._testGroup = currentTestGroup;
20         this.enqueueToRender();
21     }
22
23     setAnalysisResults(analysisResults, metric)
24     {
25         this._analysisResults = analysisResults;
26         this._currentMetric = metric;
27         this.enqueueToRender();
28     }
29
30     render()
31     {
32         if (!this._testGroup || !this._analysisResults)
33             return;
34
35         this._renderResultsTableLazily.evaluate(this._testGroup, this._expandedTests, ...this._analysisResults.highestTests());
36         this._renderCurrentMetricsLazily.evaluate(this._currentMetric);
37     }
38
39     _renderResultsTable(testGroup, expandedTests, ...tests)
40     {
41         let maxDepth = 0;
42         for (const test of expandedTests)
43             maxDepth = Math.max(maxDepth, test.path().length);
44
45         const element = ComponentBase.createElement;
46         this.renderReplace(this.content('results'), [
47             element('thead', [
48                 element('tr', [
49                     element('th', {colspan: maxDepth + 1}, 'Test'),
50                     element('th', {class: 'metric-direction'}, ''),
51                     element('th', {colspan: 2}, 'Results'),
52                     element('th', 'Averages'),
53                     element('th', 'Comparison'),
54                 ]),
55             ]),
56             tests.map((test) => this._buildRowsForTest(testGroup, expandedTests, test, [], maxDepth, 0))]);
57     }
58
59     _buildRowsForTest(testGroup, expandedTests, test, sharedPath, maxDepth, depth)
60     {
61         const element = ComponentBase.createElement;
62         const rows = element('tbody', test.metrics().map((metric) => this._buildRowForMetric(testGroup, metric, sharedPath, maxDepth, depth)));
63
64         if (expandedTests.has(test)) {
65             return [rows, test.childTests().map((childTest) => {
66                 return this._buildRowsForTest(testGroup, expandedTests, childTest, test.path(), maxDepth, depth + 1);
67             })];
68         }
69
70         if (test.childTests().length) {
71             const link = ComponentBase.createLink;
72             return [rows, element('tr', {class: 'breakdown'}, [
73                 element('td', {colspan: maxDepth + 1}, link('(Breakdown)', () => {
74                     this._expandedTests = new Set([...expandedTests, test]);
75                     this.enqueueToRender();
76                 })),
77                 element('td', {colspan: 3}),
78             ])];
79         }
80
81         return rows;
82     }
83
84     _buildRowForMetric(testGroup, metric, sharedPath, maxDepth, depth)
85     {
86         const commitSets = testGroup.requestedCommitSets();
87         const valueMap = this._buildValueMap(testGroup, this._analysisResults.viewForMetric(metric));
88
89         const formatter = metric.makeFormatter(4);
90         const deltaFormatter = metric.makeFormatter(2, false);
91         const formatValue = (value, interval) => {
92             const delta = interval ? (interval[1] - interval[0]) / 2 : null;
93             return value == null || isNaN(value) ? '-' : `${formatter(value)} \u00b1 ${deltaFormatter(delta)}`;
94         }
95
96         const barGroup = new BarGraphGroup();
97         const barCells = [];
98         const createConfigurationRow = (commitSet, previousCommitSet, barColor, meanIndicatorColor) => {
99             const entry = valueMap.get(commitSet);
100             const previousEntry = valueMap.get(previousCommitSet);
101
102             const comparison = entry && previousEntry ? testGroup.compareTestResults(metric, previousEntry.filteredValues, entry.filteredValues) : null;
103             const valueLabels = entry.measurements.map((measurement) => measurement ?  formatValue(measurement.value, measurement.interval) : '-');
104
105             const barCell = element('td', {class: 'plot-bar'},
106                 element('div', barGroup.addBar(entry.allValues, valueLabels, entry.mean, entry.interval, barColor, meanIndicatorColor)));
107             barCell.expandedHeight = +valueLabels.length + 'rem';
108             barCells.push(barCell);
109
110             const significance = comparison && comparison.isStatisticallySignificant ? 'significant' : 'negligible';
111             const changeType = comparison ? comparison.changeType : null;
112             return [
113                 element('th', testGroup.labelForCommitSet(commitSet)),
114                 barCell,
115                 element('td', formatValue(entry.mean, entry.interval)),
116                 element('td', {class: `comparison ${changeType} ${significance}`}, comparison ? comparison.fullLabel : ''),
117             ];
118         }
119
120         this._barGraphCellMap.set(metric, {barGroup, barCells});
121
122         const rowspan = commitSets.length;
123         const element = ComponentBase.createElement;
124         const link = ComponentBase.createLink;
125         const metricName = metric.test().metrics().length == 1 ? metric.test().relativeName(sharedPath) : metric.relativeName(sharedPath);
126         const onclick = this.createEventHandler((event) => {
127             if (this._currentMetric == metric) {
128                 if (event.target.localName == 'bar-graph')
129                     return;
130                 this._currentMetric = null;
131             } else
132                 this._currentMetric = metric;
133             this.enqueueToRender();
134         });
135         return [
136             element('tr', {onclick}, [
137                 this._buildEmptyCells(depth, rowspan),
138                 element('th', {colspan: maxDepth - depth + 1, rowspan}, link(metricName, onclick)),
139                 element('td', {class: 'metric-direction', rowspan}, metric.isSmallerBetter() ? '\u21A4' : '\u21A6'),
140                 createConfigurationRow(commitSets[0], null, '#ddd', '#333')
141             ]),
142             commitSets.slice(1).map((commitSet, setIndex) => {
143                 return element('tr', {onclick},
144                     createConfigurationRow(commitSet, commitSets[setIndex], '#aaa', '#000'));
145             })
146         ];
147     }
148
149     _buildValueMap(testGroup, resultsView)
150     {
151         const commitSets = testGroup.requestedCommitSets();
152         const map = new Map;
153         for (const commitSet of commitSets) {
154             const requests = testGroup.requestsForCommitSet(commitSet);
155             const measurements = requests.map((request) => resultsView.resultForRequest(request));
156             const filteredValues = measurements.filter((result) => !!result).map((measurement) => measurement.value);
157             const allValues = measurements.map((result) => result != null ? result.value : NaN);
158             const interval = Statistics.confidenceInterval(filteredValues);
159             map.set(commitSet, {requests, measurements, filteredValues, allValues, mean: Statistics.mean(filteredValues), interval});
160         }
161         return map;
162     }
163
164     _buildEmptyCells(count, rowspan)
165     {
166         const element = ComponentBase.createElement;
167         const emptyCells = [];
168         for (let i = 0; i < count; i++)
169             emptyCells.push(element('td', {rowspan}, ''));
170         return emptyCells;
171     }
172
173     _renderCurrentMetrics(currentMetric)
174     {
175         for (const entry of this._barGraphCellMap.values()) {
176             for (const cell of entry.barCells) {
177                 cell.style.height = null;
178                 cell.parentNode.className = null;
179             }
180             entry.barGroup.setShowLabels(false);
181         }
182
183         const entry = this._barGraphCellMap.get(currentMetric);
184         if (entry) {
185             for (const cell of entry.barCells) {
186                 cell.style.height = cell.expandedHeight;
187                 cell.parentNode.className = 'selected';
188             }
189             entry.barGroup.setShowLabels(true);
190         }
191     }
192
193     static htmlTemplate()
194     {
195         return `<table id="results"></table>`;
196     }
197
198     static cssTemplate()
199     {
200         return `
201             table {
202                 border-collapse: collapse;
203                 margin: 0;
204                 padding: 0;
205             }
206             td, th {
207                 border: none;
208                 padding: 0;
209                 margin: 0;
210                 white-space: nowrap;
211             }
212             td:not(.metric-direction),
213             th:not(.metric-direction) {
214                 padding: 0.1rem 0.5rem;
215             }
216             td:not(.metric-direction) {
217                 min-width: 2rem;
218             }
219             td.metric-direction {
220                 font-size: large;
221             }
222             bar-graph {
223                 width: 7rem;
224                 height: 1rem;
225             }
226             th {
227                 font-weight: inherit;
228             }
229             thead th {
230                 font-weight: inherit;
231                 color: #c93;
232             }
233
234             tr.selected > td,
235             tr.selected > th {
236                 background: rgba(204, 153, 51, 0.05);
237             }
238
239             tr:first-child > td,
240             tr:first-child > th {
241                 border-top: solid 1px #eee;
242             }
243
244             tbody th {
245                 text-align: left;
246             }
247             tbody th,
248             tbody td {
249                 cursor: pointer;
250             }
251             a {
252                 color: inherit;
253                 text-decoration: inherit;
254             }
255             bar-graph {
256                 width: 100%;
257                 height: 100%;
258             }
259             td.plot-bar {
260                 position: relative;
261                 min-width: 7rem;
262             }
263             td.plot-bar > * {
264                 display: block;
265                 position: absolute;
266                 width: 100%;
267                 height: 100%;
268                 top: 0;
269                 left: 0;
270             }
271             .comparison {
272                 text-align: left;
273             }
274             .negligible {
275                 color: #999;
276             }
277             .significant.worse {
278                 color: #c33;
279             }
280             .significant.better {
281                 color: #33c;
282             }
283             tr.breakdown td {
284                 padding: 0;
285                 font-size: small;
286                 text-align: center;
287             }
288             tr.breakdown a {
289                 display: inline-block;
290                 text-decoration: none;
291                 color: #999;
292                 margin-bottom: 0.2rem;
293             }
294         `;
295     }
296 }
297
298 ComponentBase.defineElement('test-group-results-viewer', TestGroupResultsViewer);