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