/* * Copyright (C) 2018-2020 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 = { benchmarkDefaultParameters: { "test-interval": 30, "display": "minimal", "tiles": "big", "controller": "ramp", "kalman-process-error": 1, "kalman-measurement-error": 4, "time-measurement": "performance", "warmup-length": 2000, "warmup-frame-count": 30, "first-frame-minimum-length": 0 }, 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 = this.benchmarkDefaultParameters; 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(); });