Extend perf dashboard to support multiple summary pages.
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / pages / summary-page.js
1
2 class SummaryPage extends PageWithHeading {
3
4     constructor(summarySettings)
5     {
6         super(summarySettings.name, null);
7
8         this._route = summarySettings.route;
9         this._table = {
10             heading: summarySettings.platformGroups,
11             groups: [],
12         };
13         this._shouldConstructTable = true;
14         this._renderQueue = [];
15         this._configGroups = [];
16         this._excludedConfigurations = summarySettings.excludedConfigurations;
17
18         for (var metricGroup of summarySettings.metricGroups) {
19             var group = {name: metricGroup.name, rows: []};
20             this._table.groups.push(group);
21             for (var subMetricGroup of metricGroup.subgroups) {
22                 var row = {name: subMetricGroup.name, cells: []};
23                 group.rows.push(row);
24                 for (var platformGroup of summarySettings.platformGroups)
25                     row.cells.push(this._createConfigurationGroup(platformGroup.platforms, subMetricGroup.metrics));
26             }
27         }
28     }
29
30     routeName() { return `summary/${this._route}`; }
31
32     open(state)
33     {
34         super.open(state);
35
36         var current = Date.now();
37         var timeRange = [current - 24 * 3600 * 1000, current];
38         for (var group of this._configGroups)
39             group.fetchAndComputeSummary(timeRange).then(this.render.bind(this));
40     }
41
42     render()
43     {
44         Instrumentation.startMeasuringTime('SummaryPage', 'render');
45         super.render();
46
47         if (this._shouldConstructTable) {
48             Instrumentation.startMeasuringTime('SummaryPage', '_constructTable');
49             this.renderReplace(this.content().querySelector('.summary-table'), this._constructTable());
50             Instrumentation.endMeasuringTime('SummaryPage', '_constructTable');
51         }
52
53         for (var render of this._renderQueue)
54             render();
55         Instrumentation.endMeasuringTime('SummaryPage', 'render');
56     }
57
58     _createConfigurationGroup(platformIdList, metricIdList)
59     {
60         var platforms = platformIdList.map(function (id) { return Platform.findById(id); }).filter(function (obj) { return !!obj; });
61         var metrics = metricIdList.map(function (id) { return Metric.findById(id); }).filter(function (obj) { return !!obj; });
62         var configGroup = new SummaryPageConfigurationGroup(platforms, metrics, this._excludedConfigurations);
63         this._configGroups.push(configGroup);
64         return configGroup;
65     }
66
67     _constructTable()
68     {
69         var element = ComponentBase.createElement;
70
71         var self = this;
72
73         this._shouldConstructTable = false;
74         this._renderQueue = [];
75
76         return [
77             element('thead',
78                 element('tr', [
79                     element('td', {colspan: 2}),
80                     this._table.heading.map(function (group) {
81                         var nodes = [group.name];
82                         if (group.subtitle) {
83                             nodes.push(element('br'));
84                             nodes.push(element('span', {class: 'subtitle'}, group.subtitle));
85                         }
86                         return element('td', nodes);
87                     }),
88                 ])),
89             this._table.groups.map(function (rowGroup) {
90                 return element('tbody', rowGroup.rows.map(function (row, rowIndex) {
91                     var headings;
92                     headings = [element('th', {class: 'minorHeader'}, row.name)];
93                     if (!rowIndex)
94                         headings.unshift(element('th', {class: 'majorHeader', rowspan: rowGroup.rows.length}, rowGroup.name));
95                     return element('tr', [headings, row.cells.map(self._constructRatioGraph.bind(self))]);
96                 }));
97             }),
98         ];
99     }
100
101     _constructRatioGraph(configurationGroup)
102     {
103         var element = ComponentBase.createElement;
104         var link = ComponentBase.createLink;
105         var configurationList = configurationGroup.configurationList();
106         var ratioGraph = new RatioBarGraph();
107
108         if (configurationList.length == 0) {
109             this._renderQueue.push(function () { ratioGraph.render(); });
110             return element('td', ratioGraph);
111         }
112
113         var state = ChartsPage.createStateForConfigurationList(configurationList);
114         var anchor = link(ratioGraph, this.router().url('charts', state));
115         var cell = element('td', [anchor, new SpinnerIcon]);
116
117         this._renderQueue.push(this._renderCell.bind(this, cell, anchor, ratioGraph, configurationGroup));
118         return cell;
119     }
120
121     _renderCell(cell, anchor, ratioGraph, configurationGroup)
122     {
123         if (configurationGroup.isFetching())
124             cell.classList.add('fetching');
125         else
126             cell.classList.remove('fetching');
127
128         var warningText = this._warningTextForGroup(configurationGroup);
129         anchor.title = warningText || 'Open charts';
130         ratioGraph.update(configurationGroup.ratio(), configurationGroup.label(), !!warningText);
131         ratioGraph.render();
132     }
133
134     _warningTextForGroup(configurationGroup)
135     {
136         function mapAndSortByName(platforms)
137         {
138             return platforms && platforms.map(function (platform) { return platform.name(); }).sort();
139         }
140
141         function pluralizeIfNeeded(singularWord, platforms) { return singularWord + (platforms.length > 1 ? 's' : ''); }
142
143         var warnings = [];
144
145         var missingPlatforms = mapAndSortByName(configurationGroup.missingPlatforms());
146         if (missingPlatforms)
147             warnings.push(`Missing ${pluralizeIfNeeded('platform', missingPlatforms)}: ${missingPlatforms.join(', ')}`);
148
149         var platformsWithoutBaselines = mapAndSortByName(configurationGroup.platformsWithoutBaseline());
150         if (platformsWithoutBaselines)
151             warnings.push(`Need ${pluralizeIfNeeded('baseline', platformsWithoutBaselines)}: ${platformsWithoutBaselines.join(', ')}`);
152
153         return warnings.length ? warnings.join('\n') : null;
154     }
155
156     static htmlTemplate()
157     {
158         return `<section class="page-with-heading"><table class="summary-table"></table></section>`;
159     }
160
161     static cssTemplate()
162     {
163         return `
164             .summary-table {
165                 border-collapse: collapse;
166                 border: none;
167                 margin: 0;
168                 width: 100%;
169             }
170
171             .summary-table td,
172             .summary-table th {
173                 text-align: center;
174                 padding: 0px;
175             }
176
177             .summary-table .majorHeader {
178                 width: 5rem;
179             }
180
181             .summary-table .minorHeader {
182                 width: 7rem;
183             }
184
185             .summary-table .unifiedHeader {
186                 padding-left: 5rem;
187             }
188
189             .summary-table tbody tr:first-child > * {
190                 border-top: solid 1px #ddd;
191             }
192
193             .summary-table tbody tr:nth-child(even) > *:not(.majorHeader) {
194                 background: #f9f9f9;
195             }
196
197             .summary-table th,
198             .summary-table thead td {
199                 color: #333;
200                 font-weight: inherit;
201                 font-size: 1rem;
202                 padding: 0.2rem 0.4rem;
203             }
204
205             .summary-table thead td {
206                 font-size: 1.2rem;
207                 line-height: 1.3rem;
208             }
209
210             .summary-table .subtitle {
211                 display: block;
212                 font-size: 0.9rem;
213                 line-height: 1.2rem;
214                 color: #666;
215             }
216
217             .summary-table tbody td {
218                 position: relative;
219                 font-weight: inherit;
220                 font-size: 0.9rem;
221                 height: 2.5rem;
222                 padding: 0;
223             }
224
225             .summary-table td > * {
226                 height: 100%;
227             }
228
229             .summary-table td spinner-icon {
230                 display: block;
231                 position: absolute;
232                 top: 0.25rem;
233                 left: calc(50% - 1rem);
234                 z-index: 100;
235             }
236
237             .summary-table td.fetching a {
238                 display: none;
239             }
240
241             .summary-table td:not(.fetching) spinner-icon {
242                 display: none;
243             }
244         `;
245     }
246 }
247
248 class SummaryPageConfigurationGroup {
249     constructor(platforms, metrics, excludedConfigurations)
250     {
251         this._measurementSets = [];
252         this._configurationList = [];
253         this._setToRatio = new Map;
254         this._ratio = NaN;
255         this._label = null;
256         this._missingPlatforms = new Set;
257         this._platformsWithoutBaseline = new Set;
258         this._isFetching = false;
259         this._smallerIsBetter = metrics.length ? metrics[0].isSmallerBetter() : null;
260
261         for (var platform of platforms) {
262             console.assert(platform instanceof Platform);
263             var foundInSomeMetric = false;
264             for (var metric of metrics) {
265                 console.assert(metric instanceof Metric);
266                 console.assert(this._smallerIsBetter == metric.isSmallerBetter());
267                 metric.isSmallerBetter();
268
269                 if (excludedConfigurations && platform.id() in excludedConfigurations && excludedConfigurations[platform.id()].includes(+metric.id()))
270                     continue;
271                 if (!platform.hasMetric(metric))
272                     continue;
273                 foundInSomeMetric = true;
274                 this._measurementSets.push(MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric)));
275                 this._configurationList.push([platform.id(), metric.id()]);
276             }
277             if (!foundInSomeMetric)
278                 this._missingPlatforms.add(platform);
279         }
280     }
281
282     ratio() { return this._ratio; }
283     label() { return this._label; }
284     changeType() { return this._changeType; }
285     configurationList() { return this._configurationList; }
286     isFetching() { return this._isFetching; }
287     missingPlatforms() { return this._missingPlatforms.size ? Array.from(this._missingPlatforms) : null; }
288     platformsWithoutBaseline() { return this._platformsWithoutBaseline.size ? Array.from(this._platformsWithoutBaseline) : null; }
289
290     fetchAndComputeSummary(timeRange)
291     {
292         console.assert(timeRange instanceof Array);
293         console.assert(typeof(timeRange[0]) == 'number');
294         console.assert(typeof(timeRange[1]) == 'number');
295
296         var promises = [];
297         for (var set of this._measurementSets)
298             promises.push(this._fetchAndComputeRatio(set, timeRange));
299
300         var self = this;
301         var fetched = false;
302         setTimeout(function () {
303             // Don't set _isFetching to true if all promises were to resolve immediately (cached).
304             if (!fetched)
305                 self._isFetching = true;
306         }, 50);
307
308         return Promise.all(promises).then(function () {
309             fetched = true;
310             self._isFetching = false;
311             self._computeSummary();
312         });
313     }
314
315     _computeSummary()
316     {
317         var ratios = [];
318         for (var set of this._measurementSets) {
319             var ratio = this._setToRatio.get(set);
320             if (!isNaN(ratio))
321                 ratios.push(ratio);
322         }
323
324         var averageRatio = Statistics.mean(ratios);
325         if (isNaN(averageRatio))
326             return;
327
328         var currentIsSmallerThanBaseline = averageRatio < 1;
329         var changeType = this._smallerIsBetter == currentIsSmallerThanBaseline ? 'better' : 'worse';
330         averageRatio = Math.abs(averageRatio - 1);
331
332         this._ratio = averageRatio * (changeType == 'better' ? 1 : -1);
333         this._label = (averageRatio * 100).toFixed(1) + '%';
334         this._changeType = changeType;
335     }
336
337     _fetchAndComputeRatio(set, timeRange)
338     {
339         var setToRatio = this._setToRatio;
340         var self = this;
341         return set.fetchBetween(timeRange[0], timeRange[1]).then(function () {
342             var baselineTimeSeries = set.fetchedTimeSeries('baseline', false, false);
343             var currentTimeSeries = set.fetchedTimeSeries('current', false, false);
344
345             var baselineMedian = SummaryPageConfigurationGroup._medianForTimeRange(baselineTimeSeries, timeRange);
346             var currentMedian = SummaryPageConfigurationGroup._medianForTimeRange(currentTimeSeries, timeRange);
347             var platform = Platform.findById(set.platformId());
348             if (!currentMedian)
349                 self._missingPlatforms.add(platform);
350             else if (!baselineMedian)
351                 self._platformsWithoutBaseline.add(platform);
352
353             setToRatio.set(set, currentMedian / baselineMedian);
354         }).catch(function () {
355             setToRatio.set(set, NaN);
356         });
357     }
358
359     static _medianForTimeRange(timeSeries, timeRange)
360     {
361         if (!timeSeries.firstPoint())
362             return NaN;
363
364         var startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();
365         var afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();
366         var endPoint = timeSeries.previousPoint(afterEndPoint);
367         if (!endPoint || startPoint == afterEndPoint)
368             endPoint = afterEndPoint;
369
370         var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
371         return Statistics.median(points);
372     }
373 }