blob: 0bc8c286d8c63c8e1c121f30bdc77238812d9a43 [file] [log] [blame]
class AnalysisResultsViewer extends ResultsTable {
constructor()
{
super('analysis-results-viewer');
this._startPoint = null;
this._endPoint = null;
this._metric = null;
this._testGroups = null;
this._currentTestGroup = null;
this._rangeSelectorLabels = [];
this._selectedRange = {};
this._expandedPoints = new Set;
this._groupToCellMap = new Map;
this._selectorRadioButtonList = {};
this._renderTestGroupsLazily = new LazilyEvaluatedFunction(this.renderTestGroups.bind(this));
}
setRangeSelectorLabels(labels) { this._rangeSelectorLabels = labels; }
selectedRange() { return this._selectedRange; }
setPoints(startPoint, endPoint, metric)
{
this._metric = metric;
this._startPoint = startPoint;
this._endPoint = endPoint;
this._expandedPoints = new Set;
this._expandedPoints.add(startPoint);
this._expandedPoints.add(endPoint);
this.enqueueToRender();
}
setTestGroups(testGroups, currentTestGroup)
{
this._testGroups = testGroups;
this._currentTestGroup = currentTestGroup;
if (currentTestGroup && this._rangeSelectorLabels.length) {
const commitSets = currentTestGroup.requestedCommitSets();
this._selectedRange = {
[this._rangeSelectorLabels[0]]: commitSets[0],
[this._rangeSelectorLabels[1]]: commitSets[1]
};
}
this.enqueueToRender();
}
setAnalysisResultsView(analysisResultsView)
{
console.assert(analysisResultsView instanceof AnalysisResultsView);
this._analysisResultsView = analysisResultsView;
this.enqueueToRender();
}
render()
{
super.render();
Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'render');
this._renderTestGroupsLazily.evaluate(this._testGroups,
this._startPoint, this._endPoint, this._metric, this._analysisResultsView, this._expandedPoints);
for (const label of this._rangeSelectorLabels) {
const commitSet = this._selectedRange[label];
if (!commitSet)
continue;
const list = this._selectorRadioButtonList[label] || [];
for (const item of list) {
if (item.commitSet.equals(commitSet))
item.radio.checked = true;
}
}
const selectedCell = this.content().querySelector('td.selected');
if (selectedCell)
selectedCell.classList.remove('selected');
if (this._groupToCellMap && this._currentTestGroup) {
const cell = this._groupToCellMap.get(this._currentTestGroup);
if (cell)
cell.classList.add('selected');
}
Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'render');
}
renderTestGroups(testGroups, startPoint, endPoint, metric, analysisResults, expandedPoints)
{
if (!testGroups || !startPoint || !endPoint || !metric || !analysisResults)
return false;
Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'renderTestGroups');
const commitSetsInTestGroups = this._collectCommitSetsInTestGroups(testGroups);
const rowToMatchingCommitSets = new Map;
const rows = this._buildRowsForPointsAndTestGroups(commitSetsInTestGroups, rowToMatchingCommitSets);
const testGroupLayoutMap = new Map;
rows.forEach((row, rowIndex) => {
const matchingCommitSets = rowToMatchingCommitSets.get(row);
if (!matchingCommitSets) {
console.assert(row instanceof AnalysisResultsViewer.ExpandableRow);
return;
}
for (let entry of matchingCommitSets) {
const testGroup = entry.testGroup();
let block = testGroupLayoutMap.get(testGroup);
if (!block) {
block = new AnalysisResultsViewer.TestGroupStackingBlock(testGroup, this._analysisResultsView,
this._groupToCellMap, () => this.dispatchAction('testGroupClick', testGroup));
testGroupLayoutMap.set(testGroup, block);
}
block.addRowIndex(entry, rowIndex);
}
});
const [additionalColumnsByRow, columnCount] = AnalysisResultsViewer._layoutBlocks(rows.length, testGroups.map((group) => testGroupLayoutMap.get(group)));
for (const label of this._rangeSelectorLabels)
this._selectorRadioButtonList[label] = [];
const element = ComponentBase.createElement;
const buildHeaders = (headers) => {
return [
this._rangeSelectorLabels.map((label) => element('th', label)),
headers,
columnCount ? element('td', {colspan: columnCount + 1, class: 'stacking-block'}) : [],
]
};
const buildColumns = (columns, row, rowIndex) => {
return [
this._rangeSelectorLabels.map((label) => {
if (!row.commitSet())
return element('td', '');
const radio = element('input', {type: 'radio', name: label, onchange: () => {
this._selectedRange[label] = row.commitSet();
this.dispatchAction('rangeSelectorClick', label, row);
}});
this._selectorRadioButtonList[label].push({radio, commitSet: row.commitSet()});
return element('td', radio);
}),
columns,
additionalColumnsByRow[rowIndex],
];
}
this.renderTable(metric.makeFormatter(4), [{rows}], 'Point', buildHeaders, buildColumns);
Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'renderTestGroups');
return true;
}
_collectCommitSetsInTestGroups(testGroups)
{
if (!this._testGroups)
return [];
var commitSetsInTestGroups = [];
for (var group of this._testGroups) {
var sortedSets = group.requestedCommitSets();
for (var i = 0; i < sortedSets.length; i++)
commitSetsInTestGroups.push(new AnalysisResultsViewer.CommitSetInTestGroup(group, sortedSets[i], sortedSets[i + 1]));
}
return commitSetsInTestGroups;
}
_buildRowsForPointsAndTestGroups(commitSetsInTestGroups, rowToMatchingCommitSets)
{
console.assert(this._startPoint.series == this._endPoint.series);
var rowList = [];
var pointAfterEnd = this._endPoint.series.nextPoint(this._endPoint);
var commitSetsWithPoints = new Set;
var pointIndex = 0;
var previousPoint;
for (var point = this._startPoint; point && point != pointAfterEnd; point = point.series.nextPoint(point), pointIndex++) {
const commitSetInPoint = point.commitSet();
const matchingCommitSets = [];
for (var entry of commitSetsInTestGroups) {
if (commitSetInPoint.equals(entry.commitSet()) && !commitSetsWithPoints.has(entry)) {
matchingCommitSets.push(entry);
commitSetsWithPoints.add(entry);
}
}
const hasMatchingTestGroup = !!matchingCommitSets.length;
if (!hasMatchingTestGroup && !this._expandedPoints.has(point))
continue;
const row = new ResultsTableRow(pointIndex.toString(), commitSetInPoint);
row.setResult(point);
if (previousPoint && previousPoint.series.nextPoint(previousPoint) != point)
rowList.push(new AnalysisResultsViewer.ExpandableRow(this._expandBetween.bind(this, previousPoint, point)));
previousPoint = point;
rowToMatchingCommitSets.set(row, matchingCommitSets);
rowList.push(row);
}
commitSetsInTestGroups.forEach(function (entry) {
if (commitSetsWithPoints.has(entry))
return;
for (var i = 0; i < rowList.length; i++) {
var row = rowList[i];
if (!(row instanceof AnalysisResultsViewer.ExpandableRow) && row.commitSet().equals(entry.commitSet())) {
rowToMatchingCommitSets.get(row).push(entry);
return;
}
}
var groupTime = entry.commitSet().latestCommitTime();
var newRow = new ResultsTableRow(null, entry.commitSet());
rowToMatchingCommitSets.set(newRow, [entry]);
for (var i = 0; i < rowList.length; i++) {
if (rowList[i] instanceof AnalysisResultsViewer.ExpandableRow)
continue;
if (entry.succeedingCommitSet() && rowList[i].commitSet().equals(entry.succeedingCommitSet())) {
rowList.splice(i, 0, newRow);
return;
}
var rowTime = rowList[i].commitSet().latestCommitTime();
if (rowTime > groupTime) {
rowList.splice(i, 0, newRow);
return;
}
if (rowTime == groupTime) {
// Missing some commits. Do as best as we can to avoid going backwards in time.
var repositoriesInNewRow = entry.commitSet().repositories();
for (var j = i; j < rowList.length; j++) {
if (rowList[j] instanceof AnalysisResultsViewer.ExpandableRow)
continue;
for (var repository of repositoriesInNewRow) {
var newCommit = entry.commitSet().commitForRepository(repository);
var rowCommit = rowList[j].commitSet().commitForRepository(repository);
if (!rowCommit || newCommit.time() < rowCommit.time()) {
rowList.splice(j, 0, newRow);
return;
}
}
}
}
}
var newRow = new ResultsTableRow(null, entry.commitSet());
rowToMatchingCommitSets.set(newRow, [entry]);
rowList.push(newRow);
});
return rowList;
}
_expandBetween(pointBeforeExpansion, pointAfterExpansion)
{
console.assert(pointBeforeExpansion.series == pointAfterExpansion.series);
var indexBeforeStart = pointBeforeExpansion.seriesIndex;
var indexAfterEnd = pointAfterExpansion.seriesIndex;
console.assert(indexBeforeStart + 1 < indexAfterEnd);
var series = pointAfterExpansion.series;
var increment = Math.ceil((indexAfterEnd - indexBeforeStart) / 5);
if (increment < 3)
increment = 1;
const expandedPoints = new Set([...this._expandedPoints]);
for (var i = indexBeforeStart + 1; i < indexAfterEnd; i += increment)
expandedPoints.add(series.findPointByIndex(i));
this._expandedPoints = expandedPoints;
this.enqueueToRender();
}
static _layoutBlocks(rowCount, blocks)
{
const sortedBlocks = this._sortBlocksByRow(blocks);
const columns = [];
for (const block of sortedBlocks)
this._insertBlockInFirstAvailableColumn(columns, block);
const rows = new Array(rowCount);
for (let i = 0; i < rowCount; i++)
rows[i] = this._createCellsForRow(columns, i);
return [rows, columns.length];
}
static _sortBlocksByRow(blocks)
{
for (let i = 0; i < blocks.length; i++)
blocks[i].index = i;
return blocks.slice(0).sort((block1, block2) => {
const startRowDiff = block1.startRowIndex() - block2.startRowIndex();
if (startRowDiff)
return startRowDiff;
// Order backwards for end rows in order to place test groups with a larger range at the beginning.
const endRowDiff = block2.endRowIndex() - block1.endRowIndex();
if (endRowDiff)
return endRowDiff;
return block1.index - block2.index;
});
}
static _insertBlockInFirstAvailableColumn(columns, newBlock)
{
for (const existingColumn of columns) {
for (let i = 0; i < existingColumn.length; i++) {
const currentBlock = existingColumn[i];
if ((!i || existingColumn[i - 1].endRowIndex() < newBlock.startRowIndex())
&& newBlock.endRowIndex() < currentBlock.startRowIndex()) {
existingColumn.splice(i, 0, newBlock);
return;
}
}
const lastBlock = existingColumn[existingColumn.length - 1];
console.assert(lastBlock);
if (lastBlock.endRowIndex() < newBlock.startRowIndex()) {
existingColumn.push(newBlock);
return;
}
}
columns.push([newBlock]);
}
static _createCellsForRow(columns, rowIndex)
{
const element = ComponentBase.createElement;
const link = ComponentBase.createLink;
const crateEmptyCell = (rowspan) => element('td', {rowspan: rowspan, class: 'stacking-block'}, '');
const cells = [element('td', {class: 'stacking-block'}, '')];
for (const blocksInColumn of columns) {
if (!rowIndex && blocksInColumn[0].startRowIndex()) {
cells.push(crateEmptyCell(blocksInColumn[0].startRowIndex()));
continue;
}
for (let i = 0; i < blocksInColumn.length; i++) {
const block = blocksInColumn[i];
if (block.startRowIndex() == rowIndex) {
cells.push(block.createStackingCell());
break;
}
const rowCount = i + 1 < blocksInColumn.length ? blocksInColumn[i + 1].startRowIndex() : this._rowCount;
const remainingRows = rowCount - block.endRowIndex() - 1;
if (rowIndex == block.endRowIndex() + 1 && rowIndex < rowCount)
cells.push(crateEmptyCell(remainingRows));
}
}
return cells;
}
static htmlTemplate()
{
return `<section class="analysis-view">${ResultsTable.htmlTemplate()}</section>`;
}
static cssTemplate()
{
return ResultsTable.cssTemplate() + `
.analysis-view .stacking-block {
position: relative;
border: solid 1px #fff;
cursor: pointer;
}
.analysis-view .stacking-block a {
display: block;
text-decoration: none;
color: inherit;
font-size: 0.8rem;
padding: 0 0.1rem;
max-width: 3rem;
}
.analysis-view .stacking-block:not(.failed) {
color: black;
opacity: 1;
}
.analysis-view .stacking-block.selected,
.analysis-view .stacking-block:hover {
text-decoration: underline;
}
.analysis-view .stacking-block.selected:before {
content: '';
position: absolute;
left: 0px;
top: 0px;
width: calc(100% - 2px);
height: calc(100% - 2px);
border: solid 1px #333;
}
.analysis-view .stacking-block.failed {
background: rgba(128, 51, 128, 0.5);
}
.analysis-view .stacking-block.unchanged {
background: rgba(128, 128, 128, 0.5);
}
.analysis-view .stacking-block.pending {
background: rgba(204, 204, 51, 0.2);
}
.analysis-view .stacking-block.running {
background: rgba(204, 204, 51, 0.5);
}
.analysis-view .stacking-block.worse {
background: rgba(255, 102, 102, 0.5);
}
.analysis-view .stacking-block.better {
background: rgba(102, 102, 255, 0.5);
}
.analysis-view .point-label-with-expansion-link {
font-size: 0.7rem;
}
.analysis-view .point-label-with-expansion-link a {
color: #999;
text-decoration: none;
}
`;
}
}
ComponentBase.defineElement('analysis-results-viewer', AnalysisResultsViewer);
AnalysisResultsViewer.ExpandableRow = class extends ResultsTableRow {
constructor(callback)
{
super(null, null);
this._callback = callback;
}
resultContent() { return ''; }
heading()
{
return ComponentBase.createElement('span', {class: 'point-label-with-expansion-link'}, [
ComponentBase.createLink('(Expand)', 'Expand', this._callback),
]);
}
}
AnalysisResultsViewer.CommitSetInTestGroup = class {
constructor(testGroup, commitSet, succeedingCommitSet)
{
console.assert(testGroup instanceof TestGroup);
console.assert(commitSet instanceof CommitSet);
this._testGroup = testGroup;
this._commitSet = commitSet;
this._succeedingCommitSet = succeedingCommitSet;
}
testGroup() { return this._testGroup; }
commitSet() { return this._commitSet; }
succeedingCommitSet() { return this._succeedingCommitSet; }
}
AnalysisResultsViewer.TestGroupStackingBlock = class {
constructor(testGroup, analysisResultsView, groupToCellMap, callback)
{
this._testGroup = testGroup;
this._analysisResultsView = analysisResultsView;
this._commitSetIndexRowIndexMap = [];
this._groupToCellMap = groupToCellMap;
this._callback = callback;
}
addRowIndex(commitSetInTestGroup, rowIndex)
{
console.assert(commitSetInTestGroup instanceof AnalysisResultsViewer.CommitSetInTestGroup);
this._commitSetIndexRowIndexMap.push({commitSet: commitSetInTestGroup.commitSet(), rowIndex});
}
testGroup() { return this._testGroup; }
createStackingCell()
{
const {label, title, status} = this._computeTestGroupStatus();
const cell = ComponentBase.createElement('td', {
rowspan: this.endRowIndex() - this.startRowIndex() + 1,
title,
class: 'stacking-block ' + status,
onclick: this._callback,
}, ComponentBase.createLink(label, title, this._callback));
this._groupToCellMap.set(this._testGroup, cell);
return cell;
}
isComplete() { return this._commitSetIndexRowIndexMap.length >= 2; }
startRowIndex() { return this._commitSetIndexRowIndexMap[0].rowIndex; }
endRowIndex() { return this._commitSetIndexRowIndexMap[this._commitSetIndexRowIndexMap.length - 1].rowIndex; }
_measurementsForCommitSet(testGroup, commitSet)
{
return testGroup.requestsForCommitSet(commitSet).map((request) => {
return this._analysisResultsView.resultForRequest(request);
}).filter((result) => !!result);
}
_computeTestGroupStatus()
{
if (!this.isComplete())
return {label: null, title: null, status: null};
console.assert(this._commitSetIndexRowIndexMap.length <= 2); // FIXME: Support having more root sets.
const startValues = this._measurementsForCommitSet(this._testGroup, this._commitSetIndexRowIndexMap[0].commitSet);
const endValues = this._measurementsForCommitSet(this._testGroup, this._commitSetIndexRowIndexMap[1].commitSet);
const result = this._testGroup.compareTestResults(this._analysisResultsView.metric(), startValues, endValues);
return {label: result.label, title: result.fullLabelForMean, status: result.status};
}
}