Skip to content

Commit

Permalink
Automatic frame rate detection
Browse files Browse the repository at this point in the history
Frame rate detection was added in this commit:
85ca394
but in a way that only worked in developer.html, and had various bugs.

This change makes automatic frame rate detection work in the same way when the benchmark is run from
index.html and developer.html. The `determineFrameRate` function is moved to
`window.benchmarkController`, and called from the `initialize` functions in both cases; code is
added so that the button to run the benchmark is disabled until detection is complete; a label shows
the detected fps, or an error message in the event of failure.

For historical reasons, `developer.html` initialized `frame-rate` to 5/6 of `system-frame-rate`,
resulting in a targetFPS of 50 when run from `developer.html`, which was different from the
60fps target when run normally. Remove this so that targetFPS always matches system frame rate.

There was a lot of messiness in how targetFrameRate was propagated through the benchmark, and the
analysis functions, with lots of `|| 60` fallbacks. This change removes all those fallbacks, making
errors obvious when we would have silently fallen back to 60. So `options["frame-rate"]` and
`options["system-frame-rate"]` should always be defined. (`system-frame-rate` only differs from
`frame-rate` if the user makes changes in developer.html.)

The analysis functions use `targetFPS` everywhere. `calculateScore` had an insidious bug where we're
try to read `_targetFrameRate` off the wrong `this` object.

When importing an older JS file that does not contain any frame-rate data, assume 60fps (and log).
  • Loading branch information
smfr committed Jan 5, 2024
1 parent ff00967 commit c0a884c
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 72 deletions.
3 changes: 1 addition & 2 deletions MotionMark/developer.html
Expand Up @@ -100,8 +100,7 @@ <h3>Adjusting the test complexity:</h3>
</li>
<li>
<label>System frame rate: <input type="number" id="system-frame-rate" value="60"> FPS</label><br>
<label>Target frame rate: <input type="number" id="frame-rate" value="50"> FPS</label><br>
(Guide: should be about 5/6th of the system frame rate)
<label>Target frame rate: <input type="number" id="frame-rate" value="60"> FPS</label>
</li>
<li>
<h3>Time measurement method:</h3>
Expand Down
3 changes: 2 additions & 1 deletion MotionMark/index.html
Expand Up @@ -61,7 +61,8 @@
<p><a href="about.html">More details</a> about the benchmark are available. Bigger scores are better.</p>
<p>For accurate results, please take your browser window full screen, or rotate your device to landscape orientation.</p>
<p class="portrait-orientation-check"><b>Please rotate your device.</b></p>
<button class="landscape-orientation-check" onclick="benchmarkController.startBenchmark()">Run Benchmark</button>
<button id="start-button" class="landscape-orientation-check" onclick="benchmarkController.startBenchmark()">Run Benchmark</button>
<p id="frame-rate-label">&nbsp;</p>
</div>
</section>

Expand Down
101 changes: 52 additions & 49 deletions MotionMark/resources/debug-runner/debug-runner.js
Expand Up @@ -528,7 +528,7 @@ window.suitesManager = {
}

Utilities.extendObject(window.benchmarkController, {
initialize: function()
initialize: async function()
{
document.title = Strings.text.title.replace("%s", Strings.version);
document.querySelectorAll(".version").forEach(function(e) {
Expand All @@ -551,8 +551,6 @@ Utilities.extendObject(window.benchmarkController, {
suitesManager.updateUIFromLocalStorage();
suitesManager.updateEditsElementsState();

benchmarkController.detectSystemFrameRate();

var dropTarget = document.getElementById("drop-target");
function stopEvent(e) {
e.stopPropagation();
Expand Down Expand Up @@ -584,19 +582,65 @@ Utilities.extendObject(window.benchmarkController, {

var reader = new FileReader();
reader.filename = file.name;
reader.onload = function(e) {
reader.onload = (e) => {
var run = JSON.parse(e.target.result);
if (run.debugOutput instanceof Array)
run = run.debugOutput[0];
if (!("version" in run))
run.version = "1.0";

benchmarkController.migrateImportedData(run);
benchmarkRunnerClient.results = new ResultsDashboard(run.version, run.options, run.data);
benchmarkController.showResults();
};

reader.readAsText(file);
document.title = "File: " + reader.filename;
}, false);

this.frameRateDetectionComplete = false;
this.updateStartButtonState();

let progressElement = document.querySelector("#frame-rate-detection span");

let targetFrameRate;
try {
targetFrameRate = await benchmarkController.determineFrameRate(progressElement);
} catch (e) {
}

this.frameRateDeterminationComplete(targetFrameRate);
},

migrateImportedData: function(runData)
{
if (!("version" in runData))
runData.version = "1.0";

if (!("frame-rate" in runData.options)) {
runData.options["frame-rate"] = 60;
console.log("No frame-rate data; assuming 60fps")
}

if (!("system-frame-rate" in runData.options)) {
runData.options["system-frame-rate"] = 60;
console.log("No system-frame-rate data; assuming 60fps")
}
},

frameRateDeterminationComplete: function(targetFrameRate)
{
let frameRateLabelContent = Strings.text.usingFrameRate.replace("%s", targetFrameRate);

if (!targetFrameRate) {
frameRateLabelContent = Strings.text.frameRateDetectionFailure;
targetFrameRate = 60;
}

document.getElementById("frame-rate-detection").textContent = frameRateLabelContent;
document.getElementById("system-frame-rate").value = targetFrameRate;
document.getElementById("frame-rate").value = targetFrameRate;

this.frameRateDetectionComplete = true;
this.updateStartButtonState();
},

updateStartButtonState: function()
Expand All @@ -606,7 +650,8 @@ Utilities.extendObject(window.benchmarkController, {
startButton.disabled = true;
return;
}
startButton.disabled = !suitesManager.isAtLeastOneTestSelected();

startButton.disabled = (!suitesManager.isAtLeastOneTestSelected()) || !this.frameRateDetectionComplete;
},

onBenchmarkOptionsChanged: function(event)
Expand Down Expand Up @@ -692,47 +737,5 @@ Utilities.extendObject(window.benchmarkController, {
sectionsManager.setSectionHeader("test-graph", testName);
sectionsManager.showSection("test-graph", true);
this.updateGraphData(testResult, testData, benchmarkRunnerClient.results.options);
},

detectSystemFrameRate: function()
{
let last = 0;
let average = 0;
let count = 0;

const finish = function()
{
const commonFrameRates = [15, 30, 45, 60, 90, 120, 144];
const distanceFromFrameRates = commonFrameRates.map(rate => {
return Math.abs(Math.round(rate - average));
});
let shortestDistance = Number.MAX_VALUE;
let targetFrameRate = undefined;
for (let i = 0; i < commonFrameRates.length; i++) {
if (distanceFromFrameRates[i] < shortestDistance) {
targetFrameRate = commonFrameRates[i];
shortestDistance = distanceFromFrameRates[i];
}
}
targetFrameRate = targetFrameRate || 60;
document.getElementById("frame-rate-detection").textContent = `Detected system frame rate as ${targetFrameRate} FPS`;
document.getElementById("system-frame-rate").value = targetFrameRate;
document.getElementById("frame-rate").value = Math.round(targetFrameRate * 5 / 6);
}

const tick = function(timestamp)
{
average -= average / 30;
average += 1000. / (timestamp - last) / 30;
document.querySelector("#frame-rate-detection span").textContent = Math.round(average);
last = timestamp;
count++;
if (count < 300)
requestAnimationFrame(tick);
else
finish();
}

requestAnimationFrame(tick);
}
});
4 changes: 2 additions & 2 deletions MotionMark/resources/debug-runner/graph.js
Expand Up @@ -46,7 +46,7 @@ Utilities.extendObject(window.benchmarkController, {
samplesWithProperties[seriesName] = series.toArray();
})

this._targetFrameRate = options["frame-rate"] || 60;
this._targetFrameRate = options["frame-rate"];

this.createTimeGraph(testResult, samplesWithProperties[Strings.json.controller], testData[Strings.json.marks], testData[Strings.json.controller], options, margins, size);
this.onTimeGraphOptionsChanged();
Expand Down Expand Up @@ -225,7 +225,7 @@ Utilities.extendObject(window.benchmarkController, {
this._addRegressionLine(group, xScale, yScale, [[bootstrapResult.median, yMin], [bootstrapResult.median, yMax]], [bootstrapResult.confidenceLow, bootstrapResult.confidenceHigh], true);
group.append("circle")
.attr("cx", xScale(bootstrapResult.median))
.attr("cy", yScale(msPerSecond / 60))
.attr("cy", yScale(msPerSecond / this._targetFrameRate))
.attr("r", 5);
}

Expand Down
4 changes: 4 additions & 0 deletions MotionMark/resources/runner/motionmark.css
Expand Up @@ -78,6 +78,10 @@ body.images-loaded {
}
}

#frame-rate-label {
font-size: 75%;
}

::selection {
background-color: black;
color: white;
Expand Down
107 changes: 93 additions & 14 deletions MotionMark/resources/runner/motionmark.js
Expand Up @@ -29,8 +29,8 @@
this._options = options;
this._results = null;
this._version = version;
this._targetFrameRate = options["frame-rate"] || 60;
this._systemFrameRate = options["system-frame-rate"] || 60;
this._targetFrameRate = options["frame-rate"];
this._systemFrameRate = options["system-frame-rate"];
if (testData) {
this._iterationsSamplers = testData;
this._processData();
Expand Down Expand Up @@ -96,6 +96,7 @@
var result = {};
data[Strings.json.result] = result;
var samples = data[Strings.json.samples];
const desiredFrameLength = 1000 / this._targetFrameRate;

function findRegression(series, profile) {
var minIndex = Math.round(.025 * series.length);
Expand All @@ -112,7 +113,7 @@

var complexityIndex = series.fieldMap[Strings.json.complexity];
var frameLengthIndex = series.fieldMap[Strings.json.frameLength];
var regressionOptions = { desiredFrameLength: 1000/this._targetFrameRate };
var regressionOptions = { desiredFrameLength: desiredFrameLength };
if (profile)
regressionOptions.preferredProfile = profile;
return {
Expand Down Expand Up @@ -168,7 +169,7 @@
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);

result[Strings.json.fps] = data.targetFPS || 60;
result[Strings.json.fps] = data.targetFPS;

if (isRampController) {
var timeComplexity = new Experiment;
Expand Down Expand Up @@ -472,16 +473,48 @@ window.benchmarkController = {
"time-measurement": "performance",
"warmup-length": 2000,
"warmup-frame-count": 30,
"first-frame-minimum-length": 0
"first-frame-minimum-length": 0,
"system-frame-rate": 60,
"frame-rate": 60,
},

initialize: function()
initialize: async function()
{
document.title = Strings.text.title.replace("%s", Strings.version);
document.querySelectorAll(".version").forEach(function(e) {
e.textContent = Strings.version;
});
benchmarkController.addOrientationListenerIfNecessary();

this._startButton = document.getElementById("start-button");
this._startButton.disabled = true;
this._startButton.textContent = Strings.text.determininingFrameRate;

let targetFrameRate;
try {
targetFrameRate = await benchmarkController.determineFrameRate();
} catch (e) {
}
this.frameRateDeterminationComplete(targetFrameRate);
},

frameRateDeterminationComplete: function(frameRate)
{
const frameRateLabel = document.getElementById("frame-rate-label");

let labelContent = Strings.text.usingFrameRate.replace("%s", frameRate);
if (!frameRate) {
labelContent = Strings.text.frameRateDetectionFailure;
frameRate = 60;
}

frameRateLabel.textContent = labelContent;

this.benchmarkDefaultParameters["system-frame-rate"] = frameRate;
this.benchmarkDefaultParameters["frame-rate"] = frameRate;

this._startButton.textContent = Strings.text.runBenchmark;
this._startButton.disabled = false;
},

determineCanvasSize: function()
Expand All @@ -507,6 +540,52 @@ window.benchmarkController = {
document.body.classList.add("large");
},

determineFrameRate: function(detectionProgressElement)
{
return new Promise((resolve, reject) => {
let last = 0;
let average = 0;
let count = 0;

const finish = function()
{
const commonFrameRates = [15, 30, 45, 60, 90, 120, 144];
const distanceFromFrameRates = commonFrameRates.map(rate => {
return Math.abs(Math.round(rate - average));
});

let shortestDistance = Number.MAX_VALUE;
let targetFrameRate = undefined;
for (let i = 0; i < commonFrameRates.length; i++) {
if (distanceFromFrameRates[i] < shortestDistance) {
targetFrameRate = commonFrameRates[i];
shortestDistance = distanceFromFrameRates[i];
}
}
if (!targetFrameRate)
reject("Failed to map frame rate to a common frame rate");

resolve(targetFrameRate);
}

const tick = function(timestamp)
{
average -= average / 30;
average += 1000. / (timestamp - last) / 30;
if (detectionProgressElement)
detectionProgressElement.textContent = Math.round(average);
last = timestamp;
count++;
if (count < 300)
requestAnimationFrame(tick);
else
finish();
}

requestAnimationFrame(tick);
})
},

addOrientationListenerIfNecessary: function()
{
if (!("orientation" in window))
Expand Down Expand Up @@ -535,8 +614,6 @@ window.benchmarkController = {

_startBenchmark: function(suites, options, frameContainerID)
{
benchmarkController.determineCanvasSize();

var configuration = document.body.className.match(/small|medium|large/);
if (configuration)
options[Strings.json.configuration] = configuration[0];
Expand All @@ -549,9 +626,11 @@ window.benchmarkController = {
sectionsManager.showSection("test-container");
},

startBenchmark: function()
startBenchmark: async function()
{
var options = this.benchmarkDefaultParameters;
benchmarkController.determineCanvasSize();

let options = this.benchmarkDefaultParameters;
this._startBenchmark(Suites, options, "test-container");
},

Expand All @@ -562,10 +641,10 @@ window.benchmarkController = {
this.addedKeyEvent = true;
}

var dashboard = benchmarkRunnerClient.results;
var score = dashboard.score;
var confidence = "±" + (Statistics.largestDeviationPercentage(dashboard.scoreLowerBound, score, dashboard.scoreUpperBound) * 100).toFixed(2) + "%";
var fps = dashboard._systemFrameRate;
const dashboard = benchmarkRunnerClient.results;
const score = dashboard.score;
const confidence = "±" + (Statistics.largestDeviationPercentage(dashboard.scoreLowerBound, score, dashboard.scoreUpperBound) * 100).toFixed(2) + "%";
const fps = dashboard._targetFrameRate;
sectionsManager.setSectionVersion("results", dashboard.version);
sectionsManager.setSectionScore("results", score.toFixed(2), confidence, fps);
sectionsManager.populateTable("results-header", Headers.testName, dashboard);
Expand Down
3 changes: 1 addition & 2 deletions MotionMark/resources/statistics.js
Expand Up @@ -179,8 +179,7 @@ Experiment.defaults =
Regression = Utilities.createClass(
function(samples, getComplexity, getFrameLength, startIndex, endIndex, options)
{
var targetFrameRate = options["frame-rate"] || 60;
var desiredFrameLength = options.desiredFrameLength || 1000/targetFrameRate;
const desiredFrameLength = options.desiredFrameLength;
var bestProfile;

if (!options.preferredProfile || options.preferredProfile == Strings.json.profiles.slope) {
Expand Down
4 changes: 4 additions & 0 deletions MotionMark/resources/strings.js
Expand Up @@ -28,6 +28,10 @@ var Strings = {
testName: "Test Name",
score: "Score",
title: "MotionMark %s",
determininingFrameRate: "Detecting Frame Rate…",
runBenchmark: "Run Benchmark",
usingFrameRate: "Framerate %sfps",
frameRateDetectionFailure: "Failed to determine framerate, using 60fps",
},
json: {
version: "version",
Expand Down

0 comments on commit c0a884c

Please sign in to comment.