Diff
Modified: trunk/Websites/perf.webkit.org/ChangeLog (215632 => 215633)
--- trunk/Websites/perf.webkit.org/ChangeLog 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/ChangeLog 2017-04-21 20:20:05 UTC (rev 215633)
@@ -1,3 +1,145 @@
+2017-04-21 Ryosuke Niwa <[email protected]>
+
+ Make it possible to view results for sub tests and metrics in A/B testing
+ https://bugs.webkit.org/show_bug.cgi?id=170975
+
+ Reviewed by Chris Dumez.
+
+ Replaced TestGroupResultsTable, which was a single table that presented the test results with a set of revisions
+ each build request used, with TestGroupResultsViewer and TestGroupRevisionTable. TestGroupResultsViewer provides
+ an UI to look the results of sub-tests and sub-metrics and TestGroupRevisionTable provides an UI to display
+ the set of revisions each build request used. TestGroupRevisionTable can also show the list of custom roots now
+ that we've added UI to schedule an analysis task with a custom test group.
+
+ This patch extends BarGraphGroup to show multiple bars per SingleBarGraph using a canvas with bars indicating
+ their mean and confidence interval.
+
+ * browser-tests/index.html:
+ (ChartTest.importChartScripts): Include lazily-evaluated-function.js now that Test model object uses
+ LazilyEvaluatedFunction.
+
+ * public/v3/components/analysis-results-viewer.js:
+ (AnalysisResultsViewer.TestGroupStackingBlock.prototype._valuesForCommitSet):
+ (AnalysisResultsViewer.TestGroupStackingBlock.prototype._computeTestGroupStatus):
+
+ * public/v3/components/bar-graph-group.js:
+ (BarGraphGroup): No longer takes formatter. Added _computeRangeLazily and _showLabels as instance variables.
+ (BarGraphGroup.prototype.addBar): Now takes a list of values, their labels, mean, confidence interval, and
+ the colors of bar graphs shown for each value and the mean indicator.
+ (BarGraphGroup.prototype.showLabels): Added.
+ (BarGraphGroup.prototype.setShowLabels): Added.
+ (BarGraphGroup.prototype.range): Added.
+ (BarGraphGroup.prototype._computeRange): Renamed from updateGroupRendering. Now returns the range instead off
+ setting it to each bar, and each SingleBarGraph's render function uses the value via BarGraphGroup's range.
+ (BarGraph): Renamed from SingleBarGraph. Added various arguments introduced in addBar, and now stores various
+ lazily evaluated functions used for rendering.
+ (BarGraph.prototype.render): Rewritten to use canvas to draw bar graphs and show a label when group's
+ showLabels() returns true.
+ (BarGraph.prototype._resizeCanvas): Added.
+ (BarGraph.prototype._renderCanvas): Added.
+ (BarGraph.prototype._renderLabels): Added. We align the top of each label to the middle of each bar and shift it
+ back up by half the height of the label (0.4rem) using margin-top in css.
+ (BarGraph.htmlTemplate): Uses a canvas now.
+ (BarGraph.cssTemplate):
+
+ * public/v3/components/results-table.js:
+ (ResultsTable.prototype.renderTable): Updated per code changes to BarGraphGroup.
+ (ResultsTableRow.prototype.resultContent): Ditto.
+
+ * public/v3/components/test-group-results-table.js: Removed.
+ * public/v3/components/test-group-results-viewer.js: Added.
+ (TestGroupResultsViewer): Added. Shows a list of test results with bar graphs with mean and confidence interval
+ indicators. The results of sub tests and metrics can be expanded via "(Breakdown)" link shown below each test.
+ (TestGroupResultsViewer.prototype.setTestGroup): Added.
+ (TestGroupResultsViewer.prototype.setAnalysisResults): Added.
+ (TestGroupResultsViewer.prototype.render): Added.
+ (TestGroupResultsViewer.prototype._renderResultsTable): Compute the depth of the test tree we show, and construct
+ the header rows. Each sub test is "indented" by a new column.
+ (TestGroupResultsViewer.prototype._buildRowForTest): Added. Build rows for metrics of the given test. Expand the
+ list of its child tests if it's in expandedTests. Otherwise add a link to "Breakdown" if it has any child tests.
+ (TestGroupResultsViewer.prototype._buildRowForMetric): Added. Builds rows of table cells to show the results for
+ the given metric for each configuration.
+ (TestGroupResultsViewer.prototype._buildRowForMetric.createConfigurationRow): Added. A helper to build cells for
+ a given configuration represented by a requested commit set.
+ (TestGroupResultsViewer.prototype._buildValueMap): Added. Creates a mappting between a requested commit set, and
+ the list of values, mean, etc... associated with the results for the commit set.
+ (TestGroupResultsViewer.prototype._buildEmptyCells): Added. A helper to create empty cells to indent sub tests.
+ (TestGroupResultsViewer.prototype._expandCurrentMetrics): Added. Highlights the current metrics and renders the
+ label for each bar in the results.
+ (TestGroupResultsViewer.htmlTemplate): Added.
+ (TestGroupResultsViewer.cssTemplate): Added.
+
+ * public/v3/components/test-group-revision-table.js: Added.
+ (TestGroupRevisionTable): Added. Renders the list of revisions requested for each test configuration as well as
+ ones used in actual testing, and additional repositories being reported (e.g. repositories for helper scripts).
+ (TestGroupRevisionTable.prototype.setTestGroup): Added.
+ (TestGroupRevisionTable.prototype.setAnalysisResults): Added.
+ (TestGroupRevisionTable.prototype.render): Added.
+ (TestGroupRevisionTable.prototype._renderTable): Added. The basic algorithm here is to first create a row entry
+ object for each build request, merge cells that use the same revision of the same repository, and then render
+ the entire table.
+ (TestGroupRevisionTable.prototype._buildCommitCell): Added.
+ (TestGroupRevisionTable.prototype._buildCustomRootsCell): Added.
+ (TestGroupRevisionTable.prototype._mergeCellsWithSameCommitsAcrossRows): Added. Compute rowspan for each cell
+ by traversing the rows that use the same revision per repository, and store it in rowCountByRepository while
+ adding the repository to each succeeding row's repositoriesToSkip.
+ (TestGroupRevisionTable.htmlTemplate): Added.
+ (TestGroupRevisionTable.cssTemplate): Added.
+
+ * public/v3/index.html:
+ * public/v3/models/analysis-results.js:
+ (AnalysisResults): Added _metricIds and _lazilyComputedHighestTests as instance variables.
+ (AnalysisResults.prototype.findResult): Renamed from find.
+ (AnalysisResults.prototype.highestTests): Added.
+ (AnalysisResults.prototype._computeHighestTests): Added. Finds the root tests for this analysis result.
+ (AnalysisResults.prototype.add): Update _metricIds.
+ (AnalysisResults.prototype.commitSetForRequest): Added. Returns the reported commit set for the build request.
+ This commit set contains the set of revisions reported to /api/report by A/B testers.
+ (AnalysisResultsView.prototype.resultForRequest): Renamed from resultForBuildId.
+
+ * public/v3/models/metric.js:
+ (Metric.prototype.relativeName): Added. Computes the relative name given the test/metric path. This function is
+ used to determine the label for each test/metric in TestGroupResultsViewer.
+ (Metric.prototype.aggregatorLabel): Extracted from label.
+ (Metric.prototype.label):
+ (Metric.makeFormatter): Added the default value of false to alwaysShowSign.
+
+ * public/v3/models/test-group.js:
+ (TestGroup.prototype.compareTestResults): Now takes a metric instead of retrieving it from the analysis task
+ since a custom analysis task may not have a metric associated with it.
+
+ * public/v3/models/test.js:
+ (Test): Added _computePathLazily as an instance variable.
+ (Test.prototype.path): Lazily computes the path now that this function can be called on the same test for many
+ times in TestGroupResultsViewer while computing relative names of tests and metrics.
+ (Test.prototype._computePath): Extracted path.
+ (Test.prototype.fullName): Modernized the code.
+ (Test.prototype.relativeName): Added.
+
+ * public/v3/models/uploaded-file.js:
+ (UploadedFile):
+ (UploadedFile.prototype.deletedAt): Added.
+ (UploadedFile.prototype.label): Added.
+ (UploadedFile.prototype.url): Added.
+
+ * public/v3/pages/analysis-task-page.js:
+ (AnalysisTaskTestGroupPane.prototype.setTestGroups):
+ (AnalysisTaskTestGroupPane.prototype.setAnalysisResults): Replaced setAnalysisResultsView. Now takes an
+ analysisResults instead of its view.
+ (AnalysisTaskTestGroupPane.prototype.render): No longer enqueues the results table and the retry form to render
+ since the results table no longer exists, and the retry form re-renders itself as needed.
+ (AnalysisTaskTestGroupPane.htmlTemplate): Now uses test-group-results-viewer and test-group-revision-table
+ instead of test-group-results-table, which has been removed.
+ (AnalysisTaskTestGroupPane.cssTemplate):
+ (AnalysisTaskPage.prototype._assignTestResultsIfPossible):
+
+ * public/v3/pages/create-analysis-task-page.js:
+ (CreateAnalysisTaskPage.prototype._createAnalysisTaskWithGroup): Removed superflous console.log's.
+
+ * tools/js/v3-models.js: Import LazilyEvaluatedFunction now that it's used in the Test model.
+
+ * unit-tests/test-model-tests.js: Added.
+
2017-04-19 Ryosuke Niwa <[email protected]>
Another build fix after r215061. Clear TriggerableRepositoryGroup's static map in each iteration.
Modified: trunk/Websites/perf.webkit.org/browser-tests/index.html (215632 => 215633)
--- trunk/Websites/perf.webkit.org/browser-tests/index.html 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/browser-tests/index.html 2017-04-21 20:20:05 UTC (rev 215633)
@@ -207,6 +207,7 @@
{
return context.importScripts([
'../shared/statistics.js',
+ 'lazily-evaluated-function.js',
'instrumentation.js',
'models/data-model.js',
'models/time-series.js',
Modified: trunk/Websites/perf.webkit.org/public/v3/components/analysis-results-viewer.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/components/analysis-results-viewer.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/components/analysis-results-viewer.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -487,7 +487,7 @@
_valuesForCommitSet(testGroup, commitSet)
{
return testGroup.requestsForCommitSet(commitSet).map((request) => {
- return this._analysisResultsView.resultForBuildId(request.buildId());
+ return this._analysisResultsView.resultForRequest(request);
}).filter((result) => !!result).map((result) => result.value);
}
@@ -498,7 +498,7 @@
console.assert(this._commitSetIndexRowIndexMap.length <= 2); // FIXME: Support having more root sets.
const startValues = this._valuesForCommitSet(this._testGroup, this._commitSetIndexRowIndexMap[0].commitSet);
const endValues = this._valuesForCommitSet(this._testGroup, this._commitSetIndexRowIndexMap[1].commitSet);
- const result = this._testGroup.compareTestResults(startValues, endValues);
+ const result = this._testGroup.compareTestResults(this._analysisResultsView.metric(), startValues, endValues);
return {label: result.label, title: result.fullLabel, status: result.status};
}
}
Modified: trunk/Websites/perf.webkit.org/public/v3/components/bar-graph-group.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/components/bar-graph-group.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/components/bar-graph-group.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -1,104 +1,205 @@
class BarGraphGroup {
- constructor(formatter)
+ constructor()
{
this._bars = [];
- this._formatter = formatter;
+ this._computeRangeLazily = new LazilyEvaluatedFunction(this._computeRange.bind(this));
+ this._showLabels = false;
}
- addBar(value, interval)
+ addBar(values, valueLabels, mean, interval, barColor, meanIndicatorColor)
{
- var newBar = new SingleBarGraph(this);
- this._bars.push({bar: newBar, value: value, interval: interval});
- return newBar;
+ const bar = new BarGraph(this, values, valueLabels, mean, interval, barColor, meanIndicatorColor);
+ this._bars.push({bar, values, interval});
+ return bar;
}
- updateGroupRendering()
+ showLabels() { return this._showLabels; }
+ setShowLabels(showLabels)
{
- Instrumentation.startMeasuringTime('BarGraphGroup', 'updateGroupRendering');
+ this._showLabels = showLabels;
+ for (const entry of this._bars)
+ entry.bar.enqueueToRender();
+ }
- var min = Infinity;
- var max = -Infinity;
- for (var entry of this._bars) {
- min = Math.min(min, entry.interval ? entry.interval[0] : entry.value);
- max = Math.max(max, entry.interval ? entry.interval[1] : entry.value);
+ range()
+ {
+ return this._computeRangeLazily.evaluate(...this._bars);
+ }
+
+ _computeRange(...bars)
+ {
+ Instrumentation.startMeasuringTime('BarGraphGroup', 'updateGroup');
+
+ let min = Infinity;
+ let max = -Infinity;
+ for (const entry of bars) {
+ for (const value of entry.values) {
+ if (isNaN(value))
+ continue;
+ min = Math.min(min, value);
+ max = Math.max(max, value);
+ }
+ if (entry.interval) {
+ for (const value of entry.interval) {
+ min = Math.min(min, value);
+ max = Math.max(max, value);
+ }
+ }
}
- for (var entry of this._bars) {
- var value = entry.value;
- var formattedValue = this._formatter(value);
- if (entry.interval)
- formattedValue += ' \u00B1' + ((value - entry.interval[0]) * 100 / value).toFixed(2) + '%';
+ const diff = max - min;
+ min -= diff * 0.1;
+ max += diff * 0.1;
- var diff = (max - min);
- var range = diff * 1.2;
- var start = min - (range - diff) / 2;
+ const xForValue = (value, width) => (value - min) / (max - min) * width;
+ const barRangeForValue = (value, width) => [0, (value - min) / (max - min) * width];
- entry.bar.update((value - start) / range, formattedValue);
- entry.bar.enqueueToRender();
- }
+ Instrumentation.endMeasuringTime('BarGraphGroup', 'updateGroup');
- Instrumentation.endMeasuringTime('BarGraphGroup', 'updateGroupRendering');
+ return {min, max, xForValue, barRangeForValue};
}
}
-class SingleBarGraph extends ComponentBase {
+class BarGraph extends ComponentBase {
+ constructor(group, values, valueLabels, mean, interval, barColor, meanIndicatorColor)
+ {
+ super('bar-graph');
+ this._group = group;
+ this._values = values;
+ this._valueLabels = valueLabels;
+ this._mean = mean;
+ this._interval = interval;
+ this._barColor = barColor;
+ this._meanIndicatorColor = meanIndicatorColor;
+ this._resizeCanvasLazily = new LazilyEvaluatedFunction(this._resizeCanvas.bind(this));
+ this._renderCanvasLazily = new LazilyEvaluatedFunction(this._renderCanvas.bind(this));
+ this._renderLabelsLazily = new LazilyEvaluatedFunction(this._renderLabels.bind(this));
+ }
- constructor(group)
+ render()
{
- console.assert(group instanceof BarGraphGroup);
- super('single-bar-graph');
- this._percentage = 0;
- this._label = null;
+ Instrumentation.startMeasuringTime('SingleBarGraph', 'render');
+
+ if (!this._values)
+ return false;
+
+ const range = this._group.range();
+ const showLabels = this._group.showLabels();
+
+ const canvas = this.content('graph');
+ const element = this.element();
+ const width = element.offsetWidth;
+ const height = element.offsetHeight;
+ const scale = this._resizeCanvasLazily.evaluate(canvas, width, height);
+
+ const step = this._renderCanvasLazily.evaluate(canvas, width, height, scale, this._values,
+ showLabels ? null : this._mean, showLabels ? null : this._interval, range);
+
+ this._renderLabelsLazily.evaluate(canvas, step, showLabels ? this._valueLabels : null);
+
+ Instrumentation.endMeasuringTime('SingleBarGraph', 'render');
}
- update(percentage, label)
+ _resizeCanvas(canvas, width, height)
{
- this._percentage = percentage;
- this._label = label;
+ const scale = window.devicePixelRatio;
+ canvas.width = width * scale;
+ canvas.height = height * scale;
+ canvas.style.width = width + 'px';
+ canvas.style.height = height + 'px';
+ return scale;
}
- render()
+ _renderCanvas(canvas, width, height, scale, values, mean, interval, range)
{
- this.content().querySelector('.percentage').style.width = `calc(${this._percentage * 100}% - 2px)`;
- this.content().querySelector('.label').textContent = this._label;
+ const context = canvas.getContext('2d');
+ context.scale(scale, scale);
+ context.clearRect(0, 0, width, height);
+
+ context.fillStyle = this._barColor;
+ context.strokeStyle = this._meanIndicatorColor;
+ context.lineWidth = 1;
+
+ const step = Math.floor(height / values.length);
+ for (let i = 0; i < values.length; i++) {
+ const value = values[i];
+ if (isNaN(value))
+ continue;
+ const barWidth = range.xForValue(value, width);
+ const barRange = range.barRangeForValue(value, width);
+ const y = i * step;
+ context.fillRect(0, y, barWidth, step - 1);
+ }
+
+ const filteredValues = values.filter((value) => !isNaN(value));
+ if (mean) {
+ const x = range.xForValue(mean, width);
+ context.beginPath();
+ context.moveTo(x, 0);
+ context.lineTo(x, height);
+ context.stroke();
+ }
+
+ if (interval) {
+ const x1 = range.xForValue(interval[0], width);
+ const x2 = range.xForValue(interval[1], width);
+
+ const errorBarHeight = 10;
+ const endBarY1 = height / 2 - errorBarHeight / 2;
+ const endBarY2 = height / 2 + errorBarHeight / 2;
+
+ context.beginPath();
+ context.moveTo(x1, endBarY1);
+ context.lineTo(x1, endBarY2);
+ context.moveTo(x1, height / 2);
+ context.lineTo(x2, height / 2);
+ context.moveTo(x2, endBarY1);
+ context.lineTo(x2, endBarY2);
+ context.stroke();
+ }
+
+ return step;
}
+ _renderLabels(canvas, step, valueLabels)
+ {
+ if (!valueLabels)
+ valueLabels = [];
+
+ const element = ComponentBase.createElement;
+ this.renderReplace(this.content('labels'), valueLabels.map((label, i) => {
+ const labelElement = element('div', {class: 'label'}, label);
+ labelElement.style.top = (i + 0.5) * step + 'px';
+ return labelElement;
+ }));
+ canvas.style.opacity = valueLabels.length ? 0.5 : 1;
+ }
+
static htmlTemplate()
{
- return `<div class="single-bar-graph"><div class="percentage"></div><div class="label">-</div></div>`;
+ return `<canvas id="graph"></canvas><div id="labels"></div>`;
}
static cssTemplate()
{
return `
- .single-bar-graph {
+ :host {
+ display: block !important;
+ overflow: hidden;
position: relative;
- display: block;
- background: #eee;
- height: 100%;
- overflow: hidden;
- text-decoration: none;
- color: black;
}
- .single-bar-graph .percentage {
+ .label {
position: absolute;
- top: 1px;
- left: 1px;
- background: #ccc;
- height: calc(100% - 2px);
- }
- .single-bar-graph .label {
- position: absolute;
- top: calc(50% - 0.35rem);
left: 0;
width: 100%;
- height: 100%;
+ text-align: center;
font-size: 0.8rem;
line-height: 0.8rem;
- text-align: center;
+ margin-top: -0.4rem;
}
`;
}
+}
-}
+ComponentBase.defineElement('bar-graph', BarGraph);
Modified: trunk/Websites/perf.webkit.org/public/v3/components/base.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/components/base.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/components/base.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -16,6 +16,7 @@
this._element = element;
this._shadow = null;
this._actionCallbacks = new Map;
+ this._oldSizeToCheckForResize = null;
if (!ComponentBase.useNativeCustomElements)
element.addEventListener('DOMNodeInsertedIntoDocument', () => this.enqueueToRender());
@@ -74,18 +75,55 @@
{
Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire');
+ const componentsToRender = ComponentBase._componentsToRender;
+ this._renderLoop();
+ if (ComponentBase._componentsToRenderOnResize) {
+ const resizedComponents = this._resizedComponents(ComponentBase._componentsToRenderOnResize);
+ if (resizedComponents.length) {
+ ComponentBase._componentsToRender = new Set(resizedComponents);
+ this._renderLoop();
+ }
+ }
+
+ Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire');
+ }
+
+ static _renderLoop()
+ {
+ const componentsToRender = ComponentBase._componentsToRender;
do {
- const currentSet = [...ComponentBase._componentsToRender];
- ComponentBase._componentsToRender.clear();
+ const currentSet = [...componentsToRender];
+ componentsToRender.clear();
+ const resizeSet = ComponentBase._componentsToRenderOnResize;
for (let component of currentSet) {
Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
component.render();
+ if (resizeSet && resizeSet.has(component)) {
+ const element = component.element();
+ component._oldSizeToCheckForResize = {width: element.offsetWidth, height: element.offsetHeight};
+ }
Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
}
- } while (ComponentBase._componentsToRender.size);
+ } while (componentsToRender.size);
ComponentBase._componentsToRender = null;
+ }
- Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire');
+ static _resizedComponents(componentSet)
+ {
+ if (!componentSet)
+ return [];
+
+ const resizedList = [];
+ for (let component of componentSet) {
+ const element = component.element();
+ const width = element.offsetWidth;
+ const height = element.offsetHeight;
+ const oldSize = component._oldSizeToCheckForResize;
+ if (oldSize && oldSize.width == width && oldSize.height == height)
+ continue;
+ resizedList.push(component);
+ }
+ return resizedList;
}
static _connectedComponentToRenderOnResize(component)
@@ -93,7 +131,8 @@
if (!ComponentBase._componentsToRenderOnResize) {
ComponentBase._componentsToRenderOnResize = new Set;
window.addEventListener('resize', () => {
- for (let component of ComponentBase._componentsToRenderOnResize)
+ const resized = this._resizedComponents(ComponentBase._componentsToRenderOnResize);
+ for (const component of resized)
component.enqueueToRender();
});
}
Modified: trunk/Websites/perf.webkit.org/public/v3/components/results-table.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/components/results-table.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/components/results-table.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -19,7 +19,8 @@
const [repositoryList, constantCommits] = this._computeRepositoryList(rowGroups);
- const barGraphGroup = new BarGraphGroup(valueFormatter);
+ const barGraphGroup = new BarGraphGroup();
+ barGraphGroup.setShowLabels(true);
const element = ComponentBase.createElement;
let hasGroupHeading = false;
const tableBodies = rowGroups.map((group) => {
@@ -37,7 +38,7 @@
if (row.labelForWholeRow())
cells.push(element('td', {class: 'whole-row-label', colspan: repositoryList.length + 1}, row.labelForWholeRow()));
else {
- cells.push(element('td', row.resultContent(barGraphGroup)));
+ cells.push(element('td', row.resultContent(valueFormatter, barGraphGroup)));
cells.push(this._createRevisionListCells(repositoryList, revisionSupressionCount, group, row.commitSet(), rowIndex));
}
@@ -58,8 +59,6 @@
this.renderReplace(this.content('constant-commits'), constantCommits.map((commit) => element('li', commit.title())));
- barGraphGroup.updateGroupRendering();
-
Instrumentation.endMeasuringTime('ResultsTable', 'renderTable');
}
@@ -201,10 +200,10 @@
border-top: solid 1px #ccc;
}
- .results-table single-bar-graph {
+ .results-table bar-graph {
display: block;
width: 7rem;
- height: 1.2rem;
+ height: 1rem;
}
#constant-commits {
@@ -252,9 +251,17 @@
setLabelForWholeRow(label) { this._labelForWholeRow = label; }
labelForWholeRow() { return this._labelForWholeRow; }
- resultContent(barGraphGroup)
+ resultContent(valueFormatter, barGraphGroup)
{
- var resultContent = this._result ? barGraphGroup.addBar(this._result.value, this._result.interval) : this._label;
+ let resultContent = this._label;
+ if (this._result) {
+ const value = this._result.value;
+ const interval = this._result.interval;
+ let label = valueFormatter(value);
+ if (interval)
+ label += ' \u00B1' + ((value - interval[0]) * 100 / value).toFixed(2) + '%';
+ resultContent = barGraphGroup.addBar([value], [label], null, null, '#ccc', null);
+ }
return this._link ? ComponentBase.createLink(resultContent, this._label, this._link) : resultContent;
}
}
Deleted: trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-table.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-table.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-table.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -1,139 +0,0 @@
-
-class TestGroupResultsTable extends ResultsTable {
- constructor()
- {
- super('test-group-results-table');
- this._testGroup = null;
- this._renderTestGroupLazily = new LazilyEvaluatedFunction(this._renderTestGroup.bind(this));
- }
-
- setTestGroup(testGroup)
- {
- this._testGroup = testGroup;
- this.enqueueToRender();
- }
-
- render()
- {
- super.render();
- this._renderTestGroupLazily.evaluate(this._testGroup, this._analysisResultsView);
- }
-
- _renderTestGroup(testGroup, analysisResults)
- {
- if (!analysisResults)
- return;
- const rowGroups = this._buildRowGroups();
- this.renderTable(
- analysisResults.metric().makeFormatter(4),
- rowGroups,
- 'Configuration');
- }
-
- _buildRowGroups()
- {
- const testGroup = this._testGroup;
- if (!testGroup)
- return [];
-
- const commitSets = this._testGroup.requestedCommitSets();
- const resultsByCommitSet = new Map;
- const groups = commitSets.map((commitSet) => {
- const group = this._buildRowGroupForCommitSet(testGroup, commitSet, resultsByCommitSet);
- resultsByCommitSet.set(commitSet, group.results);
- return group;
- });
-
- const comparisonRows = [];
- for (let i = 0; i < commitSets.length - 1; i++) {
- const startCommit = commitSets[i];
- for (let j = i + 1; j < commitSets.length; j++) {
- const endCommit = commitSets[j];
- const startResults = resultsByCommitSet.get(startCommit) || [];
- const endResults = resultsByCommitSet.get(endCommit) || [];
- const row = this._buildComparisonRow(testGroup, startCommit, startResults, endCommit, endResults);
- if (!row)
- continue;
- comparisonRows.push(row);
- }
- }
-
- groups.unshift({heading: '', rows: comparisonRows});
-
- return groups;
- }
-
- _buildRowGroupForCommitSet(testGroup, commitSet)
- {
- const rows = [new ResultsTableRow('Mean', commitSet)];
- const results = [];
-
- for (const request of testGroup.requestsForCommitSet(commitSet)) {
- const result = this._analysisResultsView.resultForBuildId(request.buildId());
- // Call result.commitSet() for each result since the set of revisions used in testing maybe different from requested ones.
- const row = new ResultsTableRow(1 + +request.order(), result ? result.commitSet() : null);
- rows.push(row);
- if (result) {
- row.setLink(result.build().url(), result.build().label());
- row.setResult(result);
- results.push(result);
- } else
- row.setLink(request.statusUrl(), request.statusLabel());
- }
-
- const aggregatedResult = MeasurementAdaptor.aggregateAnalysisResults(results);
- if (!isNaN(aggregatedResult.value))
- rows[0].setResult(aggregatedResult);
-
- return {heading: testGroup.labelForCommitSet(commitSet), rows, results};
- }
-
- _buildComparisonRow(testGroup, startCommit, startResults, endCommit, endResults)
- {
- const startConfig = testGroup.labelForCommitSet(startCommit);
- const endConfig = testGroup.labelForCommitSet(endCommit);
-
- const result = this._testGroup.compareTestResults(
- startResults.map((result) => result.value), endResults.map((result) => result.value));
- if (result.changeType == null)
- return null;
-
- const row = new ResultsTableRow(`${startConfig} to ${endConfig}`, null);
- const element = ComponentBase.createElement;
- row.setLabelForWholeRow(element('span',
- {class: 'results-label ' + result.status}, `${endConfig} is ${result.fullLabel} than ${startConfig}`));
- return row;
- }
-
- static cssTemplate()
- {
- return super.cssTemplate() + `
- .results-label {
- padding: 0.1rem;
- width: 100%;
- height: 100%;
- }
-
- th {
- vertical-align: top;
- }
-
- .failed {
- color: rgb(128, 51, 128);
- }
- .unchanged {
- color: rgb(128, 128, 128);
- }
- .worse {
- color: rgb(255, 102, 102);
- font-weight: bold;
- }
- .better {
- color: rgb(102, 102, 255);
- font-weight: bold;
- }
- `;
- }
-}
-
-ComponentBase.defineElement('test-group-results-table', TestGroupResultsTable);
Added: trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-viewer.js (0 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-viewer.js (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-viewer.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -0,0 +1,298 @@
+
+class TestGroupResultsViewer extends ComponentBase {
+ constructor()
+ {
+ super('test-group-results-table');
+ this._analysisResults = null;
+ this._testGroup = null;
+ this._startPoint = null;
+ this._endPoint = null;
+ this._currentMetric = null;
+ this._expandedTests = new Set;
+ this._barGraphCellMap = new Map;
+ this._renderResultsTableLazily = new LazilyEvaluatedFunction(this._renderResultsTable.bind(this));
+ this._renderCurrentMetricsLazily = new LazilyEvaluatedFunction(this._renderCurrentMetrics.bind(this));
+ }
+
+ setTestGroup(currentTestGroup)
+ {
+ this._testGroup = currentTestGroup;
+ this.enqueueToRender();
+ }
+
+ setAnalysisResults(analysisResults, metric)
+ {
+ this._analysisResults = analysisResults;
+ this._currentMetric = metric;
+ this.enqueueToRender();
+ }
+
+ render()
+ {
+ if (!this._testGroup || !this._analysisResults)
+ return;
+
+ this._renderResultsTableLazily.evaluate(this._testGroup, this._expandedTests, ...this._analysisResults.highestTests());
+ this._renderCurrentMetricsLazily.evaluate(this._currentMetric);
+ }
+
+ _renderResultsTable(testGroup, expandedTests, ...tests)
+ {
+ let maxDepth = 0;
+ for (const test of expandedTests)
+ maxDepth = Math.max(maxDepth, test.path().length);
+
+ const element = ComponentBase.createElement;
+ this.renderReplace(this.content('results'), [
+ element('thead', [
+ element('tr', [
+ element('th', {colspan: maxDepth + 1}, 'Test'),
+ element('th', {class: 'metric-direction'}, ''),
+ element('th', {colspan: 2}, 'Results'),
+ element('th', 'Averages'),
+ element('th', 'Comparison'),
+ ]),
+ ]),
+ tests.map((test) => this._buildRowsForTest(testGroup, expandedTests, test, [], maxDepth, 0))]);
+ }
+
+ _buildRowsForTest(testGroup, expandedTests, test, sharedPath, maxDepth, depth)
+ {
+ const element = ComponentBase.createElement;
+ const rows = element('tbody', test.metrics().map((metric) => this._buildRowForMetric(testGroup, metric, sharedPath, maxDepth, depth)));
+
+ if (expandedTests.has(test)) {
+ return [rows, test.childTests().map((childTest) => {
+ return this._buildRowsForTest(testGroup, expandedTests, childTest, test.path(), maxDepth, depth + 1);
+ })];
+ }
+
+ if (test.childTests().length) {
+ const link = ComponentBase.createLink;
+ return [rows, element('tr', {class: 'breakdown'}, [
+ element('td', {colspan: maxDepth + 1}, link('(Breakdown)', () => {
+ this._expandedTests = new Set([...expandedTests, test]);
+ this.enqueueToRender();
+ })),
+ element('td', {colspan: 3}),
+ ])];
+ }
+
+ return rows;
+ }
+
+ _buildRowForMetric(testGroup, metric, sharedPath, maxDepth, depth)
+ {
+ const commitSets = testGroup.requestedCommitSets();
+ const valueMap = this._buildValueMap(testGroup, this._analysisResults.viewForMetric(metric));
+
+ const formatter = metric.makeFormatter(4);
+ const deltaFormatter = metric.makeFormatter(2, false);
+ const formatValue = (value, interval) => {
+ const delta = interval ? (interval[1] - interval[0]) / 2 : null;
+ return value == null || isNaN(value) ? '-' : `${formatter(value)} \u00b1 ${deltaFormatter(delta)}`;
+ }
+
+ const barGroup = new BarGraphGroup();
+ const barCells = [];
+ const createConfigurationRow = (commitSet, previousCommitSet, barColor, meanIndicatorColor) => {
+ const entry = valueMap.get(commitSet);
+ const previousEntry = valueMap.get(previousCommitSet);
+
+ const comparison = entry && previousEntry ? testGroup.compareTestResults(metric, previousEntry.filteredValues, entry.filteredValues) : null;
+ const valueLabels = entry.measurements.map((measurement) => measurement ? formatValue(measurement.value, measurement.interval) : '-');
+
+ const barCell = element('td', {class: 'plot-bar'},
+ element('div', barGroup.addBar(entry.allValues, valueLabels, entry.mean, entry.interval, barColor, meanIndicatorColor)));
+ barCell.expandedHeight = +valueLabels.length + 'rem';
+ barCells.push(barCell);
+
+ const significance = comparison && comparison.isStatisticallySignificant ? 'significant' : 'negligible';
+ const changeType = comparison ? comparison.changeType : null;
+ return [
+ element('th', testGroup.labelForCommitSet(commitSet)),
+ barCell,
+ element('td', formatValue(entry.mean, entry.interval)),
+ element('td', {class: `comparison ${changeType} ${significance}`}, comparison ? comparison.fullLabel : ''),
+ ];
+ }
+
+ this._barGraphCellMap.set(metric, {barGroup, barCells});
+
+ const rowspan = commitSets.length;
+ const element = ComponentBase.createElement;
+ const link = ComponentBase.createLink;
+ const metricName = metric.test().metrics().length == 1 ? metric.test().relativeName(sharedPath) : metric.relativeName(sharedPath);
+ const _onclick_ = this.createEventHandler((event) => {
+ if (this._currentMetric == metric) {
+ if (event.target.localName == 'bar-graph')
+ return;
+ this._currentMetric = null;
+ } else
+ this._currentMetric = metric;
+ this.enqueueToRender();
+ });
+ return [
+ element('tr', {onclick}, [
+ this._buildEmptyCells(depth, rowspan),
+ element('th', {colspan: maxDepth - depth + 1, rowspan}, link(metricName, onclick)),
+ element('td', {class: 'metric-direction', rowspan}, metric.isSmallerBetter() ? '\u21A4' : '\u21A6'),
+ createConfigurationRow(commitSets[0], null, '#ddd', '#333')
+ ]),
+ commitSets.slice(1).map((commitSet, setIndex) => {
+ return element('tr', {onclick},
+ createConfigurationRow(commitSet, commitSets[setIndex], '#aaa', '#000'));
+ })
+ ];
+ }
+
+ _buildValueMap(testGroup, resultsView)
+ {
+ const commitSets = testGroup.requestedCommitSets();
+ const map = new Map;
+ for (const commitSet of commitSets) {
+ const requests = testGroup.requestsForCommitSet(commitSet);
+ const measurements = requests.map((request) => resultsView.resultForRequest(request));
+ const filteredValues = measurements.filter((result) => !!result).map((measurement) => measurement.value);
+ const allValues = measurements.map((result) => result != null ? result.value : NaN);
+ const interval = Statistics.confidenceInterval(filteredValues);
+ map.set(commitSet, {requests, measurements, filteredValues, allValues, mean: Statistics.mean(filteredValues), interval});
+ }
+ return map;
+ }
+
+ _buildEmptyCells(count, rowspan)
+ {
+ const element = ComponentBase.createElement;
+ const emptyCells = [];
+ for (let i = 0; i < count; i++)
+ emptyCells.push(element('td', {rowspan}, ''));
+ return emptyCells;
+ }
+
+ _renderCurrentMetrics(currentMetric)
+ {
+ for (const entry of this._barGraphCellMap.values()) {
+ for (const cell of entry.barCells) {
+ cell.style.height = null;
+ cell.parentNode.className = null;
+ }
+ entry.barGroup.setShowLabels(false);
+ }
+
+ const entry = this._barGraphCellMap.get(currentMetric);
+ if (entry) {
+ for (const cell of entry.barCells) {
+ cell.style.height = cell.expandedHeight;
+ cell.parentNode.className = 'selected';
+ }
+ entry.barGroup.setShowLabels(true);
+ }
+ }
+
+ static htmlTemplate()
+ {
+ return `<table id="results"></table>`;
+ }
+
+ static cssTemplate()
+ {
+ return `
+ table {
+ border-collapse: collapse;
+ margin: 0;
+ padding: 0;
+ }
+ td, th {
+ border: none;
+ padding: 0;
+ margin: 0;
+ white-space: nowrap;
+ }
+ td:not(.metric-direction),
+ th:not(.metric-direction) {
+ padding: 0.1rem 0.5rem;
+ }
+ td:not(.metric-direction) {
+ min-width: 2rem;
+ }
+ td.metric-direction {
+ font-size: large;
+ }
+ bar-graph {
+ width: 7rem;
+ height: 1rem;
+ }
+ th {
+ font-weight: inherit;
+ }
+ thead th {
+ font-weight: inherit;
+ color: #c93;
+ }
+
+ tr.selected > td,
+ tr.selected > th {
+ background: rgba(204, 153, 51, 0.05);
+ }
+
+ tr:first-child > td,
+ tr:first-child > th {
+ border-top: solid 1px #eee;
+ }
+
+ tbody th {
+ text-align: left;
+ }
+ tbody th,
+ tbody td {
+ cursor: pointer;
+ }
+ a {
+ color: inherit;
+ text-decoration: inherit;
+ }
+ bar-graph {
+ width: 100%;
+ height: 100%;
+ }
+ td.plot-bar {
+ position: relative;
+ min-width: 7rem;
+ }
+ td.plot-bar > * {
+ display: block;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ }
+ .comparison {
+ text-align: left;
+ }
+ .negligible {
+ color: #999;
+ }
+ .significant.worse {
+ color: #c33;
+ }
+ .significant.better {
+ color: #33c;
+ }
+ tr.breakdown td {
+ padding: 0;
+ font-size: small;
+ text-align: center;
+ }
+ tr.breakdown a {
+ display: inline-block;
+ text-decoration: none;
+ color: #999;
+ margin-bottom: 0.2rem;
+ }
+ `;
+ }
+}
+
+ComponentBase.defineElement('test-group-results-viewer', TestGroupResultsViewer);
Added: trunk/Websites/perf.webkit.org/public/v3/components/test-group-revision-table.js (0 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/components/test-group-revision-table.js (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/components/test-group-revision-table.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -0,0 +1,203 @@
+
+class TestGroupRevisionTable extends ComponentBase {
+ constructor()
+ {
+ super('test-group-revision-table');
+ this._testGroup = null;
+ this._analysisResults = null;
+ this._renderTableLazily = new LazilyEvaluatedFunction(this._renderTable.bind(this));
+ }
+
+ setTestGroup(testGroup)
+ {
+ this._testGroup = testGroup;
+ this.enqueueToRender();
+ }
+
+ setAnalysisResults(analysisResults)
+ {
+ this._analysisResults = analysisResults;
+ this.enqueueToRender();
+ }
+
+ render()
+ {
+ this._renderTableLazily.evaluate(this._testGroup, this._analysisResults);
+ }
+
+ _renderTable(testGroup, analysisResults)
+ {
+ if (!testGroup)
+ return;
+
+ const commitSets = testGroup.requestedCommitSets();
+
+ const requestedRepositorySet = new Set;
+ const additionalRepositorySet = new Set;
+ let hasCustomRoots = false;
+ for (const commitSet of commitSets) {
+ if (commitSet.customRoots().length)
+ hasCustomRoots = true;
+ for (const repository of commitSet.repositories())
+ requestedRepositorySet.add(repository);
+ }
+
+ const rowEntries = [];
+ commitSets.forEach((commitSet, commitSetIndex) => {
+ const setLabel = testGroup.labelForCommitSet(commitSet);
+ const buildRequests = testGroup.requestsForCommitSet(commitSet);
+ buildRequests.forEach((request, i) => {
+ const resultCommitSet = analysisResults ? analysisResults.commitSetForRequest(request) : null;
+ if (resultCommitSet) {
+ for (const repository of resultCommitSet.repositories()) {
+ if (!requestedRepositorySet.has(repository))
+ additionalRepositorySet.add(repository);
+ }
+ }
+ rowEntries.push({
+ groupHeader: !i ? setLabel : null,
+ groupRowCount: buildRequests.length,
+ label: (1 + +request.order()).toString(),
+ commitSet: resultCommitSet || commitSet,
+ customRoots: commitSet.customRoots(), // FIXME: resultCommitSet should also report roots that got installed.
+ rowCountByRepository: new Map,
+ repositoriesToSkip: new Set,
+ customRootsRowCount: 1,
+ request,
+ });
+ });
+ });
+
+ this._mergeCellsWithSameCommitsAcrossRows(rowEntries);
+
+ const requestedRepositoryList = Repository.sortByNamePreferringOnesWithURL([...requestedRepositorySet]);
+ const additionalRepositoryList = Repository.sortByNamePreferringOnesWithURL([...additionalRepositorySet]);
+
+ const element = ComponentBase.createElement;
+ const link = ComponentBase.createLink;
+ this.renderReplace(this.content('revision-table'), [
+ element('thead', [
+ element('th', 'Configuration'),
+ requestedRepositoryList.map((repository) => element('th', repository.name())),
+ hasCustomRoots ? element('th', 'Roots') : [],
+ element('th', 'Order'),
+ element('th', 'Status'),
+ additionalRepositoryList.map((repository) => element('th', repository.name())),
+ ]),
+ element('tbody', rowEntries.map((entry) => {
+ const request = entry.request;
+ return element('tr', [
+ entry.groupHeader ? element('td', {rowspan: entry.groupRowCount}, entry.groupHeader) : [],
+ requestedRepositoryList.map((repository) => this._buildCommitCell(entry, repository)),
+ hasCustomRoots ? this._buildCustomRootsCell(entry) : [],
+ element('td', 1 + +request.order()),
+ element('td', request.statusUrl() ? link(request.statusLabel(), request.statusUrl()) : request.statusLabel()),
+ additionalRepositoryList.map((repository) => this._buildCommitCell(entry, repository)),
+ ]);
+ }))]);
+ }
+
+ _buildCommitCell(entry, repository)
+ {
+ if (entry.repositoriesToSkip.has(repository))
+ return [];
+ const commit = entry.commitSet.commitForRepository(repository);
+ return ComponentBase.createElement('td', {rowspan: entry.rowCountByRepository.get(repository)}, commit ? commit.label() : '');
+ }
+
+ _buildCustomRootsCell(entry)
+ {
+ const rowspan = entry.customRootsRowCount;
+ if (!rowspan)
+ return [];
+ const element = ComponentBase.createElement;
+ const link = ComponentBase.createLink;
+
+ if (!entry.customRoots.length)
+ return element('td', {class: 'roots', rowspan});
+
+ return element('td', {class: 'roots', rowspan},
+ element('ul', entry.customRoots.map((customRoot) => {
+ if (customRoot.deletedAt())
+ return [customRoot.label(), ' ', element('span', {class: 'purged'}, '(Purged)')];
+ return link(customRoot.label(), customRoot.filename(), customRoot.url());
+ }).map((content) => element('li', content))));
+ }
+
+ _mergeCellsWithSameCommitsAcrossRows(rowEntries)
+ {
+ for (let rowIndex = 0; rowIndex < rowEntries.length; rowIndex++) {
+ const entry = rowEntries[rowIndex];
+ for (const repository of entry.commitSet.repositories()) {
+ if (entry.repositoriesToSkip.has(repository))
+ continue;
+ const commit = entry.commitSet.commitForRepository(repository);
+ let rowCount = 1;
+ for (let otherRowIndex = rowIndex + 1; otherRowIndex < rowEntries.length; otherRowIndex++) {
+ const otherEntry = rowEntries[otherRowIndex];
+ const otherCommit = otherEntry.commitSet.commitForRepository(repository);
+ if (commit != otherCommit)
+ break;
+ otherEntry.repositoriesToSkip.add(repository);
+ rowCount++;
+ }
+ entry.rowCountByRepository.set(repository, rowCount);
+ }
+ if (entry.customRootsRowCount) {
+ let rowCount = 1;
+ for (let otherRowIndex = rowIndex + 1; otherRowIndex < rowEntries.length; otherRowIndex++) {
+ const otherEntry = rowEntries[otherRowIndex];
+ if (!CommitSet.areCustomRootsEqual(entry.customRoots, otherEntry.customRoots))
+ break;
+ otherEntry.customRootsRowCount = 0;
+ rowCount++;
+ }
+ entry.customRootsRowCount = rowCount;
+ }
+ }
+ }
+
+ static htmlTemplate()
+ {
+ return `<table id="revision-table"></table>`;
+ }
+
+ static cssTemplate()
+ {
+ return `
+ table {
+ border-collapse: collapse;
+ }
+ th, td {
+ text-align: center;
+ padding: 0.2rem 0.8rem;
+ }
+ tbody th,
+ tbody td {
+ border-top: solid 1px #eee;
+ border-bottom: solid 1px #eee;
+ }
+ th {
+ font-weight: inherit;
+ }
+ .roots {
+ max-width: 20rem;
+ }
+ .purged {
+ color: #999;
+ }
+ .roots ul,
+ .roots li {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+ .roots li {
+ margin-top: 0.4rem;
+ margin-bottom: 0.4rem;
+ }
+ `;
+ }
+}
+
+ComponentBase.defineElement('test-group-revision-table', TestGroupRevisionTable);
Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/index.html 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html 2017-04-21 20:20:05 UTC (rev 215633)
@@ -84,7 +84,8 @@
<script src=""
<script src=""
<script src=""
- <script src=""
+ <script src=""
+ <script src=""
<script src=""
<script src=""
<script src=""
Modified: trunk/Websites/perf.webkit.org/public/v3/models/analysis-results.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/models/analysis-results.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/models/analysis-results.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -3,9 +3,11 @@
constructor()
{
this._metricToBuildMap = {};
+ this._metricIds = [];
+ this._lazilyComputedHighestTests = new LazilyEvaluatedFunction(this._computeHighestTests);
}
- find(buildId, metricId)
+ findResult(buildId, metricId)
{
const map = this._metricToBuildMap[metricId];
if (!map)
@@ -13,17 +15,37 @@
return map[buildId];
}
+ highestTests() { return this._lazilyComputedHighestTests.evaluate(this._metricIds); }
+
+ _computeHighestTests(metricIds)
+ {
+ const testsInResults = new Set(metricIds.map((metricId) => Metric.findById(metricId).test()));
+ return [...testsInResults].filter((test) => !testsInResults.has(test.parentTest()));
+ }
+
add(measurement)
{
console.assert(measurement.configType == 'current');
const metricId = measurement.metricId;
- if (!(metricId in this._metricToBuildMap))
+ if (!(metricId in this._metricToBuildMap)) {
this._metricToBuildMap[metricId] = {};
- var map = this._metricToBuildMap[metricId];
+ this._metricIds = Object.keys(this._metricToBuildMap);
+ }
+ const map = this._metricToBuildMap[metricId];
console.assert(!map[measurement.buildId]);
map[measurement.buildId] = measurement;
}
+ commitSetForRequest(buildRequest)
+ {
+ if (!this._metricIds.length)
+ return null;
+ const result = this.findResult(buildRequest.buildId(), this._metricIds[0]);
+ if (!result)
+ return null;
+ return result.commitSet();
+ }
+
viewForMetric(metric)
{
console.assert(metric instanceof Metric);
@@ -60,8 +82,8 @@
metric() { return this._metric; }
- resultForBuildId(buildId)
+ resultForRequest(buildRequest)
{
- return this._results.find(buildId, this._metric.id());
+ return this._results.findResult(buildRequest.buildId(), this._metric.id());
}
}
Modified: trunk/Websites/perf.webkit.org/public/v3/models/metric.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/models/metric.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/models/metric.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -48,28 +48,35 @@
fullName() { return this._test.fullName() + ' : ' + this.label(); }
- label()
+ relativeName(path)
{
- var suffix = '';
+ const relativeTestName = this._test.relativeName(path);
+ if (relativeTestName == null)
+ return this.label();
+ return relativeTestName + ' : ' + this.label();
+ }
+
+ aggregatorLabel()
+ {
switch (this._aggregatorName) {
- case null:
- break;
case 'Arithmetic':
- suffix = ' : Arithmetic mean';
- break;
+ return 'Arithmetic mean';
case 'Geometric':
- suffix = ' : Geometric mean';
- break;
+ return 'Geometric mean';
case 'Harmonic':
- suffix = ' : Harmonic mean';
- break;
+ return 'Harmonic mean';
case 'Total':
- default:
- suffix = ' : ' + this._aggregatorName;
+ return 'Total';
}
- return this.name() + suffix;
+ return null;
}
+ label()
+ {
+ const aggregatorLabel = this.aggregatorLabel();
+ return this.name() + (aggregatorLabel ? ` : ${aggregatorLabel}` : '');
+ }
+
unit() { return this._unit; }
isSmallerBetter()
@@ -80,7 +87,7 @@
makeFormatter(sigFig, alwaysShowSign) { return Metric.makeFormatter(this.unit(), sigFig, alwaysShowSign); }
- static makeFormatter(unit, sigFig = 2, alwaysShowSign)
+ static makeFormatter(unit, sigFig = 2, alwaysShowSign = false)
{
let isMiliseconds = false;
if (unit == 'ms') {
Modified: trunk/Websites/perf.webkit.org/public/v3/models/test-group.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/models/test-group.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/models/test-group.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -109,14 +109,12 @@
return this._buildRequests.some(function (request) { return request.isPending(); });
}
- compareTestResults(beforeValues, afterValues)
+ compareTestResults(metric, beforeValues, afterValues)
{
+ console.assert(metric);
const beforeMean = Statistics.sum(beforeValues) / beforeValues.length;
const afterMean = Statistics.sum(afterValues) / afterValues.length;
- var metric = AnalysisTask.findById(this._taskId).metric();
- console.assert(metric);
-
var result = {changeType: null, status: 'failed', label: 'Failed', fullLabel: 'Failed', isStatisticallySignificant: false};
var hasCompleted = this.hasFinished();
Modified: trunk/Websites/perf.webkit.org/public/v3/models/test.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/models/test.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/models/test.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -8,6 +8,7 @@
this._parentId = object.parentId;
this._childTests = [];
this._metrics = [];
+ this._computePathLazily = new LazilyEvaluatedFunction(this._computePath.bind(this));
if (!this._parentId)
this.ensureNamedStaticMap('topLevelTests')[id] = this;
@@ -43,10 +44,12 @@
parentTest() { return Test.findById(this._parentId); }
- path()
+ path() { return this._computePathLazily.evaluate(); }
+
+ _computePath()
{
- var path = [];
- var currentTest = this;
+ const path = [];
+ let currentTest = this;
while (currentTest) {
path.unshift(currentTest);
currentTest = currentTest.parentTest();
@@ -54,8 +57,24 @@
return path;
}
- fullName() { return this.path().map(function (test) { return test.label(); }).join(' \u220B '); }
+ fullName() { return this.path().map((test) => test.label()).join(' \u220B '); }
+ relativeName(sharedPath)
+ {
+ const path = this.path();
+ const partialName = (index) => path.slice(index).map((test) => test.label()).join(' \u220B ');
+ if (!sharedPath || !sharedPath.length)
+ return partialName(0);
+ let i = 0;
+ for (; i < path.length && i < sharedPath.length; i++) {
+ if (sharedPath[i] != path[i])
+ return partialName(i);
+ }
+ if (i < path.length)
+ return partialName(i);
+ return null;
+ }
+
onlyContainsSingleMetric() { return !this.childTests().length && this._metrics.length == 1; }
childTests()
Modified: trunk/Websites/perf.webkit.org/public/v3/models/uploaded-file.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/models/uploaded-file.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/models/uploaded-file.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -5,6 +5,7 @@
{
super(id, object);
this._createdAt = new Date(object.createdAt);
+ this._deletedAt = new Date(object.deletedAt);
this._filename = object.filename;
this._author = object.author;
this._size = object.size;
@@ -13,9 +14,12 @@
}
createdAt() { return this._createdAt; }
+ deletedAt() { return this._deletedAt; }
filename() { return this._filename; }
author() { return this._author; }
size() { return this._size; }
+ label() { return this.filename(); }
+ url() { return `/api/uploaded-file/${this.id()}`; }
static uploadFile(file, uploadProgressCallback = null)
{
Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -161,13 +161,15 @@
this._testGroups = testGroups;
this._currentTestGroup = currentTestGroup;
this._showHiddenGroups = showHiddenGroups;
- this.part('results-table').setTestGroup(currentTestGroup);
+ this.part('revision-table').setTestGroup(currentTestGroup);
+ this.part('results-viewer').setTestGroup(currentTestGroup);
this.enqueueToRender();
}
- setAnalysisResultsView(analysisResultsView)
+ setAnalysisResults(analysisResults, metric)
{
- this.part('results-table').setAnalysisResultsView(analysisResultsView);
+ this.part('revision-table').setAnalysisResults(analysisResults);
+ this.part('results-viewer').setAnalysisResults(analysisResults, metric);
this.enqueueToRender();
}
@@ -177,8 +179,6 @@
this._renderTestGroupVisibilityLazily.evaluate(...this._testGroups.map((group) => group.isHidden() ? 'hidden' : 'visible'));
this._renderTestGroupNamesLazily.evaluate(...this._testGroups.map((group) => group.label()));
this._renderCurrentTestGroup(this._currentTestGroup);
- this.part('results-table').enqueueToRender();
- this.part('retry-form').enqueueToRender();
}
_renderTestGroups(showHiddenGroups, ...testGroups)
@@ -239,7 +239,8 @@
return `
<ul id="test-group-list"></ul>
<div id="test-group-details">
- <test-group-results-table id="results-table"></test-group-results-table>
+ <test-group-results-viewer id="results-viewer"></test-group-results-viewer>
+ <test-group-revision-table id="revision-table"></test-group-revision-table>
<test-group-form id="retry-form">Retry</test-group-form>
<button id="hide-button">Hide</button>
<span id="pending-request-cancel-warning">(cancels pending requests)</span>
@@ -251,8 +252,17 @@
return `
:host {
display: flex !important;
+ font-size: 0.9rem;
}
+ #new-container {
+ display: flex;
+ }
+
+ #new-container test-group-revision-table {
+ margin-left: 2rem;
+ }
+
#test-group-list {
margin: 0;
padding: 0.2rem 0;
@@ -490,7 +500,7 @@
return false;
const view = this._analysisResults.viewForMetric(this._metric);
- this.part('group-pane').setAnalysisResultsView(view);
+ this.part('group-pane').setAnalysisResults(this._analysisResults, this._metric);
this.part('results-pane').setAnalysisResultsView(view);
return true;
Modified: trunk/Websites/perf.webkit.org/public/v3/pages/create-analysis-task-page.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/public/v3/pages/create-analysis-task-page.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/create-analysis-task-page.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -45,9 +45,7 @@
const commitSets = configurator.commitSets();
TestGroup.createWithTask(taskName, platform, tests[0], testGroupName, iterationCount, commitSets).then((task) => {
- console.log('yay?', task);
const url = ""
- console.log('moving to ' + url);
location.href = ""
}, (error) => {
alert('Failed to create a new test group: ' + error);
Modified: trunk/Websites/perf.webkit.org/tools/js/v3-models.js (215632 => 215633)
--- trunk/Websites/perf.webkit.org/tools/js/v3-models.js 2017-04-21 20:07:07 UTC (rev 215632)
+++ trunk/Websites/perf.webkit.org/tools/js/v3-models.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -35,5 +35,6 @@
importFromV3('privileged-api.js', 'PrivilegedAPI');
importFromV3('instrumentation.js', 'Instrumentation');
+importFromV3('lazily-evaluated-function.js', 'LazilyEvaluatedFunction');
global.Statistics = require('../../public/shared/statistics.js');
Added: trunk/Websites/perf.webkit.org/unit-tests/test-model-tests.js (0 => 215633)
--- trunk/Websites/perf.webkit.org/unit-tests/test-model-tests.js (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/test-model-tests.js 2017-04-21 20:20:05 UTC (rev 215633)
@@ -0,0 +1,179 @@
+'use strict';
+
+const assert = require('assert');
+require('../tools/js/v3-models.js');
+
+describe('Test', function () {
+ beforeEach(() => {
+ Test.clearStaticMap();
+ });
+
+ describe('topLevelTests', () => {
+ it('should contain the tests without a parent test', () => {
+ assert.deepEqual(Test.topLevelTests(), []);
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ assert.deepEqual(Test.topLevelTests(), [someTest]);
+ });
+
+ it('should not contain the tests with a parent test', () => {
+ assert.deepEqual(Test.topLevelTests(), []);
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const childTest = new Test(2, {id: 2, name: 'child test', parentId: 1});
+ assert.equal(childTest.parentTest(), someTest);
+ assert.deepEqual(Test.topLevelTests(), [someTest]);
+ });
+ });
+
+ describe('childTests', () => {
+ it('must return the list of the child tests', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const childTest = new Test(2, {id: 2, name: 'child test', parentId: 1});
+ const otherChildTest = new Test(3, {id: 3, name: 'other child test', parentId: 1});
+ assert.deepEqual(someTest.childTests(), [childTest, otherChildTest]);
+ });
+
+ it('must not return a list that contains a grand child test', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const childTest = new Test(2, {id: 2, name: 'child test', parentId: 1});
+ const grandChildTest = new Test(3, {id: 3, name: 'grand child test', parentId: 2});
+ assert.deepEqual(someTest.childTests(), [childTest]);
+ assert.deepEqual(childTest.childTests(), [grandChildTest]);
+ });
+ });
+
+ describe('parentTest', () => {
+ it('must return null for a test without a parent test', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ assert.equal(someTest.parentTest(), null);
+ });
+
+ it('must return the parent test', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const childTest = new Test(2, {id: 2, name: 'child test', parentId: 1});
+ const grandChildTest = new Test(3, {id: 3, name: 'grand child test', parentId: 2});
+ assert.equal(childTest.parentTest(), someTest);
+ assert.equal(grandChildTest.parentTest(), childTest);
+ });
+ });
+
+ describe('path', () => {
+ it('must return an array containing itself for a test without a parent', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ assert.deepEqual(someTest.path(), [someTest]);
+ });
+
+ it('must return an array containing every ancestor and itself for a test with a parent', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const childTest = new Test(2, {id: 2, name: 'child test', parentId: 1});
+ const grandChildTest = new Test(3, {id: 3, name: 'grand child test', parentId: 2});
+ assert.deepEqual(childTest.path(), [someTest, childTest]);
+ assert.deepEqual(grandChildTest.path(), [someTest, childTest, grandChildTest]);
+ });
+ });
+
+ describe('findByPath', () => {
+ it('must return null when there are no tests', () => {
+ assert.equal(Test.findByPath(['some test']), null);
+ });
+
+ it('must be able to find top-level tests', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const otherTest = new Test(2, {id: 2, name: 'other test', parentId: null});
+ assert.equal(Test.findByPath(['some test']), someTest);
+ });
+
+ it('must be able to find second-level tests', () => {
+ const parent = new Test(1, {id: 1, name: 'parent', parentId: null});
+ const someChild = new Test(2, {id: 2, name: 'some', parentId: 1});
+ const otherChild = new Test(3, {id: 3, name: 'other', parentId: 1});
+ assert.equal(Test.findByPath(['some']), null);
+ assert.equal(Test.findByPath(['other']), null);
+ assert.equal(Test.findByPath(['parent', 'some']), someChild);
+ assert.equal(Test.findByPath(['parent', 'other']), otherChild);
+ });
+
+ it('must be able to find third-level tests', () => {
+ const parent = new Test(1, {id: 1, name: 'parent', parentId: null});
+ const child = new Test(2, {id: 2, name: 'child', parentId: 1});
+ const grandChild = new Test(3, {id: 3, name: 'grandChild', parentId: 2});
+ assert.equal(Test.findByPath(['child']), null);
+ assert.equal(Test.findByPath(['grandChild']), null);
+ assert.equal(Test.findByPath(['child', 'grandChild']), null);
+ assert.equal(Test.findByPath(['parent', 'grandChild']), null);
+ assert.equal(Test.findByPath(['parent', 'child']), child);
+ assert.equal(Test.findByPath(['parent', 'child', 'grandChild']), grandChild);
+ });
+ });
+
+ describe('fullName', () => {
+ it('must return the name of a top-level test', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ assert.equal(someTest.fullName(), 'some test');
+ });
+
+ it('must return the name of a second-level test and the name of its parent concatenated with \u220B', () => {
+ const parent = new Test(1, {id: 1, name: 'parent', parentId: null});
+ const child = new Test(2, {id: 2, name: 'child', parentId: 1});
+ assert.equal(child.fullName(), 'parent \u220B child');
+ });
+
+ it('must return the name of a third-level test concatenated with the names of its ancestor tests with \u220B', () => {
+ const parent = new Test(1, {id: 1, name: 'parent', parentId: null});
+ const child = new Test(2, {id: 2, name: 'child', parentId: 1});
+ const grandChild = new Test(3, {id: 3, name: 'grandChild', parentId: 2});
+ assert.equal(grandChild.fullName(), 'parent \u220B child \u220B grandChild');
+ });
+ });
+
+ describe('relativeName', () => {
+ it('must return the full name of a test when the shared path is null', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const childTest = new Test(2, {id: 2, name: 'child test', parentId: 1});
+ assert.equal(someTest.relativeName(null), someTest.fullName());
+ assert.equal(childTest.relativeName(null), childTest.fullName());
+ });
+
+ it('must return the full name of a test when the shared path is empty', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const childTest = new Test(2, {id: 2, name: 'child test', parentId: 1});
+ assert.equal(someTest.relativeName([]), someTest.fullName());
+ assert.equal(childTest.relativeName([]), childTest.fullName());
+ });
+
+ it('must return null when the shared path is identical to the path of the test', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const childTest = new Test(2, {id: 2, name: 'child test', parentId: 1});
+ assert.equal(someTest.relativeName(someTest.path()), null);
+ assert.equal(childTest.relativeName(childTest.path()), null);
+ });
+
+ it('must return the full name of a test when the first part in the path differs', () => {
+ const someTest = new Test(1, {id: 1, name: 'some test', parentId: null});
+ const otherTest = new Test(2, {id: 1, name: 'some test', parentId: null});
+ const childTest = new Test(3, {id: 3, name: 'child test', parentId: 1});
+ assert.equal(someTest.relativeName([otherTest]), someTest.fullName());
+ assert.equal(childTest.relativeName([otherTest]), childTest.fullName());
+ });
+
+ it('must return the name relative to its parent when the shared path is of the parent', () => {
+ const parent = new Test(1, {id: 1, name: 'parent', parentId: null});
+ const child = new Test(2, {id: 2, name: 'child', parentId: 1});
+ assert.equal(child.relativeName([parent]), 'child');
+ });
+
+ it('must return the name relative to its grand parent when the shared path is of the grand parent', () => {
+ const grandParent = new Test(1, {id: 1, name: 'grandParent', parentId: null});
+ const parent = new Test(2, {id: 2, name: 'parent', parentId: 1});
+ const self = new Test(3, {id: 3, name: 'self', parentId: 2});
+ assert.equal(self.relativeName([grandParent]), 'parent \u220B self');
+ });
+
+ it('must return the name relative to its parent when the shared path is of the parent even if it had a grandparent', () => {
+ const grandParent = new Test(1, {id: 1, name: 'grandParent', parentId: null});
+ const parent = new Test(2, {id: 2, name: 'parent', parentId: 1});
+ const self = new Test(3, {id: 3, name: 'self', parentId: 2});
+ assert.equal(self.relativeName([grandParent, parent]), 'self');
+ });
+ });
+
+});
\ No newline at end of file