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