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