ffd844d723539aad4944bf6fcc0136c2a95eb5c0
[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         var anchor = link(ratioGraph, this.router().url('charts', state));
98         this._renderQueue.push(function () {
99             var warnings = configurationGroup.warnings();
100             var warningText = '';
101             for (var type in warnings) {
102                 var platformString = Array.from(warnings[type]).map(function (platform) { return platform.name(); }).join(', ');
103                 warningText += `Missing ${type} for following platform(s): ${platformString}`;
104             }
105
106             anchor.title = warningText || 'Open charts';
107             ratioGraph.update(configurationGroup.ratio(), configurationGroup.label(), !!warningText);
108             ratioGraph.render();
109         });
110         if (configurationList.length == 0)
111             return element('td', ratioGraph);
112
113         return element('td', anchor);
114     }
115
116     static htmlTemplate()
117     {
118         return `<section class="page-with-heading"><table class="summary-table"></table></section>`;
119     }
120
121     static cssTemplate()
122     {
123         return `
124             .summary-table {
125                 border-collapse: collapse;
126                 border: none;
127                 margin: 0;
128                 width: 100%;
129             }
130
131             .summary-table td,
132             .summary-table th {
133                 text-align: center;
134                 padding: 0px;
135             }
136
137             .summary-table .majorHeader {
138                 width: 5rem;
139             }
140
141             .summary-table .minorHeader {
142                 width: 7rem;
143             }
144
145             .summary-table .unifiedHeader {
146                 padding-left: 5rem;
147             }
148
149             .summary-table tbody tr:first-child > * {
150                 border-top: solid 1px #ddd;
151             }
152
153             .summary-table tbody tr:nth-child(even) > *:not(.majorHeader) {
154                 background: #f9f9f9;
155             }
156
157             .summary-table th,
158             .summary-table thead td {
159                 color: #333;
160                 font-weight: inherit;
161                 font-size: 1rem;
162                 padding: 0.2rem 0.4rem;
163             }
164
165             .summary-table thead td {
166                 font-size: 1.2rem;
167             }
168
169             .summary-table tbody td {
170                 font-weight: inherit;
171                 font-size: 0.9rem;
172                 padding: 0;
173             }
174
175             .summary-table td > * {
176                 height: 100%;
177             }
178         `;
179     }
180 }
181
182 class SummaryPageConfigurationGroup {
183     constructor(platforms, metrics, excludedConfigurations)
184     {
185         this._measurementSets = [];
186         this._configurationList = [];
187         this._setToRatio = new Map;
188         this._ratio = null;
189         this._label = null;
190         this._warnings = {};
191         this._smallerIsBetter = metrics.length ? metrics[0].isSmallerBetter() : null;
192
193         for (var platform of platforms) {
194             console.assert(platform instanceof Platform);
195             for (var metric of metrics) {
196                 console.assert(metric instanceof Metric);
197                 console.assert(this._smallerIsBetter == metric.isSmallerBetter());
198                 metric.isSmallerBetter();
199
200                 if (excludedConfigurations && platform.id() in excludedConfigurations && excludedConfigurations[platform.id()].includes(+metric.id()))
201                     continue;
202                 if (platform.hasMetric(metric)) {
203                     this._measurementSets.push(MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric)));
204                     this._configurationList.push([platform.id(), metric.id()]);
205                 }
206             }
207         }
208     }
209
210     ratio() { return this._ratio; }
211     label() { return this._label; }
212     warnings() { return this._warnings; }
213     changeType() { return this._changeType; }
214     configurationList() { return this._configurationList; }
215
216     fetchAndComputeSummary(timeRange)
217     {
218         console.assert(timeRange instanceof Array);
219         console.assert(typeof(timeRange[0]) == 'number');
220         console.assert(typeof(timeRange[1]) == 'number');
221
222         var promises = [];
223         for (var set of this._measurementSets)
224             promises.push(this._fetchAndComputeRatio(set, timeRange));
225
226         return Promise.all(promises).then(this._computeSummary.bind(this));
227     }
228
229     _computeSummary()
230     {
231         var ratios = [];
232         for (var set of this._measurementSets) {
233             var ratio = this._setToRatio.get(set);
234             if (!isNaN(ratio))
235                 ratios.push(ratio);
236         }
237
238         var averageRatio = Statistics.mean(ratios);
239         if (isNaN(averageRatio))
240             return;
241
242         var currentIsSmallerThanBaseline = averageRatio < 1;
243         var changeType = this._smallerIsBetter == currentIsSmallerThanBaseline ? 'better' : 'worse';
244         averageRatio = Math.abs(averageRatio - 1);
245
246         this._ratio = averageRatio * (changeType == 'better' ? 1 : -1);
247         this._label = (averageRatio * 100).toFixed(1) + '%';
248         this._changeType = changeType;
249     }
250
251     _fetchAndComputeRatio(set, timeRange)
252     {
253         var setToRatio = this._setToRatio;
254         var self = this;
255         return set.fetchBetween(timeRange[0], timeRange[1]).then(function () {
256             var baselineTimeSeries = set.fetchedTimeSeries('baseline', false, false);
257             var currentTimeSeries = set.fetchedTimeSeries('current', false, false);
258
259             var baselineMedian = SummaryPageConfigurationGroup._medianForTimeRange(baselineTimeSeries, timeRange);
260             var currentMedian = SummaryPageConfigurationGroup._medianForTimeRange(currentTimeSeries, timeRange);
261             var platform = Platform.findById(set.platformId());
262             if (!baselineMedian) {
263                 if(!('baseline' in self._warnings))
264                     self._warnings['baseline'] = new Set;
265                 self._warnings['baseline'].add(platform);
266             }
267             if (!currentMedian) {
268                 if(!('current' in self._warnings))
269                     self._warnings['current'] = new Set;
270                 self._warnings['current'].add(platform);
271             }
272
273             setToRatio.set(set, currentMedian / baselineMedian);
274         }).catch(function () {
275             setToRatio.set(set, NaN);
276         });
277     }
278
279     static _medianForTimeRange(timeSeries, timeRange)
280     {
281         if (!timeSeries.firstPoint())
282             return NaN;
283
284         var startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();
285         var afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();
286         var endPoint = timeSeries.previousPoint(afterEndPoint);
287         if (!endPoint || startPoint == afterEndPoint)
288             endPoint = afterEndPoint;
289
290         var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
291         return Statistics.median(points);
292     }
293 }