| |
| class SummaryPage extends PageWithHeading { |
| |
| constructor(summarySettings) |
| { |
| super(summarySettings.name, null); |
| |
| this._route = summarySettings.route; |
| this._table = { |
| heading: summarySettings.platformGroups, |
| groups: [], |
| }; |
| this._shouldConstructTable = true; |
| this._renderQueue = []; |
| this._configGroups = []; |
| this._excludedConfigurations = summarySettings.excludedConfigurations; |
| |
| for (var metricGroup of summarySettings.metricGroups) { |
| var group = {name: metricGroup.name, rows: []}; |
| this._table.groups.push(group); |
| for (var subMetricGroup of metricGroup.subgroups) { |
| var row = {name: subMetricGroup.name, cells: []}; |
| group.rows.push(row); |
| for (var platformGroup of summarySettings.platformGroups) |
| row.cells.push(this._createConfigurationGroup(platformGroup.platforms, subMetricGroup.metrics)); |
| } |
| } |
| } |
| |
| routeName() { return `summary/${this._route}`; } |
| |
| open(state) |
| { |
| super.open(state); |
| |
| var current = Date.now(); |
| var timeRange = [current - 24 * 3600 * 1000, current]; |
| for (var group of this._configGroups) |
| group.fetchAndComputeSummary(timeRange).then(() => { this.enqueueToRender(); }); |
| } |
| |
| render() |
| { |
| Instrumentation.startMeasuringTime('SummaryPage', 'render'); |
| super.render(); |
| |
| if (this._shouldConstructTable) { |
| Instrumentation.startMeasuringTime('SummaryPage', '_constructTable'); |
| this.renderReplace(this.content().querySelector('.summary-table'), this._constructTable()); |
| Instrumentation.endMeasuringTime('SummaryPage', '_constructTable'); |
| } |
| |
| for (var render of this._renderQueue) |
| render(); |
| Instrumentation.endMeasuringTime('SummaryPage', 'render'); |
| } |
| |
| _createConfigurationGroup(platformIdList, metricIdList) |
| { |
| var platforms = platformIdList.map(function (id) { return Platform.findById(id); }).filter(function (obj) { return !!obj; }); |
| var metrics = metricIdList.map(function (id) { return Metric.findById(id); }).filter(function (obj) { return !!obj; }); |
| var configGroup = new SummaryPageConfigurationGroup(platforms, metrics, this._excludedConfigurations); |
| this._configGroups.push(configGroup); |
| return configGroup; |
| } |
| |
| _constructTable() |
| { |
| var element = ComponentBase.createElement; |
| |
| var self = this; |
| |
| this._shouldConstructTable = false; |
| this._renderQueue = []; |
| |
| return [ |
| element('thead', |
| element('tr', [ |
| element('td', {colspan: 2}), |
| this._table.heading.map(function (group) { |
| var nodes = [group.name]; |
| if (group.subtitle) { |
| nodes.push(element('br')); |
| nodes.push(element('span', {class: 'subtitle'}, group.subtitle)); |
| } |
| return element('td', nodes); |
| }), |
| ])), |
| this._table.groups.map(function (rowGroup) { |
| return element('tbody', rowGroup.rows.map(function (row, rowIndex) { |
| var headings; |
| headings = [element('th', {class: 'minorHeader'}, row.name)]; |
| if (!rowIndex) |
| headings.unshift(element('th', {class: 'majorHeader', rowspan: rowGroup.rows.length}, rowGroup.name)); |
| return element('tr', [headings, row.cells.map(self._constructRatioGraph.bind(self))]); |
| })); |
| }), |
| ]; |
| } |
| |
| _constructRatioGraph(configurationGroup) |
| { |
| var element = ComponentBase.createElement; |
| var link = ComponentBase.createLink; |
| var configurationList = configurationGroup.configurationList(); |
| var ratioGraph = new RatioBarGraph(); |
| |
| if (configurationList.length == 0) { |
| this._renderQueue.push(() => { ratioGraph.enqueueToRender(); }); |
| return element('td', ratioGraph); |
| } |
| |
| var state = ChartsPage.createStateForConfigurationList(configurationList); |
| var anchor = link(ratioGraph, this.router().url('charts', state)); |
| var spinner = new SpinnerIcon; |
| var cell = element('td', [anchor, spinner]); |
| |
| this._renderQueue.push(this._renderCell.bind(this, cell, spinner, anchor, ratioGraph, configurationGroup)); |
| return cell; |
| } |
| |
| _renderCell(cell, spinner, anchor, ratioGraph, configurationGroup) |
| { |
| if (configurationGroup.isFetching()) |
| cell.classList.add('fetching'); |
| else |
| cell.classList.remove('fetching'); |
| |
| var warningText = this._warningTextForGroup(configurationGroup); |
| anchor.title = warningText || 'Open charts'; |
| ratioGraph.update(configurationGroup.ratio(), configurationGroup.label(), !!warningText); |
| ratioGraph.enqueueToRender(); |
| } |
| |
| _warningTextForGroup(configurationGroup) |
| { |
| function mapAndSortByName(platforms) |
| { |
| return platforms && platforms.map(function (platform) { return platform.name(); }).sort(); |
| } |
| |
| function pluralizeIfNeeded(singularWord, platforms) { return singularWord + (platforms.length > 1 ? 's' : ''); } |
| |
| var warnings = []; |
| |
| var missingPlatforms = mapAndSortByName(configurationGroup.missingPlatforms()); |
| if (missingPlatforms) |
| warnings.push(`Missing ${pluralizeIfNeeded('platform', missingPlatforms)}: ${missingPlatforms.join(', ')}`); |
| |
| var platformsWithoutBaselines = mapAndSortByName(configurationGroup.platformsWithoutBaseline()); |
| if (platformsWithoutBaselines) |
| warnings.push(`Need ${pluralizeIfNeeded('baseline', platformsWithoutBaselines)}: ${platformsWithoutBaselines.join(', ')}`); |
| |
| return warnings.length ? warnings.join('\n') : null; |
| } |
| |
| static htmlTemplate() |
| { |
| return `<section class="page-with-heading"><table class="summary-table"></table></section>`; |
| } |
| |
| static cssTemplate() |
| { |
| return ` |
| .summary-table { |
| border-collapse: collapse; |
| border: none; |
| margin: 0; |
| width: 100%; |
| } |
| |
| .summary-table td, |
| .summary-table th { |
| text-align: center; |
| padding: 0px; |
| } |
| |
| .summary-table .majorHeader { |
| width: 5rem; |
| } |
| |
| .summary-table .minorHeader { |
| width: 7rem; |
| } |
| |
| .summary-table .unifiedHeader { |
| padding-left: 5rem; |
| } |
| |
| .summary-table tbody tr:first-child > * { |
| border-top: solid 1px #ddd; |
| } |
| |
| .summary-table tbody tr:nth-child(even) > *:not(.majorHeader) { |
| background: #f9f9f9; |
| } |
| |
| .summary-table th, |
| .summary-table thead td { |
| color: #333; |
| font-weight: inherit; |
| font-size: 1rem; |
| padding: 0.2rem 0.4rem; |
| } |
| |
| .summary-table thead td { |
| font-size: 1.2rem; |
| line-height: 1.3rem; |
| } |
| |
| .summary-table .subtitle { |
| display: block; |
| font-size: 0.9rem; |
| line-height: 1.2rem; |
| color: #666; |
| } |
| |
| .summary-table tbody td { |
| position: relative; |
| font-weight: inherit; |
| font-size: 0.9rem; |
| height: 2.5rem; |
| padding: 0; |
| } |
| |
| .summary-table td > * { |
| height: 100%; |
| } |
| |
| .summary-table td spinner-icon { |
| display: block; |
| position: absolute; |
| top: 0.25rem; |
| left: calc(50% - 1rem); |
| z-index: 100; |
| } |
| |
| .summary-table td.fetching a { |
| display: none; |
| } |
| |
| .summary-table td:not(.fetching) spinner-icon { |
| display: none; |
| } |
| `; |
| } |
| } |
| |
| class SummaryPageConfigurationGroup { |
| constructor(platforms, metrics, excludedConfigurations) |
| { |
| this._measurementSets = []; |
| this._configurationList = []; |
| this._setToRatio = new Map; |
| this._ratio = NaN; |
| this._label = null; |
| this._missingPlatforms = new Set; |
| this._platformsWithoutBaseline = new Set; |
| this._isFetching = false; |
| this._smallerIsBetter = metrics.length ? metrics[0].isSmallerBetter() : null; |
| |
| for (const platform of platforms) { |
| console.assert(platform instanceof Platform); |
| let foundInSomeMetric = false; |
| let excludedMerticCount = 0; |
| for (const metric of metrics) { |
| console.assert(metric instanceof Metric); |
| console.assert(this._smallerIsBetter == metric.isSmallerBetter()); |
| metric.isSmallerBetter(); |
| |
| if (excludedConfigurations && platform.id() in excludedConfigurations && excludedConfigurations[platform.id()].includes(+metric.id())) { |
| excludedMerticCount += 1; |
| continue; |
| } |
| if (!platform.hasMetric(metric)) |
| continue; |
| foundInSomeMetric = true; |
| this._measurementSets.push(MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric))); |
| this._configurationList.push([platform.id(), metric.id()]); |
| } |
| if (!foundInSomeMetric && excludedMerticCount < metrics.length) |
| this._missingPlatforms.add(platform); |
| } |
| } |
| |
| ratio() { return this._ratio; } |
| label() { return this._label; } |
| changeType() { return this._changeType; } |
| configurationList() { return this._configurationList; } |
| isFetching() { return this._isFetching; } |
| missingPlatforms() { return this._missingPlatforms.size ? Array.from(this._missingPlatforms) : null; } |
| platformsWithoutBaseline() { return this._platformsWithoutBaseline.size ? Array.from(this._platformsWithoutBaseline) : null; } |
| |
| fetchAndComputeSummary(timeRange) |
| { |
| console.assert(timeRange instanceof Array); |
| console.assert(typeof(timeRange[0]) == 'number'); |
| console.assert(typeof(timeRange[1]) == 'number'); |
| |
| var promises = []; |
| for (var set of this._measurementSets) |
| promises.push(this._fetchAndComputeRatio(set, timeRange)); |
| |
| var self = this; |
| var fetched = false; |
| setTimeout(function () { |
| // Don't set _isFetching to true if all promises were to resolve immediately (cached). |
| if (!fetched) |
| self._isFetching = true; |
| }, 50); |
| |
| return Promise.all(promises).then(function () { |
| fetched = true; |
| self._isFetching = false; |
| self._computeSummary(); |
| }); |
| } |
| |
| _computeSummary() |
| { |
| var ratios = []; |
| for (var set of this._measurementSets) { |
| var ratio = this._setToRatio.get(set); |
| if (!isNaN(ratio)) |
| ratios.push(ratio); |
| } |
| |
| var averageRatio = Statistics.mean(ratios); |
| if (isNaN(averageRatio)) |
| return; |
| |
| var currentIsSmallerThanBaseline = averageRatio < 1; |
| var changeType = this._smallerIsBetter == currentIsSmallerThanBaseline ? 'better' : 'worse'; |
| averageRatio = Math.abs(averageRatio - 1); |
| |
| this._ratio = averageRatio * (changeType == 'better' ? 1 : -1); |
| this._label = (averageRatio * 100).toFixed(1) + '%'; |
| this._changeType = changeType; |
| } |
| |
| _fetchAndComputeRatio(set, timeRange) |
| { |
| var setToRatio = this._setToRatio; |
| var self = this; |
| return set.fetchBetween(timeRange[0], timeRange[1]).then(function () { |
| var baselineTimeSeries = set.fetchedTimeSeries('baseline', false, false); |
| var currentTimeSeries = set.fetchedTimeSeries('current', false, false); |
| |
| const baselineMean = SummaryPageConfigurationGroup._meanForTimeRange(baselineTimeSeries, timeRange); |
| const currentMean = SummaryPageConfigurationGroup._meanForTimeRange(currentTimeSeries, timeRange); |
| var platform = Platform.findById(set.platformId()); |
| if (!currentMean) |
| self._missingPlatforms.add(platform); |
| else if (!baselineMean) |
| self._platformsWithoutBaseline.add(platform); |
| |
| setToRatio.set(set, currentMean / baselineMean); |
| }).catch(function () { |
| setToRatio.set(set, NaN); |
| }); |
| } |
| |
| static _startAndEndPointForTimeRange(timeSeries, timeRange) |
| { |
| if (!timeSeries.firstPoint()) |
| return NaN; |
| |
| const startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint(); |
| const afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint(); |
| let endPoint = timeSeries.previousPoint(afterEndPoint); |
| if (!endPoint || startPoint == afterEndPoint) |
| endPoint = afterEndPoint; |
| |
| return [startPoint, endPoint]; |
| } |
| |
| static _meanForTimeRange(timeSeries, timeRange) |
| { |
| const [startPoint, endPoint] = SummaryPageConfigurationGroup._startAndEndPointForTimeRange(timeSeries, timeRange); |
| return Statistics.mean(timeSeries.viewBetweenPoints(startPoint, endPoint).values()); |
| } |
| } |