Skip to content

Commit c0a884c

Browse files
committed
Automatic frame rate detection
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).
1 parent ff00967 commit c0a884c

File tree

9 files changed

+161
-72
lines changed

9 files changed

+161
-72
lines changed

MotionMark/developer.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,7 @@ <h3>Adjusting the test complexity:</h3>
100100
</li>
101101
<li>
102102
<label>System frame rate: <input type="number" id="system-frame-rate" value="60"> FPS</label><br>
103-
<label>Target frame rate: <input type="number" id="frame-rate" value="50"> FPS</label><br>
104-
(Guide: should be about 5/6th of the system frame rate)
103+
<label>Target frame rate: <input type="number" id="frame-rate" value="60"> FPS</label>
105104
</li>
106105
<li>
107106
<h3>Time measurement method:</h3>

MotionMark/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
<p><a href="about.html">More details</a> about the benchmark are available. Bigger scores are better.</p>
6262
<p>For accurate results, please take your browser window full screen, or rotate your device to landscape orientation.</p>
6363
<p class="portrait-orientation-check"><b>Please rotate your device.</b></p>
64-
<button class="landscape-orientation-check" onclick="benchmarkController.startBenchmark()">Run Benchmark</button>
64+
<button id="start-button" class="landscape-orientation-check" onclick="benchmarkController.startBenchmark()">Run Benchmark</button>
65+
<p id="frame-rate-label">&nbsp;</p>
6566
</div>
6667
</section>
6768

MotionMark/resources/debug-runner/debug-runner.js

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ window.suitesManager = {
528528
}
529529

530530
Utilities.extendObject(window.benchmarkController, {
531-
initialize: function()
531+
initialize: async function()
532532
{
533533
document.title = Strings.text.title.replace("%s", Strings.version);
534534
document.querySelectorAll(".version").forEach(function(e) {
@@ -551,8 +551,6 @@ Utilities.extendObject(window.benchmarkController, {
551551
suitesManager.updateUIFromLocalStorage();
552552
suitesManager.updateEditsElementsState();
553553

554-
benchmarkController.detectSystemFrameRate();
555-
556554
var dropTarget = document.getElementById("drop-target");
557555
function stopEvent(e) {
558556
e.stopPropagation();
@@ -584,19 +582,65 @@ Utilities.extendObject(window.benchmarkController, {
584582

585583
var reader = new FileReader();
586584
reader.filename = file.name;
587-
reader.onload = function(e) {
585+
reader.onload = (e) => {
588586
var run = JSON.parse(e.target.result);
589587
if (run.debugOutput instanceof Array)
590588
run = run.debugOutput[0];
591-
if (!("version" in run))
592-
run.version = "1.0";
589+
590+
benchmarkController.migrateImportedData(run);
593591
benchmarkRunnerClient.results = new ResultsDashboard(run.version, run.options, run.data);
594592
benchmarkController.showResults();
595593
};
596594

597595
reader.readAsText(file);
598596
document.title = "File: " + reader.filename;
599597
}, false);
598+
599+
this.frameRateDetectionComplete = false;
600+
this.updateStartButtonState();
601+
602+
let progressElement = document.querySelector("#frame-rate-detection span");
603+
604+
let targetFrameRate;
605+
try {
606+
targetFrameRate = await benchmarkController.determineFrameRate(progressElement);
607+
} catch (e) {
608+
}
609+
610+
this.frameRateDeterminationComplete(targetFrameRate);
611+
},
612+
613+
migrateImportedData: function(runData)
614+
{
615+
if (!("version" in runData))
616+
runData.version = "1.0";
617+
618+
if (!("frame-rate" in runData.options)) {
619+
runData.options["frame-rate"] = 60;
620+
console.log("No frame-rate data; assuming 60fps")
621+
}
622+
623+
if (!("system-frame-rate" in runData.options)) {
624+
runData.options["system-frame-rate"] = 60;
625+
console.log("No system-frame-rate data; assuming 60fps")
626+
}
627+
},
628+
629+
frameRateDeterminationComplete: function(targetFrameRate)
630+
{
631+
let frameRateLabelContent = Strings.text.usingFrameRate.replace("%s", targetFrameRate);
632+
633+
if (!targetFrameRate) {
634+
frameRateLabelContent = Strings.text.frameRateDetectionFailure;
635+
targetFrameRate = 60;
636+
}
637+
638+
document.getElementById("frame-rate-detection").textContent = frameRateLabelContent;
639+
document.getElementById("system-frame-rate").value = targetFrameRate;
640+
document.getElementById("frame-rate").value = targetFrameRate;
641+
642+
this.frameRateDetectionComplete = true;
643+
this.updateStartButtonState();
600644
},
601645

602646
updateStartButtonState: function()
@@ -606,7 +650,8 @@ Utilities.extendObject(window.benchmarkController, {
606650
startButton.disabled = true;
607651
return;
608652
}
609-
startButton.disabled = !suitesManager.isAtLeastOneTestSelected();
653+
654+
startButton.disabled = (!suitesManager.isAtLeastOneTestSelected()) || !this.frameRateDetectionComplete;
610655
},
611656

612657
onBenchmarkOptionsChanged: function(event)
@@ -692,47 +737,5 @@ Utilities.extendObject(window.benchmarkController, {
692737
sectionsManager.setSectionHeader("test-graph", testName);
693738
sectionsManager.showSection("test-graph", true);
694739
this.updateGraphData(testResult, testData, benchmarkRunnerClient.results.options);
695-
},
696-
697-
detectSystemFrameRate: function()
698-
{
699-
let last = 0;
700-
let average = 0;
701-
let count = 0;
702-
703-
const finish = function()
704-
{
705-
const commonFrameRates = [15, 30, 45, 60, 90, 120, 144];
706-
const distanceFromFrameRates = commonFrameRates.map(rate => {
707-
return Math.abs(Math.round(rate - average));
708-
});
709-
let shortestDistance = Number.MAX_VALUE;
710-
let targetFrameRate = undefined;
711-
for (let i = 0; i < commonFrameRates.length; i++) {
712-
if (distanceFromFrameRates[i] < shortestDistance) {
713-
targetFrameRate = commonFrameRates[i];
714-
shortestDistance = distanceFromFrameRates[i];
715-
}
716-
}
717-
targetFrameRate = targetFrameRate || 60;
718-
document.getElementById("frame-rate-detection").textContent = `Detected system frame rate as ${targetFrameRate} FPS`;
719-
document.getElementById("system-frame-rate").value = targetFrameRate;
720-
document.getElementById("frame-rate").value = Math.round(targetFrameRate * 5 / 6);
721-
}
722-
723-
const tick = function(timestamp)
724-
{
725-
average -= average / 30;
726-
average += 1000. / (timestamp - last) / 30;
727-
document.querySelector("#frame-rate-detection span").textContent = Math.round(average);
728-
last = timestamp;
729-
count++;
730-
if (count < 300)
731-
requestAnimationFrame(tick);
732-
else
733-
finish();
734-
}
735-
736-
requestAnimationFrame(tick);
737740
}
738741
});

MotionMark/resources/debug-runner/graph.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Utilities.extendObject(window.benchmarkController, {
4646
samplesWithProperties[seriesName] = series.toArray();
4747
})
4848

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

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

MotionMark/resources/runner/motionmark.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ body.images-loaded {
7878
}
7979
}
8080

81+
#frame-rate-label {
82+
font-size: 75%;
83+
}
84+
8185
::selection {
8286
background-color: black;
8387
color: white;

MotionMark/resources/runner/motionmark.js

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
this._options = options;
3030
this._results = null;
3131
this._version = version;
32-
this._targetFrameRate = options["frame-rate"] || 60;
33-
this._systemFrameRate = options["system-frame-rate"] || 60;
32+
this._targetFrameRate = options["frame-rate"];
33+
this._systemFrameRate = options["system-frame-rate"];
3434
if (testData) {
3535
this._iterationsSamplers = testData;
3636
this._processData();
@@ -96,6 +96,7 @@
9696
var result = {};
9797
data[Strings.json.result] = result;
9898
var samples = data[Strings.json.samples];
99+
const desiredFrameLength = 1000 / this._targetFrameRate;
99100

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

113114
var complexityIndex = series.fieldMap[Strings.json.complexity];
114115
var frameLengthIndex = series.fieldMap[Strings.json.frameLength];
115-
var regressionOptions = { desiredFrameLength: 1000/this._targetFrameRate };
116+
var regressionOptions = { desiredFrameLength: desiredFrameLength };
116117
if (profile)
117118
regressionOptions.preferredProfile = profile;
118119
return {
@@ -168,7 +169,7 @@
168169
result[Strings.json.complexity][Strings.json.complexity] = calculation.complexity;
169170
result[Strings.json.complexity][Strings.json.measurements.stdev] = Math.sqrt(calculation.error / samples[Strings.json.complexity].length);
170171

171-
result[Strings.json.fps] = data.targetFPS || 60;
172+
result[Strings.json.fps] = data.targetFPS;
172173

173174
if (isRampController) {
174175
var timeComplexity = new Experiment;
@@ -472,16 +473,48 @@ window.benchmarkController = {
472473
"time-measurement": "performance",
473474
"warmup-length": 2000,
474475
"warmup-frame-count": 30,
475-
"first-frame-minimum-length": 0
476+
"first-frame-minimum-length": 0,
477+
"system-frame-rate": 60,
478+
"frame-rate": 60,
476479
},
477480

478-
initialize: function()
481+
initialize: async function()
479482
{
480483
document.title = Strings.text.title.replace("%s", Strings.version);
481484
document.querySelectorAll(".version").forEach(function(e) {
482485
e.textContent = Strings.version;
483486
});
484487
benchmarkController.addOrientationListenerIfNecessary();
488+
489+
this._startButton = document.getElementById("start-button");
490+
this._startButton.disabled = true;
491+
this._startButton.textContent = Strings.text.determininingFrameRate;
492+
493+
let targetFrameRate;
494+
try {
495+
targetFrameRate = await benchmarkController.determineFrameRate();
496+
} catch (e) {
497+
}
498+
this.frameRateDeterminationComplete(targetFrameRate);
499+
},
500+
501+
frameRateDeterminationComplete: function(frameRate)
502+
{
503+
const frameRateLabel = document.getElementById("frame-rate-label");
504+
505+
let labelContent = Strings.text.usingFrameRate.replace("%s", frameRate);
506+
if (!frameRate) {
507+
labelContent = Strings.text.frameRateDetectionFailure;
508+
frameRate = 60;
509+
}
510+
511+
frameRateLabel.textContent = labelContent;
512+
513+
this.benchmarkDefaultParameters["system-frame-rate"] = frameRate;
514+
this.benchmarkDefaultParameters["frame-rate"] = frameRate;
515+
516+
this._startButton.textContent = Strings.text.runBenchmark;
517+
this._startButton.disabled = false;
485518
},
486519

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

543+
determineFrameRate: function(detectionProgressElement)
544+
{
545+
return new Promise((resolve, reject) => {
546+
let last = 0;
547+
let average = 0;
548+
let count = 0;
549+
550+
const finish = function()
551+
{
552+
const commonFrameRates = [15, 30, 45, 60, 90, 120, 144];
553+
const distanceFromFrameRates = commonFrameRates.map(rate => {
554+
return Math.abs(Math.round(rate - average));
555+
});
556+
557+
let shortestDistance = Number.MAX_VALUE;
558+
let targetFrameRate = undefined;
559+
for (let i = 0; i < commonFrameRates.length; i++) {
560+
if (distanceFromFrameRates[i] < shortestDistance) {
561+
targetFrameRate = commonFrameRates[i];
562+
shortestDistance = distanceFromFrameRates[i];
563+
}
564+
}
565+
if (!targetFrameRate)
566+
reject("Failed to map frame rate to a common frame rate");
567+
568+
resolve(targetFrameRate);
569+
}
570+
571+
const tick = function(timestamp)
572+
{
573+
average -= average / 30;
574+
average += 1000. / (timestamp - last) / 30;
575+
if (detectionProgressElement)
576+
detectionProgressElement.textContent = Math.round(average);
577+
last = timestamp;
578+
count++;
579+
if (count < 300)
580+
requestAnimationFrame(tick);
581+
else
582+
finish();
583+
}
584+
585+
requestAnimationFrame(tick);
586+
})
587+
},
588+
510589
addOrientationListenerIfNecessary: function()
511590
{
512591
if (!("orientation" in window))
@@ -535,8 +614,6 @@ window.benchmarkController = {
535614

536615
_startBenchmark: function(suites, options, frameContainerID)
537616
{
538-
benchmarkController.determineCanvasSize();
539-
540617
var configuration = document.body.className.match(/small|medium|large/);
541618
if (configuration)
542619
options[Strings.json.configuration] = configuration[0];
@@ -549,9 +626,11 @@ window.benchmarkController = {
549626
sectionsManager.showSection("test-container");
550627
},
551628

552-
startBenchmark: function()
629+
startBenchmark: async function()
553630
{
554-
var options = this.benchmarkDefaultParameters;
631+
benchmarkController.determineCanvasSize();
632+
633+
let options = this.benchmarkDefaultParameters;
555634
this._startBenchmark(Suites, options, "test-container");
556635
},
557636

@@ -562,10 +641,10 @@ window.benchmarkController = {
562641
this.addedKeyEvent = true;
563642
}
564643

565-
var dashboard = benchmarkRunnerClient.results;
566-
var score = dashboard.score;
567-
var confidence = "±" + (Statistics.largestDeviationPercentage(dashboard.scoreLowerBound, score, dashboard.scoreUpperBound) * 100).toFixed(2) + "%";
568-
var fps = dashboard._systemFrameRate;
644+
const dashboard = benchmarkRunnerClient.results;
645+
const score = dashboard.score;
646+
const confidence = "±" + (Statistics.largestDeviationPercentage(dashboard.scoreLowerBound, score, dashboard.scoreUpperBound) * 100).toFixed(2) + "%";
647+
const fps = dashboard._targetFrameRate;
569648
sectionsManager.setSectionVersion("results", dashboard.version);
570649
sectionsManager.setSectionScore("results", score.toFixed(2), confidence, fps);
571650
sectionsManager.populateTable("results-header", Headers.testName, dashboard);

MotionMark/resources/statistics.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,7 @@ Experiment.defaults =
179179
Regression = Utilities.createClass(
180180
function(samples, getComplexity, getFrameLength, startIndex, endIndex, options)
181181
{
182-
var targetFrameRate = options["frame-rate"] || 60;
183-
var desiredFrameLength = options.desiredFrameLength || 1000/targetFrameRate;
182+
const desiredFrameLength = options.desiredFrameLength;
184183
var bestProfile;
185184

186185
if (!options.preferredProfile || options.preferredProfile == Strings.json.profiles.slope) {

MotionMark/resources/strings.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ var Strings = {
2828
testName: "Test Name",
2929
score: "Score",
3030
title: "MotionMark %s",
31+
determininingFrameRate: "Detecting Frame Rate…",
32+
runBenchmark: "Run Benchmark",
33+
usingFrameRate: "Framerate %sfps",
34+
frameRateDetectionFailure: "Failed to determine framerate, using 60fps",
3135
},
3236
json: {
3337
version: "version",

0 commit comments

Comments
 (0)