Summary page should show warnings when current or baseline data is missing.
[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 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 1rem;
128                 width: calc(100% - 2rem - 2px);
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 > tr:nth-child(even) > *:not(.majorHeader) {
150                 background: #f9f9f9;
151             }
152
153             .summary-table th,
154             .summary-table thead td {
155                 color: #333;
156                 font-weight: inherit;
157                 font-size: 1rem;
158                 padding: 0.2rem 0.4rem;
159             }
160
161             .summary-table thead td {
162                 font-size: 1.2rem;
163             }
164
165             .summary-table tbody td {
166                 font-weight: inherit;
167                 font-size: 0.9rem;
168                 padding: 0;
169             }
170
171             .summary-table td > * {
172                 height: 100%;
173             }
174         `;
175     }
176 }
177
178 class SummaryPageConfigurationGroup {
179     constructor(platforms, metrics, excludedConfigurations)
180     {
181         this._measurementSets = [];
182         this._configurationList = [];
183         this._setToRatio = new Map;
184         this._ratio = null;
185         this._label = null;
186         this._warnings = {};
187         this._smallerIsBetter = metrics.length ? metrics[0].isSmallerBetter() : null;
188
189         for (var platform of platforms) {
190             console.assert(platform instanceof Platform);
191             for (var metric of metrics) {
192                 console.assert(metric instanceof Metric);
193                 console.assert(this._smallerIsBetter == metric.isSmallerBetter());
194                 metric.isSmallerBetter();
195
196                 if (excludedConfigurations && platform.id() in excludedConfigurations && excludedConfigurations[platform.id()].includes(+metric.id()))
197                     continue;
198                 if (platform.hasMetric(metric)) {
199                     this._measurementSets.push(MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric)));
200                     this._configurationList.push([platform.id(), metric.id()]);
201                 }
202             }
203         }
204     }
205
206     ratio() { return this._ratio; }
207     label() { return this._label; }
208     warnings() { return this._warnings; }
209     changeType() { return this._changeType; }
210     configurationList() { return this._configurationList; }
211
212     fetchAndComputeSummary(timeRange)
213     {
214         console.assert(timeRange instanceof Array);
215         console.assert(typeof(timeRange[0]) == 'number');
216         console.assert(typeof(timeRange[1]) == 'number');
217
218         var promises = [];
219         for (var set of this._measurementSets)
220             promises.push(this._fetchAndComputeRatio(set, timeRange));
221
222         return Promise.all(promises).then(this._computeSummary.bind(this));
223     }
224
225     _computeSummary()
226     {
227         var ratios = [];
228         for (var set of this._measurementSets) {
229             var ratio = this._setToRatio.get(set);
230             if (!isNaN(ratio))
231                 ratios.push(ratio);
232         }
233
234         var averageRatio = Statistics.mean(ratios);
235         if (isNaN(averageRatio))
236             return;
237
238         var currentIsSmallerThanBaseline = averageRatio < 1;
239         var changeType = this._smallerIsBetter == currentIsSmallerThanBaseline ? 'better' : 'worse';
240         averageRatio = Math.abs(averageRatio - 1);
241
242         this._ratio = averageRatio * (changeType == 'better' ? 1 : -1);
243         this._label = (averageRatio * 100).toFixed(1) + '%';
244         this._changeType = changeType;
245     }
246
247     _fetchAndComputeRatio(set, timeRange)
248     {
249         var setToRatio = this._setToRatio;
250         var self = this;
251         return set.fetchBetween(timeRange[0], timeRange[1]).then(function () {
252             var baselineTimeSeries = set.fetchedTimeSeries('baseline', false, false);
253             var currentTimeSeries = set.fetchedTimeSeries('current', false, false);
254
255             var baselineMedian = SummaryPageConfigurationGroup._medianForTimeRange(baselineTimeSeries, timeRange);
256             var currentMedian = SummaryPageConfigurationGroup._medianForTimeRange(currentTimeSeries, timeRange);
257             var platform = Platform.findById(set.platformId());
258             if (!baselineMedian) {
259                 if(!('baseline' in self._warnings))
260                     self._warnings['baseline'] = new Set;
261                 self._warnings['baseline'].add(platform);
262             }
263             if (!currentMedian) {
264                 if(!('current' in self._warnings))
265                     self._warnings['current'] = new Set;
266                 self._warnings['current'].add(platform);
267             }
268
269             setToRatio.set(set, currentMedian / baselineMedian);
270         }).catch(function () {
271             setToRatio.set(set, NaN);
272         });
273     }
274
275     static _medianForTimeRange(timeSeries, timeRange)
276     {
277         if (!timeSeries.firstPoint())
278             return NaN;
279
280         var startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();
281         var afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();
282         var endPoint = timeSeries.previousPoint(afterEndPoint);
283         if (!endPoint || startPoint == afterEndPoint)
284             endPoint = afterEndPoint;
285
286         var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
287         return Statistics.median(points);
288     }
289 }