diff --git a/MotionMark/developer.html b/MotionMark/developer.html
index f095355..8b2d919 100644
--- a/MotionMark/developer.html
+++ b/MotionMark/developer.html
@@ -59,11 +59,51 @@
Suites:
-
-
Drop results here
-
-
+
+
+
+
+
+
+
+
+
+
Drop results here
+
+
+
+
+
+
+
+
+
+
+
Baseline (A)
+
Drop Baseline A here
+
+
No file loaded
+
+
+
+
+
Comparison (B)
+
Drop Comparison B here
+
+
No file loaded
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MotionMark/resources/debug-runner/debug-runner.js b/MotionMark/resources/debug-runner/debug-runner.js
index 049f92a..01b4b8d 100644
--- a/MotionMark/resources/debug-runner/debug-runner.js
+++ b/MotionMark/resources/debug-runner/debug-runner.js
@@ -528,6 +528,117 @@ window.suitesManager = new class SuitesManager {
}
}
+class ComparisonResultsTable extends ResultsTable {
+ constructor(element, headers, scoreCalculatorA, scoreCalculatorB)
+ {
+ super(element, headers);
+ this.scoreCalculatorA = scoreCalculatorA;
+ this.scoreCalculatorB = scoreCalculatorB;
+ }
+
+ _addTest(testName, testResultA, testResultB, optionsA, optionsB, testDataA, testDataB)
+ {
+ const row = Utilities.createElement("tr", {}, this.tbody);
+
+ this._flattenedHeaders.forEach(function (header) {
+ var className = "";
+ if (header.className) {
+ if (typeof header.className == "function")
+ className = header.className(testResultA, testResultB, optionsA, optionsB);
+ else
+ className = header.className;
+ }
+
+ const td = Utilities.createElement("td", { class: className }, row);
+
+ if (header.text == Strings.text.testName) {
+ td.textContent = testName;
+ } else if (header.title === Strings.text.graph) {
+ var button = Utilities.createElement("button", { class: "small-button compare-button" }, td);
+ button.textContent = "Compare Graph…";
+ button.testName = testName;
+ button.testResultA = testResultA;
+ button.testResultB = testResultB;
+ button.testDataA = testDataA;
+ button.testDataB = testDataB;
+
+ button.addEventListener("click", function(e) {
+ const target = e.currentTarget;
+ benchmarkController.showComparisonGraph(
+ target.testName,
+ target.testResultA,
+ target.testResultB,
+ target.testDataA,
+ target.testDataB
+ );
+ });
+ } else if (typeof header.text == "string") {
+ var data = testResultB ? testResultB[header.text] : undefined;
+ if (typeof data == "number")
+ data = data.toFixed(2);
+ td.textContent = data !== undefined ? data : "N/A";
+ } else if (typeof header.text == "function") {
+ const rendered = header.text(testResultA, testResultB, optionsA, optionsB);
+ if (rendered instanceof HTMLElement || rendered instanceof DocumentFragment) {
+ td.appendChild(rendered);
+ } else {
+ td.textContent = rendered !== undefined ? rendered : "N/A";
+ }
+ }
+ }, this);
+ }
+
+ _addIteration(iterationResultA, iterationResultB, iterationDataA, iterationDataB, optionsA, optionsB)
+ {
+ const testsResultsA = iterationResultA ? iterationResultA[Strings.json.results.tests] : {};
+ const testsResultsB = iterationResultB ? iterationResultB[Strings.json.results.tests] : {};
+
+ for (const suiteName in testsResultsB) {
+ this._addEmptyRow();
+ const suiteResultB = testsResultsB[suiteName];
+ const suiteDataB = iterationDataB[suiteName];
+
+ const suiteResultA = testsResultsA[suiteName] || {};
+ const suiteDataA = (iterationDataA && iterationDataA[suiteName]) || {};
+
+ for (let testName in suiteResultB) {
+ const testResultB = suiteResultB[testName];
+ const testDataB = suiteDataB[testName];
+
+ const testResultA = suiteResultA[testName];
+ const testDataA = suiteDataA[testName];
+
+ this._addTest(testName, testResultA, testResultB, optionsA, optionsB, testDataA, testDataB);
+ }
+ }
+ }
+
+ showIterations()
+ {
+ this.clear();
+ this._addHeader();
+ this._addBody();
+
+ const iterationsResultsA = this.scoreCalculatorA.results;
+ const iterationsResultsB = this.scoreCalculatorB.results;
+
+ iterationsResultsB.forEach(function(iterationResultB, index) {
+ const iterationResultA = iterationsResultsA[index] || {};
+ const iterationDataA = this.scoreCalculatorA.data[index];
+ const iterationDataB = this.scoreCalculatorB.data[index];
+
+ this._addIteration(
+ iterationResultA,
+ iterationResultB,
+ iterationDataA,
+ iterationDataB,
+ this.scoreCalculatorA.options,
+ this.scoreCalculatorB.options
+ );
+ }, this);
+ }
+}
+
class DebugBenchmarkController extends BenchmarkController {
async initialize()
{
@@ -551,6 +662,10 @@ class DebugBenchmarkController extends BenchmarkController {
suitesManager.updateEditsElementsState();
this.#setupDropTarget();
+ this.#setupCompareDropTargets();
+ this.baselineFileData = null;
+ this.comparisonFileData = null;
+ this.isComparisonMode = false;
this.frameRateDetectionComplete = false;
this.updateStartButtonState();
@@ -562,6 +677,7 @@ class DebugBenchmarkController extends BenchmarkController {
#setupDropTarget()
{
var dropTarget = document.getElementById("drop-target");
+ if (!dropTarget) return;
function stopEvent(e) {
e.stopPropagation();
e.preventDefault();
@@ -580,9 +696,9 @@ class DebugBenchmarkController extends BenchmarkController {
dropTarget.addEventListener("drop", (e) => {
stopEvent(e);
+ dropTarget.classList.remove("drag-over");
if (!e.dataTransfer.files.length) {
- dropTarget.classList.remove("drag-over");
return;
}
@@ -591,10 +707,109 @@ class DebugBenchmarkController extends BenchmarkController {
}, false);
}
+ #setupCompareDropTargets()
+ {
+ const dropTargetA = document.getElementById("drop-target-a");
+ const dropTargetB = document.getElementById("drop-target-b");
+
+ if (!dropTargetA || !dropTargetB) return;
+
+ const stopEvent = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ };
+
+ // Add global window protection to prevent accidental outer drops from navigating the tab
+ if (!window.comparisonGlobalDragWired) {
+ window.addEventListener("dragover", stopEvent, false);
+ window.addEventListener("drop", stopEvent, false);
+ window.comparisonGlobalDragWired = true;
+ }
+
+ // Target A with Drag Counter to completely avoid children visual flickers
+ let dragCounterA = 0;
+ dropTargetA.addEventListener("dragenter", (e) => {
+ stopEvent(e);
+ dragCounterA++;
+ dropTargetA.classList.add("drag-over");
+ }, false);
+
+ dropTargetA.addEventListener("dragover", stopEvent, false);
+
+ dropTargetA.addEventListener("dragleave", (e) => {
+ stopEvent(e);
+ dragCounterA--;
+ if (dragCounterA === 0) {
+ dropTargetA.classList.remove("drag-over");
+ }
+ }, false);
+
+ dropTargetA.addEventListener("drop", (e) => {
+ stopEvent(e);
+ dragCounterA = 0;
+ dropTargetA.classList.remove("drag-over");
+ if (e.dataTransfer.files.length) {
+ const innerDZ = dropTargetA.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Processing…';
+ this.handleResultsFileA(e.dataTransfer.files[0]);
+ }
+ }, false);
+
+ // Target B with Drag Counter
+ let dragCounterB = 0;
+ dropTargetB.addEventListener("dragenter", (e) => {
+ stopEvent(e);
+ dragCounterB++;
+ dropTargetB.classList.add("drag-over");
+ }, false);
+
+ dropTargetB.addEventListener("dragover", stopEvent, false);
+
+ dropTargetB.addEventListener("dragleave", (e) => {
+ stopEvent(e);
+ dragCounterB--;
+ if (dragCounterB === 0) {
+ dropTargetB.classList.remove("drag-over");
+ }
+ }, false);
+
+ dropTargetB.addEventListener("drop", (e) => {
+ stopEvent(e);
+ dragCounterB = 0;
+ dropTargetB.classList.remove("drag-over");
+ if (e.dataTransfer.files.length) {
+ const innerDZ = dropTargetB.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Processing…';
+ this.handleResultsFileB(e.dataTransfer.files[0]);
+ }
+ }, false);
+ }
+
+ switchPanelMode(mode) {
+ document.querySelectorAll('.mode-tab').forEach(tab => tab.classList.remove('active'));
+ document.querySelectorAll('.control-panel').forEach(panel => panel.classList.remove('active'));
+
+ if (mode === 'run') {
+ document.getElementById('btn-mode-run').classList.add('active');
+ document.getElementById('panel-run').classList.add('active');
+ } else {
+ document.getElementById('btn-mode-compare').classList.add('active');
+ document.getElementById('panel-compare').classList.add('active');
+ }
+ }
+
loadResults() {
document.getElementById("load-results-input").click();
}
+ loadResultsA() {
+ document.getElementById("load-results-input-a").click();
+ }
+
+ loadResultsB() {
+ document.getElementById("load-results-input-b").click();
+ }
+
handleResultsFile(fileOrInput) {
const file = fileOrInput instanceof File ? fileOrInput : fileOrInput.files[0];
if (!file)
@@ -609,12 +824,279 @@ class DebugBenchmarkController extends BenchmarkController {
RunData.resultsDataFromSingleRunData(data);
this.ensureRunnerClient([], {});
this.runnerClient.scoreCalculator = new ScoreCalculator(results);
+ this.isComparisonMode = false;
+
+ // Re-show profile selector in results and graph views
+ const resultsScoreProfileSelector = document.getElementById("results-score-profile");
+ if (resultsScoreProfileSelector)
+ resultsScoreProfileSelector.style.display = "inline-block";
+ const graphScoreProfileSelector = document.getElementById("graph-score-profile");
+ if (graphScoreProfileSelector)
+ graphScoreProfileSelector.parentNode.style.display = "block";
+
+ const dropTarget = document.getElementById("drop-target");
+ if (dropTarget) dropTarget.textContent = 'Drop results here';
+
this.showResults();
};
reader.readAsText(file);
document.title = "File: " + reader.filename;
}
+ handleResultsFileA(fileOrInput) {
+ const file = fileOrInput instanceof File ? fileOrInput : fileOrInput.files[0];
+ if (!file)
+ return;
+
+ const reader = new FileReader();
+ reader.filename = file.name;
+ reader.onload = (e) => {
+ try {
+ const data = JSON.parse(e.target.result);
+ this.baselineFileData = data;
+
+ const indicator = document.getElementById("file-indicator-a");
+ indicator.textContent = file.name;
+ indicator.classList.add("loaded");
+
+ const dropTarget = document.getElementById("drop-target-a");
+ if (dropTarget) {
+ const innerDZ = dropTarget.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Baseline A Loaded';
+ }
+
+ this.updateCompareButtonState();
+ } catch (err) {
+ alert("Failed to parse JSON file A: " + err.message);
+ const dropTarget = document.getElementById("drop-target-a");
+ if (dropTarget) {
+ const innerDZ = dropTarget.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Drop Baseline A here';
+ }
+ }
+ };
+ reader.readAsText(file);
+ }
+
+ handleResultsFileB(fileOrInput) {
+ const file = fileOrInput instanceof File ? fileOrInput : fileOrInput.files[0];
+ if (!file)
+ return;
+
+ const reader = new FileReader();
+ reader.filename = file.name;
+ reader.onload = (e) => {
+ try {
+ const data = JSON.parse(e.target.result);
+ this.comparisonFileData = data;
+
+ const indicator = document.getElementById("file-indicator-b");
+ indicator.textContent = file.name;
+ indicator.classList.add("loaded");
+
+ const dropTarget = document.getElementById("drop-target-b");
+ if (dropTarget) {
+ const innerDZ = dropTarget.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Comparison B Loaded';
+ }
+
+ this.updateCompareButtonState();
+ } catch (err) {
+ alert("Failed to parse JSON file B: " + err.message);
+ const dropTarget = document.getElementById("drop-target-b");
+ if (dropTarget) {
+ const innerDZ = dropTarget.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Drop Comparison B here';
+ }
+ }
+ };
+ reader.readAsText(file);
+ }
+
+ updateCompareButtonState()
+ {
+ const btn = document.getElementById("compare-btn");
+ if (btn) {
+ if (this.baselineFileData && this.comparisonFileData) {
+ btn.classList.remove("disabled");
+ btn.disabled = false;
+ } else {
+ btn.classList.add("disabled");
+ btn.disabled = true;
+ }
+ }
+ }
+
+ triggerComparison()
+ {
+ if (!this.baselineFileData || !this.comparisonFileData)
+ return;
+
+ this.isComparisonMode = true;
+
+ // Construct baseline ScoreCalculator (Run A)
+ const resultsA = (this.baselineFileData['debugOutput'] instanceof Array) ?
+ RunData.resultsDataFromBenchmarkRunnerData(this.baselineFileData['debugOutput']) :
+ RunData.resultsDataFromSingleRunData(this.baselineFileData);
+
+ this.comparisonCalculatorA = new ScoreCalculator(resultsA);
+
+ // Construct comparison ScoreCalculator (Run B)
+ const resultsB = (this.comparisonFileData['debugOutput'] instanceof Array) ?
+ RunData.resultsDataFromBenchmarkRunnerData(this.comparisonFileData['debugOutput']) :
+ RunData.resultsDataFromSingleRunData(this.comparisonFileData);
+
+ this.comparisonCalculatorB = new ScoreCalculator(resultsB);
+
+ // Ensure runner client has correct configuration (fall back to Run B parameters)
+ this.ensureRunnerClient([], {});
+ this.runnerClient.scoreCalculator = this.comparisonCalculatorB;
+
+ // Reset file drops displays securely
+ const dropA = document.getElementById("drop-target-a");
+ if (dropA) {
+ const innerDZ = dropA.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Drop Baseline A here';
+ }
+ const dropB = document.getElementById("drop-target-b");
+ if (dropB) {
+ const innerDZ = dropB.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Drop Comparison B here';
+ }
+
+ // Show comparison results tables and score badge!
+ this.showComparisonResults();
+ }
+
+ clearComparison()
+ {
+ this.baselineFileData = null;
+ this.comparisonFileData = null;
+
+ const indicatorA = document.getElementById("file-indicator-a");
+ if (indicatorA) {
+ indicatorA.textContent = "No file loaded";
+ indicatorA.classList.remove("loaded");
+ }
+
+ const indicatorB = document.getElementById("file-indicator-b");
+ if (indicatorB) {
+ indicatorB.textContent = "No file loaded";
+ indicatorB.classList.remove("loaded");
+ }
+
+ const dropA = document.getElementById("drop-target-a");
+ if (dropA) {
+ const innerDZ = dropA.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Drop Baseline A here';
+ }
+ const dropB = document.getElementById("drop-target-b");
+ if (dropB) {
+ const innerDZ = dropB.querySelector(".drop-zone");
+ if (innerDZ) innerDZ.textContent = 'Drop Comparison B here';
+ }
+
+ this.updateCompareButtonState();
+ }
+
+ showComparisonResults()
+ {
+ if (!this.addedKeyEvent) {
+ document.addEventListener("keypress", this.handleKeyPress, false);
+ this.addedKeyEvent = true;
+ }
+
+ const calcA = this.comparisonCalculatorA;
+ const calcB = this.comparisonCalculatorB;
+
+ const scoreA = calcA.score;
+ const scoreB = calcB.score;
+ const diff = scoreB - scoreA;
+ const percent = (diff / scoreA) * 100;
+ const percentStr = (percent >= 0 ? "+" : "") + percent.toFixed(2) + "%";
+ const badgeClass = percent >= 0 ? "comp-badge-better" : "comp-badge-worse";
+
+ // Version comparison string
+ const versionStr = `Baseline A (v${calcA.version}) vs Comparison B (v${calcB.version})`;
+ sectionsManager.setSectionVersion("results", versionStr);
+
+ // Customize score display for comparison (XSS-safe DOM builder)
+ const scoreElement = document.querySelector("#results .score");
+ if (scoreElement) {
+ scoreElement.replaceChildren();
+ const badge = document.createElement("span");
+ badge.className = `comparison-badge ${badgeClass}`;
+ badge.textContent = percentStr;
+ scoreElement.appendChild(badge);
+ }
+
+ // Customize confidence/metric displays (XSS-safe DOM builder)
+ const confidenceElement = document.querySelector("#results .confidence");
+ if (confidenceElement) {
+ confidenceElement.replaceChildren();
+
+ const rowDiv = document.createElement("div");
+ rowDiv.className = "comparison-details-row";
+
+ const createSpan = (labelText, scoreVal) => {
+ const span = document.createElement("span");
+ span.textContent = labelText + ": ";
+ const strong = document.createElement("strong");
+ strong.textContent = scoreVal;
+ span.appendChild(strong);
+ return span;
+ };
+
+ const createSeparator = () => {
+ const sep = document.createElement("span");
+ sep.className = "separator";
+ sep.textContent = "|";
+ return sep;
+ };
+
+ rowDiv.appendChild(createSpan("Baseline (A)", scoreA.toFixed(2)));
+ rowDiv.appendChild(createSeparator());
+ rowDiv.appendChild(createSpan("Comparison (B)", scoreB.toFixed(2)));
+ rowDiv.appendChild(createSeparator());
+ rowDiv.appendChild(createSpan("Change", (diff >= 0 ? "+" : "") + diff.toFixed(2)));
+
+ confidenceElement.appendChild(rowDiv);
+ }
+
+ // Hide score profile in comparison view
+ const resultsScoreProfileSelector = document.getElementById("results-score-profile");
+ if (resultsScoreProfileSelector)
+ resultsScoreProfileSelector.style.display = "none";
+
+ // Populate comparison tables!
+ const compHeaderTable = new ComparisonResultsTable(
+ document.getElementById("results-header"),
+ Headers.testName,
+ calcA,
+ calcB
+ );
+ compHeaderTable.showIterations();
+
+ const compScoreTable = new ComparisonResultsTable(
+ document.getElementById("results-score"),
+ Headers.comparisonScore,
+ calcA,
+ calcB
+ );
+ compScoreTable.showIterations();
+
+ const compDataTable = new ComparisonResultsTable(
+ document.getElementById("results-data"),
+ Headers.comparisonDetails,
+ calcA,
+ calcB
+ );
+ compDataTable.showIterations();
+
+ sectionsManager.showSection("results", true);
+ document.title = "Comparison: A vs B";
+ }
+
frameRateDeterminationComplete(targetFrameRate)
{
let frameRateLabelContent = Strings.text.usingFrameRate.replace("%s", targetFrameRate);
@@ -624,9 +1106,14 @@ class DebugBenchmarkController extends BenchmarkController {
targetFrameRate = 60;
}
- document.getElementById("frame-rate-detection").textContent = frameRateLabelContent;
- document.getElementById("system-frame-rate").value = targetFrameRate;
- document.getElementById("frame-rate").value = targetFrameRate;
+ const frD = document.getElementById("frame-rate-detection");
+ if (frD) frD.textContent = frameRateLabelContent;
+
+ const sysFr = document.getElementById("system-frame-rate");
+ if (sysFr) sysFr.value = targetFrameRate;
+
+ const fr = document.getElementById("frame-rate");
+ if (fr) fr.value = targetFrameRate;
this.frameRateDetectionComplete = true;
this.updateStartButtonState();
@@ -635,6 +1122,7 @@ class DebugBenchmarkController extends BenchmarkController {
updateStartButtonState()
{
var startButton = document.getElementById("start-button");
+ if (!startButton) return;
if ("isInLandscapeOrientation" in this && !this.isInLandscapeOrientation) {
startButton.disabled = true;
return;
@@ -690,6 +1178,11 @@ class DebugBenchmarkController extends BenchmarkController {
showResults()
{
+ if (this.isComparisonMode) {
+ this.showComparisonResults();
+ return;
+ }
+
if (!this.addedKeyEvent) {
document.addEventListener("keypress", this.handleKeyPress, false);
this.addedKeyEvent = true;
@@ -736,17 +1229,66 @@ class DebugBenchmarkController extends BenchmarkController {
this._currentGraphParams = {
testName: testName,
testResult: testResult,
- testData: testData
+ testData: testData,
+ isComparison: false
};
sectionsManager.setSectionHeader("test-graph", testName);
sectionsManager.showSection("test-graph", true);
+ this.graphController.isComparisonMode = false;
+ this.graphController.restoreOriginalNav();
+
+ const graphScoreProfileSelector = document.getElementById("graph-score-profile");
+ if (graphScoreProfileSelector)
+ graphScoreProfileSelector.parentNode.style.display = "block";
+
this.graphController.updateGraphData(testResult, testData, this.runnerClient.scoreCalculator.options);
}
+ showComparisonGraph(testName, testResultA, testResultB, testDataA, testDataB)
+ {
+ this._currentGraphParams = {
+ testName: testName,
+ testResultA: testResultA,
+ testResultB: testResultB,
+ testDataA: testDataA,
+ testDataB: testDataB,
+ isComparison: true
+ };
+
+ sectionsManager.setSectionHeader("test-graph", "Compare: " + testName);
+ sectionsManager.showSection("test-graph", true);
+
+ // Hide score profile selection in comparison graph mode
+ const graphScoreProfileSelector = document.getElementById("graph-score-profile");
+ if (graphScoreProfileSelector)
+ graphScoreProfileSelector.parentNode.style.display = "none";
+
+ this.graphController.updateComparisonGraphData(
+ testName,
+ testResultA,
+ testResultB,
+ testDataA,
+ testDataB,
+ this.comparisonCalculatorA.options,
+ this.comparisonCalculatorB.options
+ );
+ }
+
reloadCurrentGraph() {
if (!this._currentGraphParams)
return;
+ if (this._currentGraphParams.isComparison) {
+ this.showComparisonGraph(
+ this._currentGraphParams.testName,
+ this._currentGraphParams.testResultA,
+ this._currentGraphParams.testResultB,
+ this._currentGraphParams.testDataA,
+ this._currentGraphParams.testDataB
+ );
+ return;
+ }
+
const scoreCalculator = this.runnerClient.scoreCalculator;
const testData = this._currentGraphParams.testData;
const testName = this._currentGraphParams.testName;
@@ -773,3 +1315,4 @@ class DebugBenchmarkController extends BenchmarkController {
window.benchmarkControllerClass = DebugBenchmarkController;
window.benchmarkRunnerClientClass = DebugBenchmarkRunnerClient;
window.sectionsManagerClass = DebugSectionsManager;
+
diff --git a/MotionMark/resources/debug-runner/graph.js b/MotionMark/resources/debug-runner/graph.js
index b4ca40f..c5b68a1 100644
--- a/MotionMark/resources/debug-runner/graph.js
+++ b/MotionMark/resources/debug-runner/graph.js
@@ -633,6 +633,14 @@ class GraphController {
_showOrHideNodes(isShown, selector)
{
+ if (this.isComparisonMode) {
+ if (selector === "#complexity") selector = "[id^=complexity-]";
+ else if (selector === "#rawFPS") selector = "[id^=rawFPS-]";
+ else if (selector === "#filteredFPS") selector = "[id^=filteredFPS-]";
+ else if (selector === "#mutFPS") selector = "[id^=mutFPS-]";
+ else if (selector === "#regressions") selector = "[id^=regressions]";
+ }
+
var nodeList = document.querySelectorAll(selector);
if (isShown) {
for (var i = 0; i < nodeList.length; ++i)
@@ -666,6 +674,11 @@ class GraphController {
onGraphTypeChanged()
{
+ if (this.isComparisonMode) {
+ this.onComparisonGraphTypeChanged();
+ return;
+ }
+
var form = document.forms["graph-type"].elements;
var testResult = document.getElementById("test-graph-data")._testResult;
var isTimeSelected = form["graph-type"].value == "time";
@@ -696,7 +709,8 @@ class GraphController {
mean = mean.join("");
} else {
var complexityRegression = testResult[Strings.json.complexity];
- document.getElementById("complexity-regression-aggregate-raw").textContent = complexityRegression.complexity.toFixed(2) + ", ±" + complexityRegression.stdev.toFixed(2) + "ms";
+ const agg = document.getElementById("complexity-regression-aggregate-raw");
+ if (agg) agg.textContent = complexityRegression.complexity.toFixed(2) + ", ±" + complexityRegression.stdev.toFixed(2) + "ms";
var bootstrap = complexityRegression[Strings.json.bootstrap];
if (bootstrap) {
score = bootstrap.median.toFixed(2);
@@ -712,4 +726,854 @@ class GraphController {
sectionsManager.setSectionScore("test-graph", score, mean, this._targetFrameRate);
}
+
+ setupComparisonNav() {
+ const nav = document.querySelector("#test-graph nav");
+ if (!nav || nav.classList.contains("comparison-nav-setup"))
+ return;
+
+ this._originalNavHTML = nav.innerHTML;
+
+ const targetClasses = ["time", "complexity", "rawFPS", "filteredFPS", "mutFPS"];
+
+ targetClasses.forEach(className => {
+ const originalSpan = document.querySelector(`#test-graph nav .${className}`);
+ if (!originalSpan) return;
+
+ originalSpan.style.display = "none";
+
+ const parent = originalSpan.parentNode;
+
+ const spanA = document.createElement("span");
+ spanA.className = `${className}-a comp-teal`;
+ spanA.style.fontWeight = "bold";
+
+ const separator = document.createElement("span");
+ separator.className = `${className}-sep`;
+ separator.textContent = className === "time" ? " / " : " vs ";
+ separator.style.opacity = "0.5";
+
+ const spanB = document.createElement("span");
+ spanB.className = `${className}-b comp-orange`;
+ spanB.style.fontWeight = "bold";
+
+ parent.appendChild(spanA);
+ parent.appendChild(separator);
+ parent.appendChild(spanB);
+ });
+
+ const originalAgg = document.getElementById("complexity-regression-aggregate-raw");
+ if (originalAgg) {
+ originalAgg.style.display = "none";
+
+ const parent = originalAgg.parentNode;
+
+ const divContainer = document.createElement("div");
+ divContainer.id = "complexity-regression-aggregate-raw-comp";
+ divContainer.className = "comp-nav-legend-col";
+
+ const divA = document.createElement("div");
+ divA.className = "comp-teal";
+ divA.style.fontWeight = "bold";
+ divA.textContent = "A: ";
+ const spanA = document.createElement("span");
+ spanA.id = "complexity-regression-aggregate-raw-a";
+ divA.appendChild(spanA);
+
+ const divB = document.createElement("div");
+ divB.className = "comp-orange";
+ divB.style.fontWeight = "bold";
+ divB.textContent = "B: ";
+ const spanB = document.createElement("span");
+ spanB.id = "complexity-regression-aggregate-raw-b";
+ divB.appendChild(spanB);
+
+ divContainer.appendChild(divA);
+ divContainer.appendChild(divB);
+ parent.appendChild(divContainer);
+ }
+
+ nav.classList.add("comparison-nav-setup");
+ }
+
+ restoreOriginalNav() {
+ const nav = document.querySelector("#test-graph nav");
+ if (nav && nav.classList.contains("comparison-nav-setup")) {
+ const targetClasses = ["time", "complexity", "rawFPS", "filteredFPS", "mutFPS"];
+
+ targetClasses.forEach(className => {
+ const originalSpan = document.querySelector(`#test-graph nav .${className}`);
+ if (originalSpan) {
+ originalSpan.style.display = "inline";
+ }
+
+ const spanA = document.querySelector(`#test-graph nav .${className}-a`);
+ if (spanA) spanA.parentNode.removeChild(spanA);
+
+ const sep = document.querySelector(`#test-graph nav .${className}-sep`);
+ if (sep) sep.parentNode.removeChild(sep);
+
+ const spanB = document.querySelector(`#test-graph nav .${className}-b`);
+ if (spanB) spanB.parentNode.removeChild(spanB);
+ });
+
+ const originalAgg = document.getElementById("complexity-regression-aggregate-raw");
+ if (originalAgg) {
+ originalAgg.style.display = "inline";
+ }
+
+ const divContainer = document.getElementById("complexity-regression-aggregate-raw-comp");
+ if (divContainer) {
+ divContainer.parentNode.removeChild(divContainer);
+ }
+
+ nav.classList.remove("comparison-nav-setup");
+ }
+ }
+
+ updateComparisonGraphData(testName, testResultA, testResultB, testDataA, testDataB, optionsA, optionsB)
+ {
+ this.isComparisonMode = true;
+ this._comparisonGraphParams = {
+ testName: testName,
+ testResultA: testResultA,
+ testResultB: testResultB,
+ testDataA: testDataA,
+ testDataB: testDataB,
+ optionsA: optionsA,
+ optionsB: optionsB
+ };
+
+ var element = document.getElementById("test-graph-data");
+ if (element) {
+ element.innerHTML = "";
+ element._testResult = testResultB;
+ element._options = optionsB;
+ }
+
+ var margins = new Insets(30, 30, 50, 40);
+ var size = GeometryHelpers.elementClientSize(element);
+
+ var samplesWithPropertiesA = null;
+ if (testDataA) {
+ samplesWithPropertiesA = {};
+ [Strings.json.controller, Strings.json.complexity].forEach(function(seriesName) {
+ var series = testDataA[Strings.json.samples][seriesName];
+ samplesWithPropertiesA[seriesName] = series.toArray();
+ });
+ }
+
+ var samplesWithPropertiesB = null;
+ if (testDataB) {
+ samplesWithPropertiesB = {};
+ [Strings.json.controller, Strings.json.complexity].forEach(function(seriesName) {
+ var series = testDataB[Strings.json.samples][seriesName];
+ samplesWithPropertiesB[seriesName] = series.toArray();
+ });
+ }
+
+ this._targetFrameRate = optionsB ? optionsB["frame-rate"] : 60;
+
+ this.setupComparisonNav();
+
+ // Create comparative overlaid time graph
+ this._createComparisonTimeGraph(
+ testResultA,
+ testResultB,
+ samplesWithPropertiesA ? samplesWithPropertiesA[Strings.json.controller] : null,
+ samplesWithPropertiesB ? samplesWithPropertiesB[Strings.json.controller] : null,
+ testDataA ? testDataA[Strings.json.marks] : null,
+ testDataB ? testDataB[Strings.json.marks] : null,
+ testDataA ? testDataA[Strings.json.controller] : null,
+ testDataB ? testDataB[Strings.json.controller] : null,
+ optionsA,
+ optionsB,
+ margins,
+ size
+ );
+ this.onTimeGraphOptionsChanged();
+
+ // Create comparative overlaid complexity graph
+ this._showOrHideNodes(true, "form[name=graph-type]");
+ document.forms["graph-type"].elements["type"] = "complexity";
+
+ this._createComparisonComplexityGraph(
+ testResultA,
+ testResultB,
+ testDataA ? testDataA[Strings.json.controller] : null,
+ testDataB ? testDataB[Strings.json.controller] : null,
+ samplesWithPropertiesA,
+ samplesWithPropertiesB,
+ optionsA,
+ optionsB,
+ margins,
+ size
+ );
+ this.onComplexityGraphOptionsChanged();
+
+ this.onGraphTypeChanged();
+ }
+
+ onComparisonGraphTypeChanged()
+ {
+ var form = document.forms["graph-type"].elements;
+ var isTimeSelected = form["graph-type"].value == "time";
+
+ this._showOrHideNodes(isTimeSelected, "#time-graph");
+ this._showOrHideNodes(isTimeSelected, "form[name=time-graph-options]");
+ this._showOrHideNodes(!isTimeSelected, "#complexity-graph");
+ this._showOrHideNodes(!isTimeSelected, "form[name=complexity-graph-options]");
+
+ const testResultA = this._comparisonGraphParams.testResultA;
+ const testResultB = this._comparisonGraphParams.testResultB;
+
+ let scoreA = 0, scoreB = 0;
+ let labelA = "", labelB = "";
+
+ if (isTimeSelected) {
+ if (testResultA) {
+ scoreA = testResultA[Strings.json.score];
+ labelA = `A: ${scoreA.toFixed(2)}`;
+ }
+ if (testResultB) {
+ scoreB = testResultB[Strings.json.score];
+ labelB = `B: ${scoreB.toFixed(2)}`;
+ }
+ } else {
+ // Complexity graph aggregates raw data displays
+ if (testResultA) {
+ const complexityRegressionA = testResultA[Strings.json.complexity];
+ const aggA = document.getElementById("complexity-regression-aggregate-raw-a");
+ if (aggA) aggA.textContent = complexityRegressionA.complexity.toFixed(2) + ", ±" + complexityRegressionA.stdev.toFixed(2) + "ms";
+
+ const bootstrapA = complexityRegressionA[Strings.json.bootstrap];
+ if (bootstrapA) {
+ scoreA = bootstrapA.median;
+ labelA = `A: ${scoreA.toFixed(2)} [${bootstrapA.confidenceLow.toFixed(1)} - ${bootstrapA.confidenceHigh.toFixed(1)}]`;
+ } else {
+ scoreA = testResultA[Strings.json.score];
+ labelA = `A: ${scoreA.toFixed(2)}`;
+ }
+ }
+ if (testResultB) {
+ const complexityRegressionB = testResultB[Strings.json.complexity];
+ const aggB = document.getElementById("complexity-regression-aggregate-raw-b");
+ if (aggB) aggB.textContent = complexityRegressionB.complexity.toFixed(2) + ", ±" + complexityRegressionB.stdev.toFixed(2) + "ms";
+
+ const bootstrapB = complexityRegressionB[Strings.json.bootstrap];
+ if (bootstrapB) {
+ scoreB = bootstrapB.median;
+ labelB = `B: ${scoreB.toFixed(2)} [${bootstrapB.confidenceLow.toFixed(1)} - ${bootstrapB.confidenceHigh.toFixed(1)}]`;
+ } else {
+ scoreB = testResultB[Strings.json.score];
+ labelB = `B: ${scoreB.toFixed(2)}`;
+ }
+ }
+ }
+
+ let changeStr = "";
+ if (scoreA && scoreB) {
+ const diff = scoreB - scoreA;
+ const pct = (diff / scoreA) * 100;
+ changeStr = `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
+ }
+
+ const scoreElement = document.querySelector("#test-graph .score");
+ if (scoreElement) {
+ scoreElement.replaceChildren();
+ if (changeStr) {
+ const pctVal = parseFloat(changeStr);
+ const className = pctVal >= 0 ? "comp-badge-better inline-badge" : "comp-badge-worse inline-badge";
+ const badge = document.createElement("span");
+ badge.className = className;
+ badge.textContent = changeStr;
+ scoreElement.appendChild(badge);
+ }
+ }
+
+ const confidenceElement = document.querySelector("#test-graph .confidence");
+ if (confidenceElement) {
+ confidenceElement.replaceChildren();
+
+ const createTextSpan = (text, className) => {
+ const span = document.createElement("span");
+ span.className = className;
+ span.textContent = text;
+ return span;
+ };
+
+ const sep = document.createElement("span");
+ sep.className = "separator";
+ sep.textContent = " | ";
+
+ if (labelA) confidenceElement.appendChild(createTextSpan(labelA, "comp-teal"));
+ confidenceElement.appendChild(sep);
+ if (labelB) confidenceElement.appendChild(createTextSpan(labelB, "comp-orange"));
+ }
+ }
+
+ _createComparisonTimeGraph(resultA, resultB, samplesA, samplesB, marksA, marksB, regressionsA, regressionsB, optionsA, optionsB, margins, size)
+ {
+ const axisWidth = size.width - margins.left - margins.right;
+ const axisHeight = size.height - margins.top - margins.bottom;
+
+ var svg = d3.select("#test-graph-data").append("svg")
+ .attr("id", "time-graph")
+ .attr("width", size.width)
+ .attr("height", size.height)
+ .append("g")
+ .attr("transform", "translate(" + margins.left + "," + margins.top + ")");
+
+ const timeMin = Math.min(
+ samplesA ? d3.min(samplesA, s => s.time) : 0,
+ samplesB ? d3.min(samplesB, s => s.time) : 0,
+ 0
+ );
+ const timeMax = Math.max(
+ samplesA ? d3.max(samplesA, s => s.time) : 0,
+ samplesB ? d3.max(samplesB, s => s.time) : 0
+ );
+
+ var x = d3.scale.linear()
+ .range([0, axisWidth])
+ .domain([timeMin, timeMax]);
+
+ let complexityMax = 0;
+ if (samplesA) {
+ complexityMax = Math.max(complexityMax, d3.max(samplesA, s => s.time > 0 ? s.complexity : 0));
+ }
+ if (samplesB) {
+ complexityMax = Math.max(complexityMax, d3.max(samplesB, s => s.time > 0 ? s.complexity : 0));
+ }
+ complexityMax *= 1.2;
+
+ const graphTop = 10;
+ var yLeft = d3.scale.linear()
+ .range([axisHeight, graphTop])
+ .domain([0, complexityMax]);
+
+ const targetFPS_A = optionsA ? optionsA["frame-rate"] : 60;
+ const targetFPS_B = optionsB ? optionsB["frame-rate"] : 60;
+
+ const minFrameRate = Math.min(this._minFrameRate(optionsA), this._minFrameRate(optionsB));
+ const maxFrameRate = Math.max(this._maxFrameRate(optionsA), this._maxFrameRate(optionsB));
+
+ const yRightMin = msPerSecond / minFrameRate;
+ const yRightMax = msPerSecond / maxFrameRate;
+
+ var yRight = d3.scale.linear()
+ .range([axisHeight, graphTop])
+ .domain([yRightMin, yRightMax]);
+
+ var xAxis = d3.svg.axis()
+ .scale(x)
+ .orient("bottom")
+ .tickFormat(function(d) { return (d / msPerSecond).toFixed(0); });
+ var yAxisLeft = d3.svg.axis()
+ .scale(yLeft)
+ .orient("left");
+
+ var yAxisRight = d3.svg.axis()
+ .scale(yRight)
+ .tickValues(this._tickValuesForFrameRate(this._targetFrameRate, minFrameRate, maxFrameRate))
+ .tickFormat(function(d) { return (msPerSecond / d).toFixed(0); })
+ .orient("right");
+
+ svg.append("g")
+ .attr("class", "x axis")
+ .attr("fill", "rgb(235, 235, 235)")
+ .attr("transform", "translate(0," + axisHeight + ")")
+ .call(xAxis)
+ .append("text")
+ .attr("class", "label")
+ .attr("x", axisWidth)
+ .attr("y", -6)
+ .attr("fill", "rgb(235, 235, 235)")
+ .style("text-anchor", "end")
+ .text("time");
+
+ svg.append("g")
+ .attr("class", "yLeft axis")
+ .attr("fill", "#7ADD49")
+ .call(yAxisLeft)
+ .append("text")
+ .attr("class", "label")
+ .attr("transform", "rotate(-90)")
+ .attr("y", 6)
+ .attr("fill", "#7ADD49")
+ .attr("dy", ".71em")
+ .style("text-anchor", "end")
+ .text(Strings.text.complexity);
+
+ svg.append("g")
+ .attr("class", "yRight axis")
+ .attr("fill", "#FA4925")
+ .attr("transform", "translate(" + axisWidth + ", 0)")
+ .call(yAxisRight)
+ .append("text")
+ .attr("class", "label")
+ .attr("x", 9)
+ .attr("y", -20)
+ .attr("fill", "#FA4925")
+ .attr("dy", ".71em")
+ .style("text-anchor", "start")
+ .text(Strings.text.frameRate);
+
+ var yMin = yRight(yAxisRight.scale().domain()[0]);
+ var yMax = yRight(yAxisRight.scale().domain()[1]);
+ const drawMarks = marksB || marksA;
+ if (drawMarks) {
+ for (var markName in drawMarks) {
+ var mark = drawMarks[markName];
+ var xLocation = x(mark.time);
+
+ var markerGroup = svg.append("g")
+ .attr("class", "marker")
+ .attr("transform", "translate(" + xLocation + ", 0)");
+ markerGroup.append("text")
+ .attr("transform", "translate(10, " + (yMin - 10) + ") rotate(-90)")
+ .style("text-anchor", "start")
+ .text(markName);
+ markerGroup.append("line")
+ .attr("x1", 0)
+ .attr("x2", 0)
+ .attr("y1", yMin)
+ .attr("y2", yMax);
+ }
+ }
+
+ if (resultA && Strings.json.controller in resultA) {
+ var compA = resultA[Strings.json.controller];
+ var regA = svg.append("g").attr("class", "complexity mean comp-run-a");
+ this._addRegressionLine(regA, x, yLeft, [[samplesA[0].time, compA.average], [samplesA[samplesA.length - 1].time, compA.average]], compA.stdev);
+ }
+ if (resultB && Strings.json.controller in resultB) {
+ var compB = resultB[Strings.json.controller];
+ var regB = svg.append("g").attr("class", "complexity mean comp-run-b");
+ this._addRegressionLine(regB, x, yLeft, [[samplesB[0].time, compB.average], [samplesB[samplesB.length - 1].time, compB.average]], compB.stdev);
+ }
+
+ if (resultA && Strings.json.frameLength in resultA) {
+ var fpsA = resultA[Strings.json.frameLength];
+ var regFpsA = svg.append("g").attr("class", "fps mean comp-run-a");
+ this._addRegressionLine(regFpsA, x, yRight, [[samplesA[0].time, msPerSecond / fpsA.average], [samplesA[samplesA.length - 1].time, msPerSecond / fpsA.average]], fpsA.stdev);
+ }
+ if (resultB && Strings.json.frameLength in resultB) {
+ var fpsB = resultB[Strings.json.frameLength];
+ var regFpsB = svg.append("g").attr("class", "fps mean comp-run-b");
+ this._addRegressionLine(regFpsB, x, yRight, [[samplesB[0].time, msPerSecond / fpsB.average], [samplesB[samplesB.length - 1].time, msPerSecond / fpsB.average]], fpsB.stdev);
+ }
+
+ if (optionsA && optionsA["controller"] == "adaptive") {
+ svg.append("line")
+ .attr("x1", x(0))
+ .attr("x2", axisWidth)
+ .attr("y1", yRight(msPerSecond / targetFPS_A))
+ .attr("y2", yRight(msPerSecond / targetFPS_A))
+ .attr("class", "target-fps marker comp-run-a");
+ }
+ if (optionsB && optionsB["controller"] == "adaptive") {
+ svg.append("line")
+ .attr("x1", x(0))
+ .attr("x2", axisWidth)
+ .attr("y1", yRight(msPerSecond / targetFPS_B))
+ .attr("y2", yRight(msPerSecond / targetFPS_B))
+ .attr("class", "target-fps marker comp-run-b");
+ }
+
+ var cursorGroup = svg.append("g").attr("class", "cursor");
+ cursorGroup.append("line")
+ .attr("x1", 0)
+ .attr("x2", 0)
+ .attr("y1", yMin)
+ .attr("y2", yMin);
+
+ function addDataset(runId, runClass, samples, animData, mutData, filteredData) {
+ function addCurve(name, data, yCoordinateCallback, pointRadius, omitLine) {
+ const compoundId = `${name}-${runId}`;
+ var svgGroup = svg.append("g")
+ .attr("id", compoundId)
+ .attr("class", runClass);
+ if (!omitLine) {
+ svgGroup.append("path")
+ .datum(data)
+ .attr("d", d3.svg.line()
+ .x(function(d) { return x(d.time); })
+ .y(yCoordinateCallback));
+ }
+ svgGroup.selectAll("circle")
+ .data(data)
+ .enter()
+ .append("circle")
+ .attr("cx", function(d) { return x(d.time); })
+ .attr("cy", yCoordinateCallback)
+ .attr("r", pointRadius);
+
+ cursorGroup.append("circle")
+ .attr("class", `${name}-${runId} ${runClass}`)
+ .attr("r", pointRadius + 2);
+ }
+
+ addCurve("complexity", samples, d => yLeft(d.complexity), 2);
+ addCurve("rawFPS", animData, d => yRight(d.frameLength), 1);
+ addCurve("mutFPS", mutData, d => yRight(d.frameLength), 1);
+ addCurve("filteredFPS", filteredData, d => yRight(d.smoothedFrameLength), 2);
+ }
+
+ if (samplesA) {
+ const animA = samplesA.filter(s => s['frameType'] == Strings.json.animationFrameType);
+ const mutA = samplesA.filter(s => s['frameType'] == Strings.json.mutationFrameType);
+ const filtA = animA.filter(s => "smoothedFrameLength" in s);
+ addDataset("a", "comp-run-a-complexity comp-run-a-rawfps comp-run-a-filteredfps", samplesA, animA, mutA, filtA);
+ }
+
+ if (samplesB) {
+ const animB = samplesB.filter(s => s['frameType'] == Strings.json.animationFrameType);
+ const mutB = samplesB.filter(s => s['frameType'] == Strings.json.mutationFrameType);
+ const filtB = animB.filter(s => "smoothedFrameLength" in s);
+ addDataset("b", "comp-run-b-complexity comp-run-b-rawfps comp-run-b-filteredfps", samplesB, animB, mutB, filtB);
+ }
+
+ var regressionGroup = svg.append("g").attr("id", "regressions");
+
+ function drawRegressions(regressionsList, runClass) {
+ if (!regressionsList) return;
+ regressionsList.forEach(function (regression) {
+ var regSub = regressionGroup.append("g").attr("class", runClass);
+ if (!isNaN(regression.segment1[0][1]) && !isNaN(regression.segment1[1][1])) {
+ regSub.append("line")
+ .attr("x1", x(regression.segment1[0][0]))
+ .attr("x2", x(regression.segment1[1][0]))
+ .attr("y1", yRight(regression.segment1[0][1]))
+ .attr("y2", yRight(regression.segment1[1][1]));
+ }
+ if (!isNaN(regression.segment2[0][1]) && !isNaN(regression.segment2[1][1])) {
+ regSub.append("line")
+ .attr("x1", x(regression.segment2[0][0]))
+ .attr("x2", x(regression.segment2[1][0]))
+ .attr("y1", yRight(regression.segment2[0][1]))
+ .attr("y2", yRight(regression.segment2[1][1]));
+ }
+ regSub.append("circle")
+ .attr("cx", x(regression.segment2[0][0]))
+ .attr("cy", yRight(regression.segment2[0][1]))
+ .attr("r", 3);
+ regSub.append("line")
+ .attr("class", "association")
+ .attr("stroke-dasharray", "5, 3")
+ .attr("x1", x(regression.segment2[0][0]))
+ .attr("x2", x(regression.segment2[0][0]))
+ .attr("y1", yRight(regression.segment2[0][1]))
+ .attr("y2", yLeft(regression.complexity));
+ regSub.append("circle")
+ .attr("cx", x(regression.segment1[1][0]))
+ .attr("cy", yLeft(regression.complexity))
+ .attr("r", 5);
+ });
+ }
+
+ drawRegressions(regressionsA, "comp-run-a");
+ drawRegressions(regressionsB, "comp-run-b");
+
+ var area = svg.append("rect")
+ .attr("fill", "transparent")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", axisWidth)
+ .attr("height", axisHeight);
+
+ var timeBisect = d3.bisector(function(d) { return d.time; }).right;
+ var statsToHighlight = ["complexity", "rawFPS", "filteredFPS", "mutFPS"];
+
+ area.on("mouseover", function() {
+ document.querySelector("#time-graph .cursor").classList.remove("hidden");
+ document.querySelector("#test-graph nav").classList.remove("hide-data");
+ }).on("mouseout", function() {
+ document.querySelector("#time-graph .cursor").classList.add("hidden");
+ document.querySelector("#test-graph nav").classList.add("hide-data");
+ }).on("mousemove", function() {
+ var form = document.forms["time-graph-options"].elements;
+ var mx_domain = x.invert(d3.mouse(this)[0]);
+
+ var indexA = samplesA ? Math.min(timeBisect(samplesA, mx_domain), samplesA.length - 1) : null;
+ var indexB = samplesB ? Math.min(timeBisect(samplesB, mx_domain), samplesB.length - 1) : null;
+
+ var dataA = indexA !== null ? samplesA[indexA] : null;
+ var dataB = indexB !== null ? samplesB[indexB] : null;
+
+ if (dataA) {
+ document.querySelector("#test-graph nav .time-a").textContent = (dataA.time / msPerSecond).toFixed(3) + "s (" + indexA + ")";
+ }
+ if (dataB) {
+ document.querySelector("#test-graph nav .time-b").textContent = (dataB.time / msPerSecond).toFixed(3) + "s (" + indexB + ")";
+ }
+
+ var cursor_x = x(mx_domain);
+ var ys = [yRight(yAxisRight.scale().domain()[0]), yRight(yAxisRight.scale().domain()[1])];
+
+ statsToHighlight.forEach(function(name) {
+ var elementA = document.querySelector("#test-graph nav ." + name + "-a");
+ var elementB = document.querySelector("#test-graph nav ." + name + "-b");
+
+ function fillStats(data, element, targetCircleClass, isA) {
+ if (!data) {
+ element.textContent = "";
+ document.querySelector("#time-graph .cursor ." + targetCircleClass).classList.add("hidden");
+ return;
+ }
+
+ var content = "";
+ var data_y = null;
+ switch (name) {
+ case "complexity":
+ content = data.complexity;
+ data_y = yLeft(data.complexity);
+ break;
+ case "rawFPS":
+ if (data.frameType == Strings.json.animationFrameType) {
+ content = (msPerSecond / data.frameLength).toFixed(1);
+ data_y = yRight(data.frameLength);
+ }
+ break;
+ case "filteredFPS":
+ if ("smoothedFrameLength" in data) {
+ content = (msPerSecond / data.smoothedFrameLength).toFixed(1);
+ data_y = yRight(data.smoothedFrameLength);
+ }
+ break;
+ case "mutFPS":
+ if (data.frameType == Strings.json.mutationFrameType) {
+ content = (msPerSecond / data.frameLength).toFixed(1);
+ data_y = yRight(data.frameLength);
+ }
+ break;
+ }
+
+ element.textContent = content;
+
+ if (form[name].checked && data_y !== null) {
+ ys.push(data_y);
+ cursorGroup.select("." + targetCircleClass)
+ .attr("cx", x(data.time))
+ .attr("cy", data_y);
+ document.querySelector("#time-graph .cursor ." + targetCircleClass).classList.remove("hidden");
+ } else {
+ document.querySelector("#time-graph .cursor ." + targetCircleClass).classList.add("hidden");
+ }
+ }
+
+ fillStats(dataA, elementA, name + "-a", true);
+ fillStats(dataB, elementB, name + "-b", false);
+ });
+
+ cursorGroup.select("line")
+ .attr("x1", cursor_x)
+ .attr("x2", cursor_x)
+ .attr("y1", Math.min.apply(null, ys))
+ .attr("y2", Math.max.apply(null, ys));
+ });
+ }
+
+ _createComparisonComplexityGraph(resultA, resultB, regressionsA, regressionsB, samplesA, samplesB, optionsA, optionsB, margins, size)
+ {
+ var svg = d3.select("#test-graph-data").append("svg")
+ .attr("id", "complexity-graph")
+ .attr("class", "hidden")
+ .attr("width", size.width)
+ .attr("height", size.height)
+ .append("g")
+ .attr("transform", "translate(" + margins.left + "," + margins.top + ")");
+
+ var timeSamplesA = samplesA ? samplesA[Strings.json.controller] : null;
+ var timeSamplesB = samplesB ? samplesB[Strings.json.controller] : null;
+
+ let xMin = 100000, xMax = 0;
+
+ function updateMinMax(samples, regressions) {
+ if (!samples) return;
+ if (regressions) {
+ regressions.forEach(function(regression) {
+ for (var i = regression.startIndex; i <= regression.endIndex; ++i) {
+ if (samples[i]) {
+ xMin = Math.min(xMin, samples[i].complexity);
+ xMax = Math.max(xMax, samples[i].complexity);
+ }
+ }
+ });
+ } else {
+ xMin = Math.min(xMin, d3.min(samples, s => s.complexity));
+ xMax = Math.max(xMax, d3.max(samples, s => s.complexity));
+ }
+ }
+
+ updateMinMax(timeSamplesA, regressionsA);
+ updateMinMax(timeSamplesB, regressionsB);
+
+ const axisWidth = size.width - margins.left - margins.right;
+ const axisHeight = size.height - margins.top - margins.bottom;
+
+ const minFrameRate = Math.min(this._minFrameRate(optionsA), this._minFrameRate(optionsB));
+ const maxFrameRate = Math.max(this._maxFrameRate(optionsA), this._maxFrameRate(optionsB));
+
+ const yMin = msPerSecond / minFrameRate;
+ const yMax = msPerSecond / maxFrameRate;
+
+ var xScale = d3.scale.linear()
+ .range([0, axisWidth])
+ .domain([xMin, xMax]);
+ var yScale = d3.scale.linear()
+ .range([axisHeight, 0])
+ .domain([yMin, yMax]);
+
+ var xAxis = d3.svg.axis()
+ .scale(xScale)
+ .orient("bottom");
+ var yAxis = d3.svg.axis()
+ .scale(yScale)
+ .tickValues(this._tickValuesForFrameRate(optionsB ? optionsB["frame-rate"] : 60, minFrameRate, maxFrameRate))
+ .tickFormat(function(d) { return (msPerSecond / d).toFixed(0); })
+ .orient("left");
+
+ svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + axisHeight + ")")
+ .call(xAxis);
+
+ svg.append("g")
+ .attr("class", "y axis")
+ .call(yAxis);
+
+ if (resultA && Strings.json.controller in resultA) {
+ var meanA = svg.append("g").attr("class", "mean complexity comp-run-a");
+ var resA = resultA[Strings.json.controller];
+ this._addRegressionLine(meanA, xScale, yScale, [[resA.average, yMin], [resA.average, yMax]], resA.stdev, true);
+ }
+ if (resultB && Strings.json.controller in resultB) {
+ var meanB = svg.append("g").attr("class", "mean complexity comp-run-b");
+ var resB = resultB[Strings.json.controller];
+ this._addRegressionLine(meanB, xScale, yScale, [[resB.average, yMin], [resB.average, yMax]], resB.stdev, true);
+ }
+
+ if (resultA && resultA[Strings.json.complexity]) {
+ this._addRegression(resultA[Strings.json.complexity], svg.append("g").attr("class", "regression raw comp-run-a"), xScale, yScale);
+ }
+ if (resultB && resultB[Strings.json.complexity]) {
+ this._addRegression(resultB[Strings.json.complexity], svg.append("g").attr("class", "regression raw comp-run-b"), xScale, yScale);
+ }
+
+ function drawBootstrap(result, runClass) {
+ if (!result) return;
+ var bootstrapResult = result[Strings.json.complexity][Strings.json.bootstrap];
+ if (bootstrapResult) {
+ var histogram = d3.layout.histogram().bins(xScale.ticks(100))(bootstrapResult.data);
+ var yBootstrapScale = d3.scale.linear()
+ .range([axisHeight/2, 0])
+ .domain([0, d3.max(histogram, function(d) { return d.y; })]);
+
+ let group = svg.append("g").attr("class", `bootstrap ${runClass}`);
+ var bar = group.selectAll(".bar")
+ .data(histogram)
+ .enter().append("g")
+ .attr("class", "bar")
+ .attr("transform", function(d) { return "translate(" + xScale(d.x) + "," + yBootstrapScale(d.y) + ")"; });
+ bar.append("rect")
+ .attr("x", 1)
+ .attr("y", axisHeight/2)
+ .attr("width", xScale(histogram[1].x) - xScale(histogram[0].x) - 1)
+ .attr("height", function(d) { return axisHeight/2 - yBootstrapScale(d.y); });
+
+ group = group.append("g").attr("class", "median");
+ this._addRegressionLine(group, xScale, yScale, [[bootstrapResult.median, yMin], [bootstrapResult.median, yMax]], [bootstrapResult.confidenceLow, bootstrapResult.confidenceHigh], true);
+
+ const targetFrameRate = optionsB ? optionsB["frame-rate"] : 60;
+ group.append("circle")
+ .attr("cx", xScale(bootstrapResult.median))
+ .attr("cy", yScale(msPerSecond / targetFrameRate))
+ .attr("r", 5);
+ }
+ }
+
+ drawBootstrap.call(this, resultA, "comp-run-a");
+ drawBootstrap.call(this, resultB, "comp-run-b");
+
+ function drawRawSeries(samples, runClass) {
+ if (!samples) return;
+ let group = svg.append("g")
+ .attr("class", `series raw ${runClass}`)
+ .selectAll("line")
+ .data(samples[Strings.json.complexity])
+ .enter();
+
+ group.append("line")
+ .attr("x1", d => xScale(d.complexity) - 3)
+ .attr("x2", d => xScale(d.complexity) + 3)
+ .attr("y1", d => yScale(d.frameLength) - 3)
+ .attr("y2", d => yScale(d.frameLength) + 3)
+ .attr("class", d => d.frameType === "m" ? 'mutation' : 'animation');
+ group.append("line")
+ .attr("x1", d => xScale(d.complexity) - 3)
+ .attr("x2", d => xScale(d.complexity) + 3)
+ .attr("y1", d => yScale(d.frameLength) + 3)
+ .attr("y2", d => yScale(d.frameLength) - 3)
+ .attr("class", d => d.frameType === "m" ? 'mutation' : 'animation');
+ }
+
+ drawRawSeries(samplesA, "comp-run-a");
+ drawRawSeries(samplesB, "comp-run-b");
+
+ var cursorGroup = svg.append("g").attr("class", "cursor hidden");
+ cursorGroup.append("line")
+ .attr("class", "x")
+ .attr("x1", 0)
+ .attr("x2", 0)
+ .attr("y1", yScale(yAxis.scale().domain()[0]) + 10)
+ .attr("y2", yScale(yAxis.scale().domain()[1]));
+ cursorGroup.append("line")
+ .attr("class", "y")
+ .attr("x1", xScale(xAxis.scale().domain()[0]) - 10)
+ .attr("x2", xScale(xAxis.scale().domain()[1]))
+ .attr("y1", 0)
+ .attr("y2", 0);
+ cursorGroup.append("text")
+ .attr("class", "label x")
+ .attr("x", 0)
+ .attr("y", yScale(yAxis.scale().domain()[0]) + 15)
+ .attr("baseline-shift", "-100%")
+ .attr("text-anchor", "middle");
+ cursorGroup.append("text")
+ .attr("class", "label y")
+ .attr("x", xScale(xAxis.scale().domain()[0]) - 15)
+ .attr("y", 0)
+ .attr("baseline-shift", "-30%")
+ .attr("text-anchor", "end");
+
+ var area = svg.append("rect")
+ .attr("fill", "transparent")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", size.width)
+ .attr("height", axisHeight);
+
+ area.on("mouseover", function() {
+ document.querySelector("#complexity-graph .cursor").classList.remove("hidden");
+ }).on("mouseout", function() {
+ document.querySelector("#complexity-graph .cursor").classList.add("hidden");
+ }).on("mousemove", function() {
+ var location = d3.mouse(this);
+ var location_domain = [xScale.invert(location[0]), yScale.invert(location[1])];
+ cursorGroup.select("line.x")
+ .attr("x1", location[0])
+ .attr("x2", location[0]);
+ cursorGroup.select("text.x")
+ .attr("x", location[0])
+ .text(location_domain[0].toFixed(1));
+ cursorGroup.select("line.y")
+ .attr("y1", location[1])
+ .attr("y2", location[1]);
+ cursorGroup.select("text.y")
+ .attr("y", location[1])
+ .text((msPerSecond / location_domain[1]).toFixed(1));
+ });
+ }
}
+
diff --git a/MotionMark/resources/debug-runner/motionmark.css b/MotionMark/resources/debug-runner/motionmark.css
index fa83391..167062b 100644
--- a/MotionMark/resources/debug-runner/motionmark.css
+++ b/MotionMark/resources/debug-runner/motionmark.css
@@ -832,3 +832,436 @@ body.showing-test-graph header, body.showing-test-graph nav {
#complexity-graph .bootstrap .median polygon {
fill: hsla(240, 56%, 66%, .4);
}
+
+/* -------------------------------------------------------------------------- */
+/* Comparison & Panel Controls */
+/* -------------------------------------------------------------------------- */
+
+.control-panel-container {
+ background: rgba(45, 45, 45, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 12px;
+ padding: 1.2em;
+ margin: 2.5em 0;
+ max-width: 580px;
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.2);
+}
+
+.mode-toggles {
+ display: flex;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ margin-bottom: 1.2em;
+ gap: 0.5em;
+}
+
+.mode-tab {
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: rgba(255, 255, 255, 0.6) !important;
+ font-size: 1.05em;
+ padding: 0.6em 1.2em;
+ border-radius: 0;
+ cursor: pointer;
+ font-weight: 500;
+ transition: all 0.25s ease;
+ display: inline-block !important;
+ min-width: initial !important;
+}
+
+.mode-tab:hover {
+ color: white !important;
+ background: transparent;
+}
+
+.mode-tab.active {
+ color: white !important;
+ border-bottom-color: hsl(20, 95%, 52%) !important;
+ background: transparent;
+}
+
+.control-panel {
+ display: none;
+ animation: fadeIn 0.3s ease;
+}
+
+.control-panel.active {
+ display: block;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.drop-zone {
+ border: 2px dashed rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ padding: 1.5em;
+ text-align: center;
+ color: rgba(255, 255, 255, 0.6);
+ background: rgba(255, 255, 255, 0.02);
+ transition: all 0.2s ease;
+ margin-bottom: 1em;
+}
+
+.drop-zone.drag-over {
+ border-color: hsl(20, 95%, 52%);
+ background: rgba(250, 73, 37, 0.08);
+ color: white;
+}
+
+.compare-zones-row {
+ display: flex;
+ gap: 1.2em;
+ margin-bottom: 1.5em;
+}
+
+.compare-col {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ padding: 1em;
+}
+
+.compare-col h4 {
+ margin: 0 0 0.8em 0;
+ font-size: 1.05em;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.85);
+}
+
+.compare-drop {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 1.2em 0.5em;
+ font-size: 0.9em;
+ margin-bottom: 0.8em;
+}
+
+.panel-btn {
+ border: 1.5px solid white !important;
+ color: white !important;
+ background: transparent !important;
+ padding: 0.5em 1.5em !important;
+ font-size: 1.1em !important;
+ border-radius: 6px !important;
+ cursor: pointer !important;
+ transition: all 0.2s !important;
+ display: block !important;
+}
+
+.panel-btn:hover {
+ background: rgba(255, 255, 255, 0.1) !important;
+}
+
+.panel-btn-small {
+ border: 1px solid rgba(255, 255, 255, 0.4) !important;
+ color: rgba(255, 255, 255, 0.9) !important;
+ background: transparent !important;
+ padding: 0.4em 1em !important;
+ font-size: 0.85em !important;
+ border-radius: 4px !important;
+ cursor: pointer !important;
+ transition: all 0.2s !important;
+ margin-bottom: 0.6em !important;
+ display: block !important;
+}
+
+.panel-btn-small:hover {
+ background: rgba(255, 255, 255, 0.08) !important;
+}
+
+.loaded-file-indicator {
+ font-size: 0.78em;
+ color: rgba(255, 255, 255, 0.4);
+ text-align: center;
+ word-break: break-all;
+ max-width: 100%;
+}
+
+.loaded-file-indicator.loaded {
+ color: hsl(122, 60%, 65%);
+ font-weight: 500;
+}
+
+.compare-actions {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1.5em;
+}
+
+.compare-run-btn {
+ border: none !important;
+ background: linear-gradient(135deg, hsl(20, 95%, 52%) 0%, hsl(10, 96%, 56%) 100%) !important;
+ color: white !important;
+ padding: 0.6em 2.2em !important;
+ font-size: 1.25em !important;
+ font-weight: bold !important;
+ border-radius: 8px !important;
+ cursor: pointer !important;
+ box-shadow: 0 4px 15px rgba(250, 73, 37, 0.3) !important;
+ transition: all 0.25s ease !important;
+ display: block !important;
+}
+
+.compare-run-btn:hover:not(.disabled) {
+ box-shadow: 0 6px 20px rgba(250, 73, 37, 0.5) !important;
+ transform: translateY(-1px) !important;
+}
+
+.compare-run-btn.disabled, .compare-run-btn:disabled {
+ background: rgba(255, 255, 255, 0.08) !important;
+ color: rgba(255, 255, 255, 0.2) !important;
+ box-shadow: none !important;
+ cursor: not-allowed !important;
+ transform: none !important;
+}
+
+.panel-btn-text {
+ background: transparent !important;
+ border: none !important;
+ color: rgba(255, 255, 255, 0.5) !important;
+ font-size: 1em !important;
+ cursor: pointer !important;
+ text-decoration: underline !important;
+ padding: 0.2em !important;
+ display: inline-block !important;
+ min-width: initial !important;
+}
+
+.panel-btn-text:hover {
+ color: white !important;
+}
+
+/* Overall Comparison Results Styling */
+.comparison-badge {
+ padding: 0.15em 0.45em;
+ border-radius: 8px;
+ font-size: 1.1em;
+ font-weight: bold;
+ display: inline-block;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+}
+
+.comp-badge-better {
+ background: hsla(122, 55%, 42%, 0.2) !important;
+ border: 1.5px solid hsl(122, 55%, 45%) !important;
+ color: hsl(122, 60%, 65%) !important;
+}
+
+.comp-badge-worse {
+ background: hsla(11, 72%, 45%, 0.2) !important;
+ border: 1.5px solid hsl(11, 72%, 48%) !important;
+ color: hsl(11, 75%, 65%) !important;
+}
+
+.comparison-details-row {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1.5em;
+ font-size: 0.95em;
+ color: rgba(255, 255, 255, 0.85);
+}
+
+.comparison-details-row .separator {
+ color: rgba(255, 255, 255, 0.15);
+}
+
+/* Delta cells in result tables */
+.diff-better {
+ color: hsl(122, 60%, 65%) !important;
+ font-weight: bold;
+}
+
+.diff-worse {
+ color: hsl(11, 75%, 65%) !important;
+ font-weight: bold;
+}
+
+.font-bold {
+ font-weight: bold;
+}
+
+.sub-stdev {
+ font-size: 0.82em;
+ color: rgba(255, 255, 255, 0.38);
+ margin-left: 0.25em;
+}
+
+.inline-badge {
+ font-size: 0.7em !important;
+ vertical-align: middle;
+ margin-left: 0.6em;
+}
+
+.comp-nav-legend, .comp-nav-values {
+ float: right;
+ font-weight: bold;
+}
+
+.comp-teal {
+ color: hsl(180, 70%, 48%) !important;
+}
+
+.comp-orange {
+ color: hsl(15, 90%, 58%) !important;
+}
+
+.comp-nav-legend-col {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ float: right;
+ margin-top: -0.8em;
+}
+
+/* Dynamic Graph Comparison Overlays HSL Rules */
+
+/* Baseline (A) HSL Teal */
+.comp-run-a-complexity path {
+ stroke: hsla(180, 70%, 48%, 0.7) !important;
+ stroke-width: 2px !important;
+ stroke-dasharray: 4, 4 !important;
+}
+.comp-run-a-complexity circle {
+ fill: hsl(180, 70%, 48%) !important;
+}
+.comp-run-a-rawfps path {
+ stroke: hsla(200, 70%, 50%, 0.4) !important;
+ stroke-width: 1px !important;
+}
+.comp-run-a-rawfps circle {
+ fill: hsl(200, 70%, 50%) !important;
+}
+.comp-run-a-filteredfps path {
+ stroke: hsla(190, 80%, 48%, 0.8) !important;
+ stroke-width: 2px !important;
+}
+.comp-run-a-filteredfps circle {
+ fill: hsl(190, 80%, 48%) !important;
+}
+.mean.comp-run-a line {
+ stroke: hsla(180, 70%, 45%, 0.8) !important;
+ stroke-width: 2px !important;
+}
+.mean.comp-run-a polygon {
+ fill: hsla(180, 70%, 45%, 0.05) !important;
+}
+
+/* Comparison (B) HSL Coral Orange */
+.comp-run-b-complexity path {
+ stroke: hsla(15, 90%, 58%, 0.85) !important;
+ stroke-width: 2.5px !important;
+}
+.comp-run-b-complexity circle {
+ fill: hsl(15, 90%, 58%) !important;
+}
+.comp-run-b-rawfps path {
+ stroke: hsla(35, 90%, 50%, 0.45) !important;
+ stroke-width: 1px !important;
+}
+.comp-run-b-rawfps circle {
+ fill: hsl(35, 90%, 50%) !important;
+}
+.comp-run-b-filteredfps path {
+ stroke: hsla(20, 95%, 54%, 0.85) !important;
+ stroke-width: 2.5px !important;
+}
+.comp-run-b-filteredfps circle {
+ fill: hsl(20, 95%, 54%) !important;
+}
+.mean.comp-run-b line {
+ stroke: hsla(15, 90%, 50%, 0.8) !important;
+ stroke-width: 2px !important;
+}
+.mean.comp-run-b polygon {
+ fill: hsla(15, 90%, 50%, 0.05) !important;
+}
+
+/* Complexity Graph Comparisons */
+#complexity-graph .regression.comp-run-a line {
+ stroke: hsla(180, 70%, 45%, 0.85) !important;
+ stroke-width: 2px !important;
+}
+#complexity-graph .regression.comp-run-a polygon {
+ fill: hsla(180, 70%, 45%, 0.15) !important;
+}
+#complexity-graph .regression.comp-run-b line {
+ stroke: hsla(15, 90%, 58%, 0.85) !important;
+ stroke-width: 2.5px !important;
+}
+#complexity-graph .regression.comp-run-b polygon {
+ fill: hsla(15, 90%, 58%, 0.2) !important;
+}
+
+#complexity-graph .bootstrap.comp-run-a .bar {
+ fill: hsla(180, 70%, 45%, 0.25) !important;
+}
+#complexity-graph .bootstrap.comp-run-a .median line {
+ stroke: hsla(180, 70%, 40%, 0.8) !important;
+ stroke-width: 2px !important;
+}
+#complexity-graph .bootstrap.comp-run-a .median circle {
+ fill: hsla(180, 70%, 40%) !important;
+}
+#complexity-graph .bootstrap.comp-run-b .bar {
+ fill: hsla(15, 90%, 58%, 0.25) !important;
+}
+#complexity-graph .bootstrap.comp-run-b .median line {
+ stroke: hsla(15, 90%, 50%, 0.8) !important;
+ stroke-width: 2.5px !important;
+}
+#complexity-graph .bootstrap.comp-run-b .median circle {
+ fill: hsla(15, 90%, 50%) !important;
+}
+
+#complexity-graph .series.raw.comp-run-a line {
+ stroke: hsla(180, 70%, 48%, 0.25) !important;
+}
+#complexity-graph .series.raw.comp-run-b line {
+ stroke: hsla(15, 90%, 58%, 0.3) !important;
+}
+#complexity-graph .comp-run-a-rawfps circle {
+ fill: hsl(180, 70%, 45%) !important;
+}
+#complexity-graph .comp-run-b-rawfps circle {
+ fill: hsl(15, 90%, 58%) !important;
+}
+
+.target-fps.comp-run-a {
+ stroke: hsla(180, 70%, 45%, 0.45) !important;
+}
+.target-fps.comp-run-b {
+ stroke: hsla(15, 90%, 58%, 0.5) !important;
+}
+#time-graph .cursor .comp-teal {
+ fill: hsl(180, 70%, 48%) !important;
+}
+#time-graph .cursor .comp-orange {
+ fill: hsl(15, 90%, 58%) !important;
+}
+#results .score-profile {
+ margin-bottom: 0.5em;
+}
+#results .confidence {
+ text-indent: 0 !important;
+}
+
+.compare-col.drag-over {
+ background: rgba(250, 73, 37, 0.08) !important;
+ border-color: hsl(20, 95%, 52%) !important;
+ border-style: solid !important;
+}
+
+.compare-col.drag-over .drop-zone {
+ border-color: hsl(20, 95%, 52%) !important;
+ color: white !important;
+}
+
diff --git a/MotionMark/resources/debug-runner/tests.js b/MotionMark/resources/debug-runner/tests.js
index f48d0b1..302c51b 100644
--- a/MotionMark/resources/debug-runner/tests.js
+++ b/MotionMark/resources/debug-runner/tests.js
@@ -456,3 +456,153 @@ Suites.push(new Suite("Basic canvas path suite",
}
]
));
+
+Utilities.extendObject(Headers, {
+ comparisonScore: [
+ {
+ title: "Baseline A",
+ text: function(testResultA, testResultB) {
+ if (!testResultA) return "N/A";
+ return testResultA[Strings.json.score].toFixed(2);
+ },
+ className: "right pad-left pad-right"
+ },
+ {
+ title: "Comp B",
+ text: function(testResultA, testResultB) {
+ if (!testResultB) return "N/A";
+ return testResultB[Strings.json.score].toFixed(2);
+ },
+ className: "right pad-left pad-right"
+ },
+ {
+ title: "Change %",
+ text: function(testResultA, testResultB) {
+ if (!testResultA || !testResultB) return "N/A";
+ const scoreA = testResultA[Strings.json.score];
+ const scoreB = testResultB[Strings.json.score];
+ const diff = scoreB - scoreA;
+ const percent = (diff / scoreA) * 100;
+ const percentStr = (percent >= 0 ? "+" : "") + percent.toFixed(2) + "%";
+ const className = percent >= 0 ? "diff-better" : "diff-worse";
+
+ const span = document.createElement("span");
+ span.className = className;
+ span.textContent = `${percentStr} (${(diff >= 0 ? "+" : "") + diff.toFixed(1)})`;
+ return span;
+ },
+ className: "right pad-left pad-right font-bold"
+ }
+ ],
+ comparisonDetails: [
+ {
+ title: Strings.text.graph
+ },
+ {
+ title: "Complexity Baseline (A)",
+ text: function(testResultA, testResultB, optionsA, optionsB) {
+ if (!testResultA) return "N/A";
+ const complexityVal = testResultA[Strings.json.complexity];
+ if (!complexityVal) return "N/A";
+
+ const bootstrap = complexityVal[Strings.json.bootstrap];
+ if (bootstrap) {
+ const fragment = document.createDocumentFragment();
+ const medianText = document.createTextNode(bootstrap.median.toFixed(1) + " ");
+ const subSpan = document.createElement("span");
+ subSpan.className = "sub-stdev";
+ subSpan.textContent = `[${bootstrap.confidenceLow.toFixed(1)} - ${bootstrap.confidenceHigh.toFixed(1)}]`;
+ fragment.appendChild(medianText);
+ fragment.appendChild(subSpan);
+ return fragment;
+ }
+
+ const controllerData = testResultA[Strings.json.controller];
+ if (controllerData) {
+ const fragment = document.createDocumentFragment();
+ const avgText = document.createTextNode(controllerData[Strings.json.measurements.average].toFixed(1) + " ");
+ const subSpan = document.createElement("span");
+ subSpan.className = "sub-stdev";
+ subSpan.textContent = `±${controllerData[Strings.json.measurements.percent].toFixed(1)}%`;
+ fragment.appendChild(avgText);
+ fragment.appendChild(subSpan);
+ return fragment;
+ }
+ return "N/A";
+ },
+ className: "center pad-left pad-right"
+ },
+ {
+ title: "Complexity Comp (B)",
+ text: function(testResultA, testResultB, optionsA, optionsB) {
+ if (!testResultB) return "N/A";
+ const complexityVal = testResultB[Strings.json.complexity];
+ if (!complexityVal) return "N/A";
+
+ const bootstrap = complexityVal[Strings.json.bootstrap];
+ if (bootstrap) {
+ const fragment = document.createDocumentFragment();
+ const medianText = document.createTextNode(bootstrap.median.toFixed(1) + " ");
+ const subSpan = document.createElement("span");
+ subSpan.className = "sub-stdev";
+ subSpan.textContent = `[${bootstrap.confidenceLow.toFixed(1)} - ${bootstrap.confidenceHigh.toFixed(1)}]`;
+ fragment.appendChild(medianText);
+ fragment.appendChild(subSpan);
+ return fragment;
+ }
+
+ const controllerData = testResultB[Strings.json.controller];
+ if (controllerData) {
+ const fragment = document.createDocumentFragment();
+ const avgText = document.createTextNode(controllerData[Strings.json.measurements.average].toFixed(1) + " ");
+ const subSpan = document.createElement("span");
+ subSpan.className = "sub-stdev";
+ subSpan.textContent = `±${controllerData[Strings.json.measurements.percent].toFixed(1)}%`;
+ fragment.appendChild(avgText);
+ fragment.appendChild(subSpan);
+ return fragment;
+ }
+ return "N/A";
+ },
+ className: "center pad-left pad-right"
+ },
+ {
+ title: "FPS Baseline (A)",
+ text: function(testResultA, testResultB, optionsA, optionsB) {
+ if (!testResultA) return "N/A";
+ const frameLength = testResultA[Strings.json.frameLength];
+ if (frameLength) {
+ const fragment = document.createDocumentFragment();
+ const avgText = document.createTextNode(frameLength[Strings.json.measurements.average].toFixed(1) + " ");
+ const subSpan = document.createElement("span");
+ subSpan.className = "sub-stdev";
+ subSpan.textContent = `±${frameLength[Strings.json.measurements.percent].toFixed(1)}%`;
+ fragment.appendChild(avgText);
+ fragment.appendChild(subSpan);
+ return fragment;
+ }
+ return "N/A";
+ },
+ className: "center pad-left pad-right"
+ },
+ {
+ title: "FPS Comp (B)",
+ text: function(testResultA, testResultB, optionsA, optionsB) {
+ if (!testResultB) return "N/A";
+ const frameLength = testResultB[Strings.json.frameLength];
+ if (frameLength) {
+ const fragment = document.createDocumentFragment();
+ const avgText = document.createTextNode(frameLength[Strings.json.measurements.average].toFixed(1) + " ");
+ const subSpan = document.createElement("span");
+ subSpan.className = "sub-stdev";
+ subSpan.textContent = `±${frameLength[Strings.json.measurements.percent].toFixed(1)}%`;
+ fragment.appendChild(avgText);
+ fragment.appendChild(subSpan);
+ return fragment;
+ }
+ return "N/A";
+ },
+ className: "center pad-left pad-right"
+ }
+ ]
+});