From cdda0518ddef026254c8a9f85e8f71e1296b5e16 Mon Sep 17 00:00:00 2001 From: Sunny Sachanandani Date: Wed, 6 Aug 2025 13:29:19 -0700 Subject: [PATCH 1/5] Add button and shortcut to download results JSON --- MotionMark/developer.html | 1 + MotionMark/resources/runner/motionmark.js | 37 +++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/MotionMark/developer.html b/MotionMark/developer.html index b2de742..0239a83 100644 --- a/MotionMark/developer.html +++ b/MotionMark/developer.html @@ -158,6 +158,7 @@

MotionMark score

'j': Show JSON results
+ 'd': Download JSON results
's': Select various results for copy/paste (use repeatedly to cycle)

diff --git a/MotionMark/resources/runner/motionmark.js b/MotionMark/resources/runner/motionmark.js index 193e212..f0ca68d 100644 --- a/MotionMark/resources/runner/motionmark.js +++ b/MotionMark/resources/runner/motionmark.js @@ -331,6 +331,9 @@ class BenchmarkController { case 106: // j benchmarkController.showDebugInfo(); break; + case 100: // d + benchmarkController.downloadDebugInfo(); + break; case 115: // s benchmarkController.selectResults(event.target); break; @@ -380,11 +383,39 @@ class BenchmarkController { selection.addRange(range); }; - var button = Utilities.createElement("button", {}, container); - button.textContent = "Done"; - button.onclick = () => { + var doneButton = Utilities.createElement("button", {}, container); + doneButton.textContent = "Done"; + doneButton.onclick = () => { this.hideDebugInfo(); }; + + var downloadButton = Utilities.createElement("button", {}, container); + downloadButton.textContent = "Download"; + downloadButton.onclick = () => { + this.downloadDebugInfo(); + }; + } + + downloadDebugInfo() + { + var output = { + version: this.runnerClient.scoreCalculator.version, + options: this.runnerClient.scoreCalculator.options, + data: this.runnerClient.scoreCalculator.data + }; + var json = JSON.stringify(output, (key, value) => { + if (typeof value === 'number') + return Utilities.toFixedNumber(value, 3); + return value; + }, 1); + var blob = new Blob([json], { type: "application/json" }); + var url = URL.createObjectURL(blob); + + var a = document.createElement('a'); + a.href = url; + a.download = 'motionmark-results.json'; + a.click(); + URL.revokeObjectURL(url); } selectResults(target) From 4a56c758a72d46bf6b62aa0dfdda77ea70e30014 Mon Sep 17 00:00:00 2001 From: Sunny Sachanandani Date: Tue, 3 Mar 2026 15:22:17 -0800 Subject: [PATCH 2/5] Add a "Load results" button to upload results JSON --- MotionMark/developer.html | 7 ++- .../resources/debug-runner/debug-runner.js | 44 +++++++++++-------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/MotionMark/developer.html b/MotionMark/developer.html index 0239a83..5107821 100644 --- a/MotionMark/developer.html +++ b/MotionMark/developer.html @@ -59,7 +59,12 @@

version

Suites:

    -
    Drop results here
    +
    + Drop results here + + +

    Options:

    diff --git a/MotionMark/resources/debug-runner/debug-runner.js b/MotionMark/resources/debug-runner/debug-runner.js index 085f4d3..ef5e72d 100644 --- a/MotionMark/resources/debug-runner/debug-runner.js +++ b/MotionMark/resources/debug-runner/debug-runner.js @@ -587,28 +587,34 @@ class DebugBenchmarkController extends BenchmarkController { } dropTarget.textContent = 'Processing…'; + this.handleResultsFile(e.dataTransfer.files[0]); + }, false); + } - var file = e.dataTransfer.files[0]; - - var reader = new FileReader(); - reader.filename = file.name; - reader.onload = (e) => { - const data = JSON.parse(e.target.result); - - let results; - if (data['debugOutput'] instanceof Array) - results = RunData.resultsDataFromBenchmarkRunnerData(data['debugOutput']); - else - results = RunData.resultsDataFromSingleRunData(data); + loadResults() { + document.getElementById("load-results-input").click(); + } - this.ensureRunnerClient([ ], { }); - this.runnerClient.scoreCalculator = new ScoreCalculator(results); - this.showResults(); - }; + handleResultsFile(fileOrInput) { + var file = fileOrInput instanceof File ? fileOrInput : fileOrInput.files[0]; + if (!file) + return; - reader.readAsText(file); - document.title = "File: " + reader.filename; - }, false); + var reader = new FileReader(); + reader.filename = file.name; + reader.onload = (e) => { + const data = JSON.parse(e.target.result); + let results; + if (data['debugOutput'] instanceof Array) + results = RunData.resultsDataFromBenchmarkRunnerData(data['debugOutput']); + else + results = RunData.resultsDataFromSingleRunData(data); + this.ensureRunnerClient([], {}); + this.runnerClient.scoreCalculator = new ScoreCalculator(results); + this.showResults(); + }; + reader.readAsText(file); + document.title = "File: " + reader.filename; } frameRateDeterminationComplete(targetFrameRate) From a7320f4f58d3e389637cd3c3f8e049817b62727b Mon Sep 17 00:00:00 2001 From: Sunny Sachanandani Date: Tue, 3 Mar 2026 15:22:17 -0800 Subject: [PATCH 3/5] Dynamic score profile selectors Allow changing the score profile on the fly from the results and graph pages and update computed results, graphs, etc. immediately. This also affects the data that's used as the source for the "Show Debug Info" and downloaded JSON. --- MotionMark/developer.html | 24 +++++++++ .../resources/debug-runner/debug-runner.js | 40 +++++++++++++++ MotionMark/resources/runner/motionmark.css | 31 +++++++++++- MotionMark/resources/runner/motionmark.js | 18 +++++++ MotionMark/resources/runner/results.js | 49 ++++++++++++------- 5 files changed, 143 insertions(+), 19 deletions(-) diff --git a/MotionMark/developer.html b/MotionMark/developer.html index 5107821..cd8481e 100644 --- a/MotionMark/developer.html +++ b/MotionMark/developer.html @@ -109,6 +109,14 @@

    Adjusting the test complexity:

  • + +
  • +
  • Time measurement method:

    • @@ -152,6 +160,14 @@

      MotionMark score

  • version

    +
    + +

    @@ -174,6 +190,14 @@

    MotionMark score

    Graph:

     

    +
    + +

     

    @@ -195,6 +199,8 @@

    Graph:

    diff --git a/MotionMark/resources/runner/results.js b/MotionMark/resources/runner/results.js index a33cdc6..2b47eea 100644 --- a/MotionMark/resources/runner/results.js +++ b/MotionMark/resources/runner/results.js @@ -187,6 +187,7 @@ class ScoreCalculator { const frameTypeIndex = series.fieldMap[Strings.json.frameType]; const complexityIndex = series.fieldMap[complexityKey]; + const frameTimeIndex = series.fieldMap[Strings.json.time]; const frameLengthIndex = series.fieldMap[Strings.json.frameLength]; const regressionOptions = { desiredFrameLength: desiredFrameLength }; if (profile) @@ -194,7 +195,7 @@ class ScoreCalculator { const regressionSamples = series.slice(minIndex, maxIndex + 1); const animationSamples = regressionSamples.data.filter((sample) => sample[frameTypeIndex] == Strings.json.animationFrameType); - const regressionData = animationSamples.map((sample) => [ sample[complexityIndex], sample[frameLengthIndex] ]); + const regressionData = animationSamples.map((sample) => [ sample[complexityIndex], sample[frameLengthIndex], sample[frameTimeIndex] ]); const regression = new Regression(regressionData, minIndex, maxIndex, regressionOptions); return { diff --git a/MotionMark/resources/statistics.js b/MotionMark/resources/statistics.js index bd0d7e3..d8e76ee 100644 --- a/MotionMark/resources/statistics.js +++ b/MotionMark/resources/statistics.js @@ -178,8 +178,10 @@ class Regression { constructor(samples, startIndex, endIndex, options) { const desiredFrameLength = options.desiredFrameLength; - var profile; + const kWindowSizeMultiple = 0.1; + var profile; + if (!options.preferredProfile || options.preferredProfile == Strings.json.profiles.slope) { profile = this._calculateRegression(samples, { shouldClip: true, @@ -195,6 +197,11 @@ class Regression { t2: 0 }); this.profile = Strings.json.profiles.flat; + } else if (options.preferredProfile == Strings.json.profiles.window || options.preferredProfile == Strings.json.profiles.windowStrict) { + const window_size = Math.max(1, Math.floor(samples.length * kWindowSizeMultiple)); + const strict = options.preferredProfile == Strings.json.profiles.windowStrict; + profile = this._windowedFit(samples, desiredFrameLength, window_size, strict); + this.profile = options.preferredProfile; } this.startIndex = Math.min(startIndex, endIndex); @@ -219,6 +226,111 @@ class Regression { return this.s1 + this.t1 * complexity; } + _windowedFit(samples, desiredFrameLength, windowSize, strict) + { + const kAllowedErrorFactor = 0.9; + + const complexityIndex = 0; + const frameLengthIndex = 1; + const frameTimeIndex = 2; + + const average = array => array.reduce((a, b) => a + b) / array.length; + + var sortedSamples = samples.slice().sort((a, b) => { + if (a[complexityIndex] == b[complexityIndex]) + return b[frameTimeIndex] - a[frameTimeIndex]; + return a[complexityIndex] - b[complexityIndex]; + }); + + var cumFrameLength = 0.0; + var bestIndex = 0; + var bestComplexity = 0; + var runningFrameLengths = []; + var runningComplexities = []; + + for (var i = 0; i < sortedSamples.length; ++i) { + runningFrameLengths.push(sortedSamples[i][frameLengthIndex]); + runningComplexities.push(sortedSamples[i][complexityIndex]); + + if (runningFrameLengths.length < windowSize) { + continue + } else if (runningFrameLengths.length > windowSize) { + runningFrameLengths.shift(); + runningComplexities.shift(); + } + + let averageFrameLength = average(runningFrameLengths); + let averageComplexity = average(runningComplexities); + let error = desiredFrameLength / averageFrameLength; + let adjustedComplexity = averageComplexity * Math.min(1.0, error); + + if (error >= kAllowedErrorFactor) { + if (adjustedComplexity > bestComplexity) { + bestComplexity = adjustedComplexity; + } + } else if (strict) { + break; + } + } + + for (var i = 0; i < sortedSamples.length; ++i) { + if (sortedSamples[i][complexityIndex] <= bestComplexity) + bestIndex = i; + } + + let complexity = bestComplexity; + + // Calculate slope for remaining points + let t_nom = 0.0; + let t_denom = 0.0; + for (var i = bestIndex + 1; i < sortedSamples.length; i++) { + const tx = sortedSamples[i][complexityIndex] - complexity; + const ty = sortedSamples[i][frameLengthIndex] - desiredFrameLength; + t_nom += tx * ty; + t_denom += tx * tx; + } + + var s1 = desiredFrameLength; + var t1 = 0; + var t2 = (t_nom / t_denom) || 0.0; + var s2 = desiredFrameLength - t2 * complexity; + var n1 = bestIndex + 1; + var n2 = sortedSamples.length - bestIndex - 1; + + function getValueAt(at_complexity) + { + if (at_complexity > complexity) + return s2 + t2 * complexity; + return s1 + t1 * complexity; + } + + let error1 = 0.0; + let error2 = 0.0; + for (var i = 0; i < n1; ++i) { + const frameLengthErr = sortedSamples[i][frameLengthIndex] - getValueAt(sortedSamples[i][complexityIndex]); + error1 += frameLengthErr * frameLengthErr; + } + for (var i = n1; i < sortedSamples.length; ++i) { + const frameLengthErr = sortedSamples[i][frameLengthIndex] - getValueAt(sortedSamples[i][complexityIndex]); + error2 += frameLengthErr * frameLengthErr; + } + + return { + s1: s1, + t1: t1, + s2: s2, + t2: t2, + complexity: complexity, + // Number of samples included in the first segment, inclusive of bestIndex + n1: n1, + // Number of samples included in the second segment + n2: n2, + stdev1: Math.sqrt(error1 / n1), + stdev2: Math.sqrt(error2 / n2), + error: error1 + error2, + }; + } + // A generic two-segment piecewise regression calculator. Based on Kundu/Ubhaya // // Minimize sum of (y - y')^2 diff --git a/MotionMark/resources/strings.js b/MotionMark/resources/strings.js index 43e0510..735e901 100644 --- a/MotionMark/resources/strings.js +++ b/MotionMark/resources/strings.js @@ -78,7 +78,9 @@ var Strings = { profiles: { slope: "slope", - flat: "flat" + flat: "flat", + window: "window", + windowStrict: "window-strict", }, results: { diff --git a/MotionMark/tests/resources/controllers.js b/MotionMark/tests/resources/controllers.js index 6a2d4f9..513ce48 100644 --- a/MotionMark/tests/resources/controllers.js +++ b/MotionMark/tests/resources/controllers.js @@ -170,10 +170,15 @@ class Controller { { return samples[sampleTimeIndex][i] - samples[sampleTimeIndex][i - 1]; } + + _getFrameTime(samples, i) + { + return samples[sampleTimeIndex][i]; + } _previousFrameComplexity(samples, i) { - if (i > 0) + if (i > 1) return this._getComplexity(samples, i - 1); return 0; @@ -415,6 +420,7 @@ class RampController extends Controller { super(benchmark, options); this.targetFPS = targetFPS; + this.preferredProfile = options["score-profile"]; // Initially start with a tier test to find the bounds // The number of objects in a tier test is 10^|_tier| @@ -613,10 +619,15 @@ class RampController extends Controller { for (var i = this._rampStartIndex; i < this._sampler.sampleCount; ++i) { if (this._getFrameType(this._sampler.samples, i) == Strings.json.mutationFrameType) continue; - regressionData.push([ this._getComplexity(this._sampler.samples, i), this._getFrameLength(this._sampler.samples, i) ]); + regressionData.push( + [ + this._getComplexity(this._sampler.samples, i), + this._getFrameLength(this._sampler.samples, i), + this._getFrameTime(this._sampler.samples, i) + ]); } - var regression = new Regression(regressionData, this._sampler.sampleCount - 1, this._rampStartIndex, { desiredFrameLength: this.frameLengthDesired }); + var regression = new Regression(regressionData, this._sampler.sampleCount - 1, this._rampStartIndex, { desiredFrameLength: this.frameLengthDesired, preferredProfile: this.preferredProfile }); this._rampRegressions.push(regression); var frameLengthAtMaxComplexity = regression.valueAt(this._maximumComplexity);