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