blob: e4619b197487ce972c18c80afe89bfdb2cea887f [file] [log] [blame]
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());
}
}