resources/perf.webkit.org/public/v3/components/analysis-results-viewer.js (366 lines of code) (raw):

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}; } }