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 = [];
14 this._configGroups = [];
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: []};
22 for (var platformGroup of summarySettings.platformGroups)
23 row.cells.push(this._createConfigurationGroup(platformGroup.platforms, subMetricGroup.metrics));
28 routeName() { return 'summary'; }
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));
44 if (this._shouldConstructTable)
45 this.renderReplace(this.content().querySelector('.summary-table'), this._constructTable());
47 for (var render of this._renderQueue)
51 _createConfigurationGroup(platformIdList, metricIdList)
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);
62 var element = ComponentBase.createElement;
66 this._shouldConstructTable = false;
67 this._renderQueue = [];
72 element('td', {colspan: 2}),
73 this._table.heading.map(function (label) { return element('td', label); }),
75 this._table.groups.map(function (rowGroup) {
76 return rowGroup.rows.map(function (row, rowIndex) {
78 if (rowGroup.rows.length == 1)
79 headings = [element('th', {class: 'unifiedHeader', colspan: 2}, row.name)];
81 headings = [element('th', {class: 'minorHeader'}, row.name)];
83 headings.unshift(element('th', {class: 'majorHeader', rowspan: rowGroup.rows.length}, rowGroup.name));
85 return element('tr', [headings, row.cells.map(self._constructRatioGraph.bind(self))]);
91 _constructRatioGraph(configurationGroup)
93 var element = ComponentBase.createElement;
94 var link = ComponentBase.createLink;
96 var ratioGraph = new RatioBarGraph();
98 this._renderQueue.push(function () {
99 ratioGraph.update(configurationGroup.ratio(), configurationGroup.label());
103 var state = ChartsPage.createStateForConfigurationList(configurationGroup.configurationList());
104 return element('td', link(ratioGraph, 'Open charts', this.router().url('charts', state)));
107 static htmlTemplate()
109 return `<section class="page-with-heading"><table class="summary-table"></table></section>`;
116 border-collapse: collapse;
119 width: calc(100% - 2rem - 2px);
128 .summary-table .majorHeader {
132 .summary-table .minorHeader {
136 .summary-table .unifiedHeader {
140 .summary-table > tr:nth-child(even) > *:not(.majorHeader) {
145 .summary-table thead td {
147 font-weight: inherit;
149 padding: 0.2rem 0.4rem;
152 .summary-table thead td {
156 .summary-table tbody td {
157 font-weight: inherit;
162 .summary-table td > * {
169 class SummaryPageConfigurationGroup {
170 constructor(platforms, metrics)
172 this._measurementSets = [];
173 this._configurationList = [];
174 this._setToRatio = new Map;
177 this._changeType = null;
178 this._smallerIsBetter = metrics.length ? metrics[0].isSmallerBetter() : null;
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()]);
194 ratio() { return this._ratio; }
195 label() { return this._label; }
196 changeType() { return this._changeType; }
197 configurationList() { return this._configurationList; }
199 fetchAndComputeSummary(timeRange)
201 console.assert(timeRange instanceof Array);
202 console.assert(typeof(timeRange[0]) == 'number');
203 console.assert(typeof(timeRange[1]) == 'number');
206 for (var set of this._measurementSets)
207 promises.push(this._fetchAndComputeRatio(set, timeRange));
209 return Promise.all(promises).then(this._computeSummary.bind(this));
215 for (var set of this._measurementSets) {
216 var ratio = this._setToRatio.get(set);
221 var averageRatio = Statistics.mean(ratios);
222 if (isNaN(averageRatio)) {
224 this._changeType = null;
228 if (Math.abs(averageRatio - 1) < 0.001) { // Less than 0.1% difference.
229 this._summary = 'No change';
230 this._changeType = null;
234 var currentIsSmallerThanBaseline = averageRatio < 1;
235 var changeType = this._smallerIsBetter == currentIsSmallerThanBaseline ? 'better' : 'worse';
236 if (currentIsSmallerThanBaseline)
237 averageRatio = 1 / averageRatio;
239 this._ratio = (averageRatio - 1) * (changeType == 'better' ? 1 : -1);
240 this._label = ((averageRatio - 1) * 100).toFixed(1) + '%';
241 this._changeType = changeType;
244 _fetchAndComputeRatio(set, timeRange)
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);
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);
259 static _medianForTimeRange(timeSeries, timeRange)
261 if (!timeSeries.firstPoint())
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;
270 var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
271 return Statistics.median(points);
274 static _fetchData(set, timeRange)
276 // FIXME: Make fetchBetween return a promise.
278 return new Promise(function (resolve, reject) {
279 set.fetchBetween(timeRange[0], timeRange[1], function (error) {
284 else if (set.hasFetchedRange(timeRange[0], timeRange[1]))