blob: d45155e6d108b275ee3f220ac3cb801187d9e033 [file] [log] [blame]
/*
* Copyright (C) 2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
ResultsDashboard = Utilities.createClass(
function(version, options, testData)
{
this._iterationsSamplers = [];
this._options = options;
this._results = null;
this._version = version;
if (testData) {
this._iterationsSamplers = testData;
this._processData();
}
}, {
push: function(suitesSamplers)
{
this._iterationsSamplers.push(suitesSamplers);
},
_processData: function()
{
this._results = {};
this._results[Strings.json.results.iterations] = [];
var iterationsScores = [];
this._iterationsSamplers.forEach(function(iteration, index) {
var testsScores = [];
var testsLowerBoundScores = [];
var testsUpperBoundScores = [];
var result = {};
this._results[Strings.json.results.iterations][index] = result;
var suitesResult = {};
result[Strings.json.results.tests] = suitesResult;
for (var suiteName in iteration) {
var suiteData = iteration[suiteName];
var suiteResult = {};
suitesResult[suiteName] = suiteResult;
for (var testName in suiteData) {
if (!suiteData[testName][Strings.json.result])
this.calculateScore(suiteData[testName]);
suiteResult[testName] = suiteData[testName][Strings.json.result];
delete suiteData[testName][Strings.json.result];
testsScores.push(suiteResult[testName][Strings.json.score]);
testsLowerBoundScores.push(suiteResult[testName][Strings.json.scoreLowerBound]);
testsUpperBoundScores.push(suiteResult[testName][Strings.json.scoreUpperBound]);
}
}
result[Strings.json.score] = Statistics.geometricMean(testsScores);
result[Strings.json.scoreLowerBound] = Statistics.geometricMean(testsLowerBoundScores);
result[Strings.json.scoreUpperBound] = Statistics.geometricMean(testsUpperBoundScores);
iterationsScores.push(result[Strings.json.score]);
}, this);
this._results[Strings.json.version] = this._version;
this._results[Strings.json.score] = Statistics.sampleMean(iterationsScores.length, iterationsScores.reduce(function(a, b) { return a + b; }));
this._results[Strings.json.scoreLowerBound] = this._results[Strings.json.results.iterations][0][Strings.json.scoreLowerBound];
this._results[Strings.json.scoreUpperBound] = this._results[Strings.json.results.iterations][0][Strings.json.scoreUpperBound];
},
calculateScore: function(data)
{
var result = {};
data[Strings.json.result] = result;
var samples = data[Strings.json.samples];
function findRegression(series, profile) {
var minIndex = Math.round(.025 * series.length);
var maxIndex = Math.round(.975 * (series.length - 1));
var minComplexity = series.getFieldInDatum(minIndex, Strings.json.complexity);
var maxComplexity = series.getFieldInDatum(maxIndex, Strings.json.complexity);
if (Math.abs(maxComplexity - minComplexity) < 20 && maxIndex - minIndex < 20) {
minIndex = 0;
maxIndex = series.length - 1;
minComplexity = series.getFieldInDatum(minIndex, Strings.json.complexity);
maxComplexity = series.getFieldInDatum(maxIndex, Strings.json.complexity);
}
var complexityIndex = series.fieldMap[Strings.json.complexity];
var frameLengthIndex = series.fieldMap[Strings.json.frameLength];
var regressionOptions = { desiredFrameLength: 1000/60 };
if (profile)
regressionOptions.preferredProfile = profile;
return {
minComplexity: minComplexity,
maxComplexity: maxComplexity,
samples: series.slice(minIndex, maxIndex + 1),
regression: new Regression(
series.data,
function (data, i) { return data[i][complexityIndex]; },
function (data, i) { return data[i][frameLengthIndex]; },
minIndex, maxIndex, regressionOptions)
};
}
// Convert these samples into SampleData objects if needed
[Strings.json.complexity, Strings.json.controller].forEach(function(seriesName) {
var series = samples[seriesName];
if (series && !(series instanceof SampleData))
samples[seriesName] = new SampleData(series.fieldMap, series.data);
});
var isRampController = this._options["controller"] == "ramp";
var predominantProfile = "";
if (isRampController) {
var profiles = {};
data[Strings.json.controller].forEach(function(regression) {
if (regression[Strings.json.regressions.profile]) {
var profile = regression[Strings.json.regressions.profile];
profiles[profile] = (profiles[profile] || 0) + 1;
}
});
var maxProfileCount = 0;
for (var profile in profiles) {
if (profiles[profile] > maxProfileCount) {
predominantProfile = profile;
maxProfileCount = profiles[profile];
}
}
}
var regressionResult = findRegression(samples[Strings.json.complexity], predominantProfile);
var calculation = regressionResult.regression;
result[Strings.json.complexity] = {};
result[Strings.json.complexity][Strings.json.regressions.segment1] = [
[regressionResult.minComplexity, calculation.s1 + calculation.t1 * regressionResult.minComplexity],
[calculation.complexity, calculation.s1 + calculation.t1 * calculation.complexity]
];
result[Strings.json.complexity][Strings.json.regressions.segment2] = [
[calculation.complexity, calculation.s2 + calculation.t2 * calculation.complexity],
[regressionResult.maxComplexity, calculation.s2 + calculation.t2 * regressionResult.maxComplexity]
];
result[Strings.json.complexity][Strings.json.complexity] = calculation.complexity;
result[Strings.json.complexity][Strings.json.measurements.stdev] = Math.sqrt(calculation.error / samples[Strings.json.complexity].length);
if (isRampController) {
var timeComplexity = new Experiment;
data[Strings.json.controller].forEach(function(regression) {
timeComplexity.sample(regression[Strings.json.complexity]);
});
var experimentResult = {};
result[Strings.json.controller] = experimentResult;
experimentResult[Strings.json.score] = timeComplexity.mean();
experimentResult[Strings.json.measurements.average] = timeComplexity.mean();
experimentResult[Strings.json.measurements.stdev] = timeComplexity.standardDeviation();
experimentResult[Strings.json.measurements.percent] = timeComplexity.percentage();
const bootstrapIterations = 2500;
var bootstrapResult = Regression.bootstrap(regressionResult.samples.data, bootstrapIterations, function(resampleData) {
var complexityIndex = regressionResult.samples.fieldMap[Strings.json.complexity];
resampleData.sort(function(a, b) {
return a[complexityIndex] - b[complexityIndex];
});
var resample = new SampleData(regressionResult.samples.fieldMap, resampleData);
var bootstrapRegressionResult = findRegression(resample, predominantProfile);
return bootstrapRegressionResult.regression.complexity;
}, .8);
result[Strings.json.complexity][Strings.json.bootstrap] = bootstrapResult;
result[Strings.json.score] = bootstrapResult.median;
result[Strings.json.scoreLowerBound] = bootstrapResult.confidenceLow;
result[Strings.json.scoreUpperBound] = bootstrapResult.confidenceHigh;
} else {
var marks = data[Strings.json.marks];
var samplingStartIndex = 0, samplingEndIndex = -1;
if (Strings.json.samplingStartTimeOffset in marks)
samplingStartIndex = marks[Strings.json.samplingStartTimeOffset].index;
if (Strings.json.samplingEndTimeOffset in marks)
samplingEndIndex = marks[Strings.json.samplingEndTimeOffset].index;
var averageComplexity = new Experiment;
var averageFrameLength = new Experiment;
var controllerSamples = samples[Strings.json.controller];
controllerSamples.forEach(function (sample, i) {
if (i >= samplingStartIndex && (samplingEndIndex == -1 || i < samplingEndIndex)) {
averageComplexity.sample(controllerSamples.getFieldInDatum(sample, Strings.json.complexity));
var smoothedFrameLength = controllerSamples.getFieldInDatum(sample, Strings.json.smoothedFrameLength);
if (smoothedFrameLength && smoothedFrameLength != -1)
averageFrameLength.sample(smoothedFrameLength);
}
});
var experimentResult = {};
result[Strings.json.controller] = experimentResult;
experimentResult[Strings.json.measurements.average] = averageComplexity.mean();
experimentResult[Strings.json.measurements.concern] = averageComplexity.concern(Experiment.defaults.CONCERN);
experimentResult[Strings.json.measurements.stdev] = averageComplexity.standardDeviation();
experimentResult[Strings.json.measurements.percent] = averageComplexity.percentage();
experimentResult = {};
result[Strings.json.frameLength] = experimentResult;
experimentResult[Strings.json.measurements.average] = 1000 / averageFrameLength.mean();
experimentResult[Strings.json.measurements.concern] = averageFrameLength.concern(Experiment.defaults.CONCERN);
experimentResult[Strings.json.measurements.stdev] = averageFrameLength.standardDeviation();
experimentResult[Strings.json.measurements.percent] = averageFrameLength.percentage();
result[Strings.json.score] = averageComplexity.score(Experiment.defaults.CONCERN);
result[Strings.json.scoreLowerBound] = result[Strings.json.score] - averageFrameLength.standardDeviation();
result[Strings.json.scoreUpperBound] = result[Strings.json.score] + averageFrameLength.standardDeviation();
}
},
get data()
{
return this._iterationsSamplers;
},
get results()
{
if (this._results)
return this._results[Strings.json.results.iterations];
this._processData();
return this._results[Strings.json.results.iterations];
},
get options()
{
return this._options;
},
get version()
{
return this._version;
},
_getResultsProperty: function(property)
{
if (this._results)
return this._results[property];
this._processData();
return this._results[property];
},
get score()
{
return this._getResultsProperty(Strings.json.score);
},
get scoreLowerBound()
{
return this._getResultsProperty(Strings.json.scoreLowerBound);
},
get scoreUpperBound()
{
return this._getResultsProperty(Strings.json.scoreUpperBound);
}
});
ResultsTable = Utilities.createClass(
function(element, headers)
{
this.element = element;
this._headers = headers;
this._flattenedHeaders = [];
this._headers.forEach(function(header) {
if (header.disabled)
return;
if (header.children)
this._flattenedHeaders = this._flattenedHeaders.concat(header.children);
else
this._flattenedHeaders.push(header);
}, this);
this._flattenedHeaders = this._flattenedHeaders.filter(function (header) {
return !header.disabled;
});
this.clear();
}, {
clear: function()
{
this.element.textContent = "";
},
_addHeader: function()
{
var thead = Utilities.createElement("thead", {}, this.element);
var row = Utilities.createElement("tr", {}, thead);
this._headers.forEach(function (header) {
if (header.disabled)
return;
var th = Utilities.createElement("th", {}, row);
if (header.title != Strings.text.graph)
th.innerHTML = header.title;
if (header.children)
th.colSpan = header.children.length;
});
},
_addBody: function()
{
this.tbody = Utilities.createElement("tbody", {}, this.element);
},
_addEmptyRow: function()
{
var row = Utilities.createElement("tr", {}, this.tbody);
this._flattenedHeaders.forEach(function (header) {
return Utilities.createElement("td", { class: "suites-separator" }, row);
});
},
_addTest: function(testName, testResult, options)
{
var row = Utilities.createElement("tr", {}, this.tbody);
this._flattenedHeaders.forEach(function (header) {
var td = Utilities.createElement("td", {}, row);
if (header.text == Strings.text.testName) {
td.textContent = testName;
} else if (typeof header.text == "string") {
var data = testResult[header.text];
if (typeof data == "number")
data = data.toFixed(2);
td.innerHTML = data;
} else
td.innerHTML = header.text(testResult);
}, this);
},
_addIteration: function(iterationResult, iterationData, options)
{
var testsResults = iterationResult[Strings.json.results.tests];
for (var suiteName in testsResults) {
this._addEmptyRow();
var suiteResult = testsResults[suiteName];
var suiteData = iterationData[suiteName];
for (var testName in suiteResult)
this._addTest(testName, suiteResult[testName], options, suiteData[testName]);
}
},
showIterations: function(dashboard)
{
this.clear();
this._addHeader();
this._addBody();
var iterationsResults = dashboard.results;
iterationsResults.forEach(function(iterationResult, index) {
this._addIteration(iterationResult, dashboard.data[index], dashboard.options);
}, this);
}
});
window.benchmarkRunnerClient = {
iterationCount: 1,
options: null,
results: null,
initialize: function(suites, options)
{
this.options = options;
},
willStartFirstIteration: function()
{
this.results = new ResultsDashboard(Strings.version, this.options);
},
didRunSuites: function(suitesSamplers)
{
this.results.push(suitesSamplers);
},
didRunTest: function(testData)
{
this.results.calculateScore(testData);
},
didFinishLastIteration: function()
{
benchmarkController.showResults();
}
};
window.sectionsManager =
{
showSection: function(sectionIdentifier, pushState)
{
var sections = document.querySelectorAll("main > section");
for (var i = 0; i < sections.length; ++i) {
document.body.classList.remove("showing-" + sections[i].id);
}
document.body.classList.add("showing-" + sectionIdentifier);
var currentSectionElement = document.querySelector("section.selected");
console.assert(currentSectionElement);
var newSectionElement = document.getElementById(sectionIdentifier);
console.assert(newSectionElement);
currentSectionElement.classList.remove("selected");
newSectionElement.classList.add("selected");
if (pushState)
history.pushState({section: sectionIdentifier}, document.title);
},
setSectionVersion: function(sectionIdentifier, version)
{
document.querySelector("#" + sectionIdentifier + " .version").textContent = version;
},
setSectionScore: function(sectionIdentifier, score, confidence)
{
document.querySelector("#" + sectionIdentifier + " .score").textContent = score;
if (confidence)
document.querySelector("#" + sectionIdentifier + " .confidence").textContent = confidence;
},
populateTable: function(tableIdentifier, headers, dashboard)
{
var table = new ResultsTable(document.getElementById(tableIdentifier), headers);
table.showIterations(dashboard);
}
};
window.benchmarkController = {
initialize: function()
{
document.title = Strings.text.title.replace("%s", Strings.version);
document.querySelectorAll(".version").forEach(function(e) {
e.textContent = Strings.version;
});
benchmarkController.addOrientationListenerIfNecessary();
},
determineCanvasSize: function() {
var match = window.matchMedia("(max-device-width: 760px)");
if (match.matches) {
document.body.classList.add("small");
return;
}
match = window.matchMedia("(max-device-width: 1600px)");
if (match.matches) {
document.body.classList.add("medium");
return;
}
match = window.matchMedia("(max-width: 1600px)");
if (match.matches) {
document.body.classList.add("medium");
return;
}
document.body.classList.add("large");
},
addOrientationListenerIfNecessary: function() {
if (!("orientation" in window))
return;
this.orientationQuery = window.matchMedia("(orientation: landscape)");
this._orientationChanged(this.orientationQuery);
this.orientationQuery.addListener(this._orientationChanged);
},
_orientationChanged: function(match)
{
benchmarkController.isInLandscapeOrientation = match.matches;
if (match.matches)
document.querySelector(".start-benchmark p").classList.add("hidden");
else
document.querySelector(".start-benchmark p").classList.remove("hidden");
benchmarkController.updateStartButtonState();
},
updateStartButtonState: function()
{
document.getElementById("run-benchmark").disabled = !this.isInLandscapeOrientation;
},
_startBenchmark: function(suites, options, frameContainerID)
{
benchmarkController.determineCanvasSize();
var configuration = document.body.className.match(/small|medium|large/);
if (configuration)
options[Strings.json.configuration] = configuration[0];
benchmarkRunnerClient.initialize(suites, options);
var frameContainer = document.getElementById(frameContainerID);
var runner = new BenchmarkRunner(suites, frameContainer, benchmarkRunnerClient);
runner.runMultipleIterations();
sectionsManager.showSection("test-container");
},
startBenchmark: function()
{
var options = {
"test-interval": 30,
"display": "minimal",
"tiles": "big",
"controller": "ramp",
"kalman-process-error": 1,
"kalman-measurement-error": 4,
"time-measurement": "performance"
};
this._startBenchmark(Suites, options, "test-container");
},
showResults: function()
{
if (!this.addedKeyEvent) {
document.addEventListener("keypress", this.handleKeyPress, false);
this.addedKeyEvent = true;
}
var dashboard = benchmarkRunnerClient.results;
var score = dashboard.score;
var confidence = "±" + (Statistics.largestDeviationPercentage(dashboard.scoreLowerBound, score, dashboard.scoreUpperBound) * 100).toFixed(2) + "%";
sectionsManager.setSectionVersion("results", dashboard.version);
sectionsManager.setSectionScore("results", score.toFixed(2), confidence);
sectionsManager.populateTable("results-header", Headers.testName, dashboard);
sectionsManager.populateTable("results-score", Headers.score, dashboard);
sectionsManager.populateTable("results-data", Headers.details, dashboard);
sectionsManager.showSection("results", true);
},
handleKeyPress: function(event)
{
switch (event.charCode)
{
case 27: // esc
benchmarkController.hideDebugInfo();
break;
case 106: // j
benchmarkController.showDebugInfo();
break;
case 115: // s
benchmarkController.selectResults(event.target);
break;
}
},
hideDebugInfo: function()
{
var overlay = document.getElementById("overlay");
if (!overlay)
return;
document.body.removeChild(overlay);
},
showDebugInfo: function()
{
if (document.getElementById("overlay"))
return;
var overlay = Utilities.createElement("div", {
id: "overlay"
}, document.body);
var container = Utilities.createElement("div", {}, overlay);
var header = Utilities.createElement("h3", {}, container);
header.textContent = "Debug Output";
var data = Utilities.createElement("div", {}, container);
data.textContent = "Please wait...";
setTimeout(function() {
var output = {
version: benchmarkRunnerClient.results.version,
options: benchmarkRunnerClient.results.options,
data: benchmarkRunnerClient.results.data
};
data.textContent = JSON.stringify(output, function(key, value) {
if (typeof value === 'number')
return Utilities.toFixedNumber(value, 3);
return value;
}, 1);
}, 0);
data.onclick = function() {
var selection = window.getSelection();
selection.removeAllRanges();
var range = document.createRange();
range.selectNode(data);
selection.addRange(range);
};
var button = Utilities.createElement("button", {}, container);
button.textContent = "Done";
button.onclick = function() {
benchmarkController.hideDebugInfo();
};
},
selectResults: function(target)
{
target.selectRange = ((target.selectRange || 0) + 1) % 3;
var selection = window.getSelection();
selection.removeAllRanges();
var range = document.createRange();
switch (target.selectRange) {
case 0: {
range.selectNode(document.getElementById("results-score"));
break;
}
case 1: {
range.setStart(document.querySelector("#results .score"), 0);
range.setEndAfter(document.querySelector("#results-score"), 0);
break;
}
case 2: {
range.selectNodeContents(document.querySelector("#results .score"));
break;
}
}
selection.addRange(range);
}
};
window.addEventListener("load", function() { benchmarkController.initialize(); });