Adopt custom elements API in perf dashboard
[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.updateRendering(); });
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(() => { ratioGraph.updateRendering(); });
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 spinner = new SpinnerIcon;
116         var cell = element('td', [anchor, spinner]);
117
118         this._renderQueue.push(this._renderCell.bind(this, cell, spinner, anchor, ratioGraph, configurationGroup));
119         return cell;
120     }
121
122     _renderCell(cell, spinner, anchor, ratioGraph, configurationGroup)
123     {
124         spinner.updateRendering();
125
126         if (configurationGroup.isFetching())
127             cell.classList.add('fetching');
128         else
129             cell.classList.remove('fetching');
130
131         var warningText = this._warningTextForGroup(configurationGroup);
132         anchor.title = warningText || 'Open charts';
133         ratioGraph.update(configurationGroup.ratio(), configurationGroup.label(), !!warningText);
134         ratioGraph.updateRendering();
135     }
136
137     _warningTextForGroup(configurationGroup)
138     {
139         function mapAndSortByName(platforms)
140         {
141             return platforms && platforms.map(function (platform) { return platform.name(); }).sort();
142         }
143
144         function pluralizeIfNeeded(singularWord, platforms) { return singularWord + (platforms.length > 1 ? 's' : ''); }
145
146         var warnings = [];
147
148         var missingPlatforms = mapAndSortByName(configurationGroup.missingPlatforms());
149         if (missingPlatforms)
150             warnings.push(`Missing ${pluralizeIfNeeded('platform', missingPlatforms)}: ${missingPlatforms.join(', ')}`);
151
152         var platformsWithoutBaselines = mapAndSortByName(configurationGroup.platformsWithoutBaseline());
153         if (platformsWithoutBaselines)
154             warnings.push(`Need ${pluralizeIfNeeded('baseline', platformsWithoutBaselines)}: ${platformsWithoutBaselines.join(', ')}`);
155
156         return warnings.length ? warnings.join('\n') : null;
157     }
158
159     static htmlTemplate()
160     {
161         return `<section class="page-with-heading"><table class="summary-table"></table></section>`;
162     }
163
164     static cssTemplate()
165     {
166         return `
167             .summary-table {
168                 border-collapse: collapse;
169                 border: none;
170                 margin: 0;
171                 width: 100%;
172             }
173
174             .summary-table td,
175             .summary-table th {
176                 text-align: center;
177                 padding: 0px;
178             }
179
180             .summary-table .majorHeader {
181                 width: 5rem;
182             }
183
184             .summary-table .minorHeader {
185                 width: 7rem;
186             }
187
188             .summary-table .unifiedHeader {
189                 padding-left: 5rem;
190             }
191
192             .summary-table tbody tr:first-child > * {
193                 border-top: solid 1px #ddd;
194             }
195
196             .summary-table tbody tr:nth-child(even) > *:not(.majorHeader) {
197                 background: #f9f9f9;
198             }
199
200             .summary-table th,
201             .summary-table thead td {
202                 color: #333;
203                 font-weight: inherit;
204                 font-size: 1rem;
205                 padding: 0.2rem 0.4rem;
206             }
207
208             .summary-table thead td {
209                 font-size: 1.2rem;
210                 line-height: 1.3rem;
211             }
212
213             .summary-table .subtitle {
214                 display: block;
215                 font-size: 0.9rem;
216                 line-height: 1.2rem;
217                 color: #666;
218             }
219
220             .summary-table tbody td {
221                 position: relative;
222                 font-weight: inherit;
223                 font-size: 0.9rem;
224                 height: 2.5rem;
225                 padding: 0;
226             }
227
228             .summary-table td > * {
229                 height: 100%;
230             }
231
232             .summary-table td spinner-icon {
233                 display: block;
234                 position: absolute;
235                 top: 0.25rem;
236                 left: calc(50% - 1rem);
237                 z-index: 100;
238             }
239
240             .summary-table td.fetching a {
241                 display: none;
242             }
243
244             .summary-table td:not(.fetching) spinner-icon {
245                 display: none;
246             }
247         `;
248     }
249 }
250
251 class SummaryPageConfigurationGroup {
252     constructor(platforms, metrics, excludedConfigurations)
253     {
254         this._measurementSets = [];
255         this._configurationList = [];
256         this._setToRatio = new Map;
257         this._ratio = NaN;
258         this._label = null;
259         this._missingPlatforms = new Set;
260         this._platformsWithoutBaseline = new Set;
261         this._isFetching = false;
262         this._smallerIsBetter = metrics.length ? metrics[0].isSmallerBetter() : null;
263
264         for (var platform of platforms) {
265             console.assert(platform instanceof Platform);
266             var foundInSomeMetric = false;
267             for (var metric of metrics) {
268                 console.assert(metric instanceof Metric);
269                 console.assert(this._smallerIsBetter == metric.isSmallerBetter());
270                 metric.isSmallerBetter();
271
272                 if (excludedConfigurations && platform.id() in excludedConfigurations && excludedConfigurations[platform.id()].includes(+metric.id()))
273                     continue;
274                 if (!platform.hasMetric(metric))
275                     continue;
276                 foundInSomeMetric = true;
277                 this._measurementSets.push(MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric)));
278                 this._configurationList.push([platform.id(), metric.id()]);
279             }
280             if (!foundInSomeMetric)
281                 this._missingPlatforms.add(platform);
282         }
283     }
284
285     ratio() { return this._ratio; }
286     label() { return this._label; }
287     changeType() { return this._changeType; }
288     configurationList() { return this._configurationList; }
289     isFetching() { return this._isFetching; }
290     missingPlatforms() { return this._missingPlatforms.size ? Array.from(this._missingPlatforms) : null; }
291     platformsWithoutBaseline() { return this._platformsWithoutBaseline.size ? Array.from(this._platformsWithoutBaseline) : null; }
292
293     fetchAndComputeSummary(timeRange)
294     {
295         console.assert(timeRange instanceof Array);
296         console.assert(typeof(timeRange[0]) == 'number');
297         console.assert(typeof(timeRange[1]) == 'number');
298
299         var promises = [];
300         for (var set of this._measurementSets)
301             promises.push(this._fetchAndComputeRatio(set, timeRange));
302
303         var self = this;
304         var fetched = false;
305         setTimeout(function () {
306             // Don't set _isFetching to true if all promises were to resolve immediately (cached).
307             if (!fetched)
308                 self._isFetching = true;
309         }, 50);
310
311         return Promise.all(promises).then(function () {
312             fetched = true;
313             self._isFetching = false;
314             self._computeSummary();
315         });
316     }
317
318     _computeSummary()
319     {
320         var ratios = [];
321         for (var set of this._measurementSets) {
322             var ratio = this._setToRatio.get(set);
323             if (!isNaN(ratio))
324                 ratios.push(ratio);
325         }
326
327         var averageRatio = Statistics.mean(ratios);
328         if (isNaN(averageRatio))
329             return;
330
331         var currentIsSmallerThanBaseline = averageRatio < 1;
332         var changeType = this._smallerIsBetter == currentIsSmallerThanBaseline ? 'better' : 'worse';
333         averageRatio = Math.abs(averageRatio - 1);
334
335         this._ratio = averageRatio * (changeType == 'better' ? 1 : -1);
336         this._label = (averageRatio * 100).toFixed(1) + '%';
337         this._changeType = changeType;
338     }
339
340     _fetchAndComputeRatio(set, timeRange)
341     {
342         var setToRatio = this._setToRatio;
343         var self = this;
344         return set.fetchBetween(timeRange[0], timeRange[1]).then(function () {
345             var baselineTimeSeries = set.fetchedTimeSeries('baseline', false, false);
346             var currentTimeSeries = set.fetchedTimeSeries('current', false, false);
347
348             var baselineMedian = SummaryPageConfigurationGroup._medianForTimeRange(baselineTimeSeries, timeRange);
349             var currentMedian = SummaryPageConfigurationGroup._medianForTimeRange(currentTimeSeries, timeRange);
350             var platform = Platform.findById(set.platformId());
351             if (!currentMedian)
352                 self._missingPlatforms.add(platform);
353             else if (!baselineMedian)
354                 self._platformsWithoutBaseline.add(platform);
355
356             setToRatio.set(set, currentMedian / baselineMedian);
357         }).catch(function () {
358             setToRatio.set(set, NaN);
359         });
360     }
361
362     static _medianForTimeRange(timeSeries, timeRange)
363     {
364         if (!timeSeries.firstPoint())
365             return NaN;
366
367         var startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();
368         var afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();
369         var endPoint = timeSeries.previousPoint(afterEndPoint);
370         if (!endPoint || startPoint == afterEndPoint)
371             endPoint = afterEndPoint;
372
373         var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
374         return Statistics.median(points);
375     }
376 }