Show t-test results based on individual measurements to analysis task page.
[WebKit-https.git] / Websites / perf.webkit.org / tools / js / test-group-result-page.js
1 class TestGroupResultPage extends MarkupPage {
2     constructor(title)
3     {
4         super(title);
5         this._testGroup = null;
6         this._analysisResults = null;
7         this._analysisURL = null;
8         this._analysisTask = null;
9         this._constructTablesLazily = new LazilyEvaluatedFunction(this.constructTables.bind(this));
10     }
11
12     async setTestGroup(testGroup)
13     {
14         this._testGroup = testGroup;
15         this._analysisTask = await testGroup.fetchTask();
16         this._analysisResults = await AnalysisResults.fetch(this._analysisTask.id());
17         this._analysisURL = TestGroupResultPage._urlForAnalysisTask(this._analysisTask);
18         this.enqueueToRender();
19     }
20
21     static _urlForAnalysisTask(analysisTask)
22     {
23         return global.RemoteAPI.url(`/v3/#/analysis/task/${analysisTask.id()}`);
24     }
25
26     _resultsForTestGroup(testGroup, analysisResultsView)
27     {
28         const resultsByCommitSet = new Map;
29         let maxValue = -Infinity;
30         let minValue = Infinity;
31         for (const commitSet of testGroup.requestedCommitSets())
32         {
33             const buildRequestsForCommitSet = testGroup.requestsForCommitSet(commitSet);
34             const results = buildRequestsForCommitSet.map((buildRequest) => analysisResultsView.resultForRequest(buildRequest));
35             resultsByCommitSet.set(commitSet, results);
36             for (const result of results) {
37                 if (!result)
38                     continue;
39                 maxValue = Math.max(maxValue, result.value);
40                 minValue = Math.min(minValue, result.value);
41             }
42         }
43         const diff = maxValue - minValue;
44         minValue -= diff * 0.1;
45         maxValue += diff * 0.1;
46
47         return {resultsByCommitSet, widthForValue: (value) => (value - minValue) / (maxValue - minValue) * 100};
48     }
49
50     constructTables(testGroup, analysisResults, analysisURL, analysisTask)
51     {
52         const requestedCommitSets = testGroup.requestedCommitSets();
53         console.assert(requestedCommitSets.length, 2);
54
55         const metrics = analysisTask.metric() ? [analysisTask.metric()] : testGroup.test().metrics();
56
57         const tablesWithSummary = metrics.map((metric) => this._constructTableForMetric(metric, testGroup, analysisResults, requestedCommitSets));
58         const description = this.createElement('h1', [this.createElement('em', testGroup.name()), ' - ', this.createElement('em', this.createLink(analysisTask.name(), analysisURL))]);
59
60         return [description, tablesWithSummary];
61     }
62
63     _constructTableForMetric(metric, testGroup, analysisResults, requestedCommitSets)
64     {
65         const formatter = metric.makeFormatter(4);
66         const deltaFormatter = metric.makeFormatter(2, false);
67         const formatValue = (value, interval) => {
68             const delta = interval ? (interval[1] - interval[0]) / 2 : null;
69             const resultParts = [value == null || isNaN(value) ? '-' : formatter(value)];
70             if (delta != null && !isNaN(delta))
71                 resultParts.push(` \u00b1 ${deltaFormatter(delta)}`);
72             return resultParts;
73         };
74
75         const analysisResultsView = analysisResults.viewForMetric(metric);
76         const {resultsByCommitSet, widthForValue} = this._resultsForTestGroup(testGroup, analysisResultsView);
77
78         const tableBodies = [];
79
80         const beforeResults = resultsByCommitSet.get(requestedCommitSets[0]).filter((result) => !!result);
81         const afterResults = resultsByCommitSet.get(requestedCommitSets[1]).filter((result) => !!result);
82         const comparison = testGroup.compareTestResults(metric, beforeResults, afterResults);
83         const changeStyleClassForMean = `${comparison.isStatisticallySignificantForMean ? comparison.changeType : 'insignificant'}-result`;
84         const changeStyleClassForIndividual = `${comparison.isStatisticallySignificantForIndividual ? comparison.changeType : 'insignificant'}-result`;
85         const caption = this.createElement('caption', `${testGroup.test().name()} - ${metric.aggregatorLabel()}`);
86
87         tableBodies.push(this.createElement('tbody', {class: 'comparision-table-body'}, [
88             this.createElement('tr', [this.createElement('td', 'Comparision by Mean'),
89                 this.createElement('td', this.createElement('em', {class: changeStyleClassForMean}, comparison.fullLabelForMean))]),
90             this.createElement('tr', [this.createElement('td', 'Comparision by Individual'),
91                 this.createElement('td', this.createElement('em', {class: changeStyleClassForIndividual}, comparison.fullLabelForIndividual))])
92         ]));
93
94         for (const commitSet of requestedCommitSets) {
95             let firstRow = true;
96             const tableRows = [];
97             const results = resultsByCommitSet.get(commitSet);
98             const values = results.filter((result) => !!result).map((result) => result.value);
99             const averageColumnContents = formatValue(Statistics.mean(values), Statistics.confidenceInterval(values));
100             const label = testGroup.labelForCommitSet(commitSet);
101
102             for (const result of results) {
103                 const cellValue = result ? formatValue(result.value, result.interval).join('') : 'Failed';
104                 const barWidth = result ? widthForValue(result.value) : 0;
105                 tableRows.push(this._constructTableRow(cellValue, barWidth, firstRow, results.length, label, averageColumnContents));
106                 firstRow = false;
107             }
108             tableBodies.push(this.createElement('tbody', tableRows));
109         }
110
111         return this.createElement('table', {class: 'result-table'}, [caption, tableBodies]);
112     }
113
114     _constructTableRow(cellValue, barWidth, firstRow, tableHeadRowSpan, labelForCommitSet, averageColumnContents)
115     {
116         const barGraph = new BarGraph;
117         barGraph.setWidth(barWidth);
118         const cellContent = [barGraph, cellValue];
119
120         if (firstRow) {
121             return this.createElement('tr', [
122                 this.createElement('th', {class: 'first-row', rowspan: tableHeadRowSpan},
123                     [labelForCommitSet + ': ', averageColumnContents.map((content => this.createElement('span', {class: 'no-wrap'}, content)))]),
124                 this.createElement('td', {class: 'result-cell first-row'}, cellContent),
125             ])
126         }
127         else
128             return this.createElement('tr', this.createElement('td', {class: 'result-cell'}, cellContent));
129     }
130
131     render()
132     {
133         super.render();
134         this.renderReplace(this.content(), this._constructTablesLazily.evaluate(this._testGroup, this._analysisResults, this._analysisURL, this._analysisTask));
135     }
136
137     static get pageContent()
138     {
139         return [];
140     }
141
142     static get styleTemplate()
143     {
144         return {
145             'body': {
146                 'font-family': 'sans-serif',
147             },
148             'h1': {
149                 'font-size': '1.3rem',
150                 'font-weight': 'normal',
151             },
152             'em': {
153                 'font-weight': 'bold',
154                 'font-style': 'normal',
155                 'padding-right': '2rem',
156             },
157             'caption': {
158                 'font-size': '1.3rem',
159                 'margin': '1rem 0',
160                 'text-align': 'left',
161                 'white-space': 'nowrap',
162             },
163             'td': {
164                 'padding': '0.2rem',
165             },
166             '.first-row': {
167                 'border-top': 'solid 1px #ccc',
168             },
169             '.no-wrap': {
170                 'white-space': 'nowrap',
171             },
172             'th': {
173                 'padding': '0.2rem',
174             },
175             '.result-table': {
176                 'margin-top': '1rem',
177                 'text-align': 'center',
178                 'border-collapse': 'collapse',
179             },
180             '.comparision-table-body': {
181                 'text-align': 'left',
182             },
183             '.result-cell': {
184                 'min-width': '20rem',
185                 'position': 'relative',
186             },
187             '.worse-result': {
188                 'color': '#c33',
189             },
190             '.better-result': {
191                 'color': '#33c',
192             },
193             '.insignificant-result': {
194                 'color': '#666',
195             },
196         }
197     }
198 }
199
200 class BarGraph extends MarkupComponentBase {
201     constructor()
202     {
203         super('bar-graph');
204         this._constructBarGraphLazily = new LazilyEvaluatedFunction(this._constructBarGraph.bind(this));
205     }
206
207     setWidth(width)
208     {
209         this._width = width;
210         this.enqueueToRender();
211     }
212
213     render()
214     {
215         super.render();
216         this.renderReplace(this.content(), this._constructBarGraphLazily.evaluate(this._width));
217     }
218
219     _constructBarGraph(width)
220     {
221         const barGraphPlaceholder = this.createElement('div',{class: 'bar-graph-placeholder'});
222         if (width)
223             barGraphPlaceholder.style.width = width + '%';
224         return barGraphPlaceholder;
225     }
226
227     static get contentTemplate()
228     {
229         return [];
230     }
231
232     static get styleTemplate()
233     {
234         return {
235             ':host': {
236                 'position': 'absolute',
237                 'left': 0,
238                 'top': 0,
239                 'width': 'calc(100% - 2px)',
240                 'height': 'calc(100% - 2px)',
241                 'padding': '1px',
242                 'z-index': -1,
243             },
244             '.bar-graph-placeholder': {
245                 'background-color': '#ccc',
246                 'height': '100%',
247                 'width': '0rem',
248             }
249         };
250     }
251 }
252
253 MarkupComponentBase.defineElement('test-group-result-page', TestGroupResultPage);
254 MarkupComponentBase.defineElement('bar-graph', BarGraph);
255
256
257 if (typeof module !== 'undefined')
258     module.exports.TestGroupResultPage = TestGroupResultPage;