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