2 class SummaryPage extends PageWithHeading {
4 constructor(summarySettings)
6 super('Summary', null);
9 heading: summarySettings.platformGroups.map(function (platformGroup) { return platformGroup.name; }),
12 this._shouldConstructTable = true;
13 this._renderQueue = [];
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: []};
23 for (var platformGroup of summarySettings.platformGroups)
24 row.cells.push(this._createConfigurationGroupAndStartFetchingData(platformGroup.platforms, subMetricGroup.metrics, timeRange));
29 routeName() { return 'summary'; }
40 if (this._shouldConstructTable)
41 this.renderReplace(this.content().querySelector('.summary-table'), this._constructTable());
43 for (var render of this._renderQueue)
47 _createConfigurationGroupAndStartFetchingData(platformIdList, metricIdList, timeRange)
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));
58 var element = ComponentBase.createElement;
62 this._shouldConstructTable = false;
63 this._renderQueue = [];
68 element('td', {colspan: 2}),
69 this._table.heading.map(function (label) { return element('td', label); }),
71 this._table.groups.map(function (rowGroup) {
72 return rowGroup.rows.map(function (row, rowIndex) {
74 if (rowGroup.rows.length == 1)
75 headings = [element('th', {class: 'unifiedHeader', colspan: 2}, row.name)];
77 headings = [element('th', {class: 'minorHeader'}, row.name)];
79 headings.unshift(element('th', {class: 'majorHeader', rowspan: rowGroup.rows.length}, rowGroup.name));
81 return element('tr', [headings, row.cells.map(self._constructRatioGraph.bind(self))]);
87 _constructRatioGraph(configurationGroup)
89 var element = ComponentBase.createElement;
90 var link = ComponentBase.createLink;
92 var ratioGraph = new RatioBarGraph();
94 this._renderQueue.push(function () {
95 ratioGraph.update(configurationGroup.ratio(), configurationGroup.label());
99 var state = ChartsPage.createStateForConfigurationList(configurationGroup.configurationList());
100 return element('td', link(ratioGraph, 'Open charts', this.router().url('charts', state)));
103 static htmlTemplate()
105 return `<section class="page-with-heading"><table class="summary-table"></table></section>`;
112 border-collapse: collapse;
115 width: calc(100% - 2rem - 2px);
124 .summary-table .majorHeader {
128 .summary-table .minorHeader {
132 .summary-table .unifiedHeader {
136 .summary-table > tr:nth-child(even) > *:not(.majorHeader) {
141 .summary-table thead td {
143 font-weight: inherit;
145 padding: 0.2rem 0.4rem;
148 .summary-table thead td {
152 .summary-table tbody td {
153 font-weight: inherit;
158 .summary-table td > * {
165 class SummaryPageConfigurationGroup {
166 constructor(platforms, metrics)
168 this._measurementSets = [];
169 this._configurationList = [];
170 this._setToRatio = new Map;
173 this._changeType = null;
174 this._smallerIsBetter = metrics.length ? metrics[0].isSmallerBetter() : null;
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()]);
190 ratio() { return this._ratio; }
191 label() { return this._label; }
192 changeType() { return this._changeType; }
193 configurationList() { return this._configurationList; }
195 fetchAndComputeSummary(timeRange)
197 console.assert(timeRange instanceof Array);
198 console.assert(typeof(timeRange[0]) == 'number');
199 console.assert(typeof(timeRange[1]) == 'number');
202 for (var set of this._measurementSets)
203 promises.push(this._fetchAndComputeRatio(set, timeRange));
205 return Promise.all(promises).then(this._computeSummary.bind(this));
211 for (var set of this._measurementSets) {
212 var ratio = this._setToRatio.get(set);
217 var averageRatio = Statistics.mean(ratios);
218 if (isNaN(averageRatio)) {
220 this._changeType = null;
224 if (Math.abs(averageRatio - 1) < 0.001) { // Less than 0.1% difference.
225 this._summary = 'No change';
226 this._changeType = null;
230 var currentIsSmallerThanBaseline = averageRatio < 1;
231 var changeType = this._smallerIsBetter == currentIsSmallerThanBaseline ? 'better' : 'worse';
232 if (currentIsSmallerThanBaseline)
233 averageRatio = 1 / averageRatio;
235 this._ratio = (averageRatio - 1) * (changeType == 'better' ? 1 : -1);
236 this._label = ((averageRatio - 1) * 100).toFixed(1) + '%';
237 this._changeType = changeType;
240 _fetchAndComputeRatio(set, timeRange)
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);
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);
255 static _medianForTimeRange(timeSeries, timeRange)
257 if (!timeSeries.firstPoint())
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;
266 var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
267 return Statistics.median(points);
270 static _fetchData(set, timeRange)
272 // FIXME: Make fetchBetween return a promise.
274 return new Promise(function (resolve, reject) {
275 set.fetchBetween(timeRange[0], timeRange[1], function (error) {
280 else if (set.hasFetchedRange(timeRange[0], timeRange[1]))