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