Bug 1633625 - Split out shared benchmark harness from HTML UI. Summary metric computation is still in the UI for now. r=jonco

Differential Revision: https://phabricator.services.mozilla.com/D72961
This commit is contained in:
Steve Fink 2020-05-06 02:43:47 +00:00
Родитель 02db0cf1b8
Коммит 6a8d572770
4 изменённых файлов: 795 добавлений и 694 удалений

Просмотреть файл

@ -18,7 +18,7 @@ tests.set(
obj.y = 2;
delete obj.x;
}
}
},
};
})()
);

Просмотреть файл

@ -2,32 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
// Per-frame time sampling infra. Also GC'd: hopefully will not perturb things too badly.
var numSamples = 500;
var delays = new Array(numSamples);
var gcs = new Array(numSamples);
var minorGCs = new Array(numSamples);
var majorGCs = new Array(numSamples);
var slices = new Array(numSamples);
var gcBytes = new Array(numSamples);
var mallocBytes = new Array(numSamples);
var sampleIndex = 0;
var sampleTime = 16; // ms
var gHistogram = new Map(); // {ms: count}
var stroke = {
gcslice: "rgb(255,100,0)",
minor: "rgb(0,255,100)",
initialMajor: "rgb(180,60,255)",
};
var features = {
trackingSizes: "mozMemory" in performance,
showingGCs: "mozMemory" in performance,
};
var load_paused = false;
class FrameTimer {
constructor() {
// Start time of the current active test, adjusted for any time spent
@ -72,22 +46,9 @@ class FrameTimer {
var gFrameTimer = new FrameTimer();
var latencyGraph;
var memoryGraph;
var ctx;
var memoryCtx;
// Current test state.
var activeTest = undefined;
var testDuration = undefined; // ms
var loadState = "(init)"; // One of '(active)', '(inactive)', '(N/A)'
var testState = "idle"; // One of 'idle' or 'running'.
var testStart = undefined; // ms
var testQueue = [];
// Global defaults
var globalDefaultGarbageTotal = "8M";
var globalDefaultGarbagePerFrame = "8K";
var gDefaultGarbageTotal = "8M";
var gDefaultGarbagePerFrame = "8K";
function parse_units(v) {
if (!v.length) {
@ -110,240 +71,70 @@ function parse_units(v) {
return NaN;
}
function Graph(ctx) {
this.ctx = ctx;
class AllocationLoad {
constructor(info, name) {
this.load = info;
this.load.name = this.load.name ?? name;
var { height } = ctx.canvas;
this.layout = {
xAxisLabel_Y: height - 20,
};
this._garbagePerFrame =
info.garbagePerFrame ||
parse_units(info.defaultGarbagePerFrame || gDefaultGarbagePerFrame);
this._garbageTotal =
info.garbageTotal ||
parse_units(info.defaultGarbageTotal || gDefaultGarbageTotal);
}
get name() {
return this.load.name;
}
get description() {
return this.load.description;
}
get garbagePerFrame() {
return this._garbagePerFrame;
}
set garbagePerFrame(amount) {
this._garbagePerFrame = amount;
}
get garbageTotal() {
return this._garbageTotal;
}
set garbageTotal(amount) {
this._garbageTotal = amount;
}
start() {
this.load.load(this._garbageTotal);
}
stop() {
this.load.unload();
}
reload() {
this.stop();
this.start();
}
tick() {
this.load.makeGarbage(this._garbagePerFrame);
}
is_dummy_load() {
return this.load.name == "noAllocation";
}
}
Graph.prototype.xpos = index => index * 2;
Graph.prototype.clear = function() {
var { width, height } = this.ctx.canvas;
this.ctx.clearRect(0, 0, width, height);
};
Graph.prototype.drawScale = function(delay) {
this.drawHBar(delay, `${delay}ms`, "rgb(150,150,150)");
};
Graph.prototype.draw60fps = function() {
this.drawHBar(1000 / 60, "60fps", "#00cf61", 25);
};
Graph.prototype.draw30fps = function() {
this.drawHBar(1000 / 30, "30fps", "#cf0061", 25);
};
Graph.prototype.drawAxisLabels = function(x_label, y_label) {
var ctx = this.ctx;
var { width, height } = ctx.canvas;
ctx.fillText(x_label, width / 2, this.layout.xAxisLabel_Y);
ctx.save();
ctx.rotate(Math.PI / 2);
var start = height / 2 - ctx.measureText(y_label).width / 2;
ctx.fillText(y_label, start, -width + 20);
ctx.restore();
};
Graph.prototype.drawFrame = function() {
var ctx = this.ctx;
var { width, height } = ctx.canvas;
// Draw frame to show size
ctx.strokeStyle = "rgb(0,0,0)";
ctx.fillStyle = "rgb(0,0,0)";
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(width, 0);
ctx.lineTo(width, height);
ctx.lineTo(0, height);
ctx.closePath();
ctx.stroke();
};
function LatencyGraph(ctx) {
Graph.call(this, ctx);
console.log(this.ctx);
}
LatencyGraph.prototype = Object.create(Graph.prototype);
Object.defineProperty(LatencyGraph.prototype, "constructor", {
enumerable: false,
value: LatencyGraph,
});
LatencyGraph.prototype.ypos = function(delay) {
var { height } = this.ctx.canvas;
var r = height + 100 - Math.log(delay) * 64;
if (r < 5) {
return 5;
}
return r;
};
LatencyGraph.prototype.drawHBar = function(
delay,
label,
color = "rgb(0,0,0)",
label_offset = 0
) {
var ctx = this.ctx;
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.fillText(
label,
this.xpos(numSamples) + 4 + label_offset,
this.ypos(delay) + 3
);
ctx.beginPath();
ctx.moveTo(this.xpos(0), this.ypos(delay));
ctx.lineTo(this.xpos(numSamples) + label_offset, this.ypos(delay));
ctx.stroke();
ctx.strokeStyle = "rgb(0,0,0)";
ctx.fillStyle = "rgb(0,0,0)";
};
LatencyGraph.prototype.draw = function() {
var ctx = this.ctx;
this.clear();
this.drawFrame();
for (var delay of [10, 20, 30, 50, 100, 200, 400, 800]) {
this.drawScale(delay);
}
this.draw60fps();
this.draw30fps();
var worst = 0,
worstpos = 0;
ctx.beginPath();
for (var i = 0; i < numSamples; i++) {
ctx.lineTo(this.xpos(i), this.ypos(delays[i]));
if (delays[i] >= worst) {
worst = delays[i];
worstpos = i;
}
}
ctx.stroke();
// Draw vertical lines marking minor and major GCs
if (features.showingGCs) {
ctx.strokeStyle = stroke.gcslice;
var idx = sampleIndex % numSamples;
const count = {
major: majorGCs[idx],
minor: 0,
slice: slices[idx],
};
for (let i = 0; i < numSamples; i++) {
idx = (sampleIndex + i) % numSamples;
const isMajorStart = count.major < majorGCs[idx];
if (count.slice < slices[idx]) {
if (isMajorStart) {
ctx.strokeStyle = stroke.initialMajor;
}
ctx.beginPath();
ctx.moveTo(this.xpos(idx), 0);
ctx.lineTo(this.xpos(idx), this.layout.xAxisLabel_Y);
ctx.stroke();
if (isMajorStart) {
ctx.strokeStyle = stroke.gcslice;
}
}
count.major = majorGCs[idx];
count.slice = slices[idx];
}
ctx.strokeStyle = stroke.minor;
idx = sampleIndex % numSamples;
count.minor = gcs[idx];
for (let i = 0; i < numSamples; i++) {
idx = (sampleIndex + i) % numSamples;
if (count.minor < minorGCs[idx]) {
ctx.beginPath();
ctx.moveTo(this.xpos(idx), 0);
ctx.lineTo(this.xpos(idx), 20);
ctx.stroke();
}
count.minor = minorGCs[idx];
}
}
ctx.fillStyle = "rgb(255,0,0)";
if (worst) {
ctx.fillText(
`${worst.toFixed(2)}ms`,
this.xpos(worstpos) - 10,
this.ypos(worst) - 14
);
}
// Mark and label the slowest frame
ctx.beginPath();
var where = sampleIndex % numSamples;
ctx.arc(this.xpos(where), this.ypos(delays[where]), 5, 0, Math.PI * 2, true);
ctx.fill();
ctx.fillStyle = "rgb(0,0,0)";
this.drawAxisLabels("Time", "Pause between frames (log scale)");
};
function MemoryGraph(ctx) {
Graph.call(this, ctx);
this.worstEver = this.bestEver = performance.mozMemory.zone.gcBytes;
this.limit = Math.max(
this.worstEver,
performance.mozMemory.zone.gcAllocTrigger
);
}
MemoryGraph.prototype = Object.create(Graph.prototype);
Object.defineProperty(MemoryGraph.prototype, "constructor", {
enumerable: false,
value: MemoryGraph,
});
MemoryGraph.prototype.ypos = function(size) {
var { height } = this.ctx.canvas;
var range = this.limit - this.bestEver;
var percent = (size - this.bestEver) / range;
return (1 - percent) * height * 0.9 + 20;
};
MemoryGraph.prototype.drawHBar = function(
size,
label,
color = "rgb(150,150,150)"
) {
var ctx = this.ctx;
var y = this.ypos(size);
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.fillText(label, this.xpos(numSamples) + 4, y + 3);
ctx.beginPath();
ctx.moveTo(this.xpos(0), y);
ctx.lineTo(this.xpos(numSamples), y);
ctx.stroke();
ctx.strokeStyle = "rgb(0,0,0)";
ctx.fillStyle = "rgb(0,0,0)";
};
// Current test state.
var gActiveLoad = undefined;
var testDuration = undefined; // ms
var testStart = undefined; // ms
var testQueue = [];
function format_gcBytes(bytes) {
if (bytes < 4000) {
@ -356,177 +147,6 @@ function format_gcBytes(bytes) {
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
MemoryGraph.prototype.draw = function() {
var ctx = this.ctx;
this.clear();
this.drawFrame();
var worst = 0,
worstpos = 0;
for (let i = 0; i < numSamples; i++) {
if (gcBytes[i] >= worst) {
worst = gcBytes[i];
worstpos = i;
}
if (gcBytes[i] < this.bestEver) {
this.bestEver = gcBytes[i];
}
}
if (this.worstEver < worst) {
this.worstEver = worst;
this.limit = Math.max(
this.worstEver,
performance.mozMemory.zone.gcAllocTrigger
);
}
this.drawHBar(
this.bestEver,
`${format_gcBytes(this.bestEver)} min`,
"#00cf61"
);
this.drawHBar(
this.worstEver,
`${format_gcBytes(this.worstEver)} max`,
"#cc1111"
);
this.drawHBar(
performance.mozMemory.zone.gcAllocTrigger,
`${format_gcBytes(performance.mozMemory.zone.gcAllocTrigger)} trigger`,
"#cc11cc"
);
ctx.fillStyle = "rgb(255,0,0)";
if (worst) {
ctx.fillText(
format_gcBytes(worst),
this.xpos(worstpos) - 10,
this.ypos(worst) - 14
);
}
ctx.beginPath();
var where = sampleIndex % numSamples;
ctx.arc(this.xpos(where), this.ypos(gcBytes[where]), 5, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
for (let i = 0; i < numSamples; i++) {
if (i == (sampleIndex + 1) % numSamples) {
ctx.moveTo(this.xpos(i), this.ypos(gcBytes[i]));
} else {
ctx.lineTo(this.xpos(i), this.ypos(gcBytes[i]));
}
if (i == where) {
ctx.stroke();
}
}
ctx.stroke();
this.drawAxisLabels("Time", "Heap Memory Usage");
};
function stopstart() {
const do_graph = document.getElementById("do-graph");
if (do_graph.checked) {
window.requestAnimationFrame(handler);
gFrameTimer.resume();
} else {
gFrameTimer.stop();
}
update_load_state();
}
function do_load() {
const do_load = document.getElementById("do-load");
load_paused = !do_load.checked;
console.log(`load paused: ${load_paused}`);
update_load_state();
}
var previous = 0;
function handler(timestamp) {
if (gFrameTimer.is_stopped()) {
return;
}
if (testState === "running" && timestamp - testStart > testDuration) {
end_test(timestamp);
}
if (testState == "running") {
document.getElementById("test-progress").textContent =
((testDuration - (timestamp - testStart)) / 1000).toFixed(1) + " sec";
}
if (!load_paused) {
activeTest.makeGarbage(activeTest.garbagePerFrame);
}
const delay = gFrameTimer.record_frame_callback(timestamp);
update_histogram(gHistogram, delay);
// Total time elapsed while the active test has been running.
var t = timestamp - gFrameTimer.start;
var newIndex = Math.round(t / sampleTime);
while (sampleIndex < newIndex) {
sampleIndex++;
var idx = sampleIndex % numSamples;
delays[idx] = delay;
if (features.trackingSizes) {
gcBytes[idx] = performance.mozMemory.gcBytes;
}
if (features.showingGCs) {
gcs[idx] = performance.mozMemory.gcNumber;
minorGCs[idx] = performance.mozMemory.minorGCCount;
majorGCs[idx] = performance.mozMemory.majorGCCount;
// Previous versions lacking sliceCount will fall back to assuming
// any GC activity was a major GC slice, even though that
// incorrectly includes minor GCs. Although this file is versioned
// with the code that implements the new sliceCount field, it is
// common to load the gc-ubench index.html with different browser
// versions.
slices[idx] =
performance.mozMemory.sliceCount || performance.mozMemory.gcNumber;
}
}
latencyGraph.draw();
if (memoryGraph) {
memoryGraph.draw();
}
window.requestAnimationFrame(handler);
}
function summarize(arr) {
if (!arr.length) {
return [];
}
var result = [];
var run_start = 0;
var prev = arr[0];
for (var i = 1; i <= arr.length; i++) {
if (i == arr.length || arr[i] != prev) {
if (i == run_start + 1) {
result.push(arr[i]);
} else {
result.push(prev + " x " + (i - run_start));
}
run_start = i;
}
if (i != arr.length) {
prev = arr[i];
}
}
return result;
}
function update_histogram(histogram, delay) {
// Round to a whole number of 10us intervals to provide enough resolution to
// capture a 16ms target with adequate accuracy.
@ -535,150 +155,18 @@ function update_histogram(histogram, delay) {
histogram.set(delay, ++current);
}
function reset_draw_state() {
for (var i = 0; i < numSamples; i++) {
delays[i] = 0;
}
gFrameTimer.start_recording();
sampleIndex = 0;
}
function onunload() {
if (activeTest) {
activeTest.unload();
}
activeTest = undefined;
}
function onload() {
// Load initial test duration.
duration_changed();
// Load initial garbage size.
garbage_total_changed();
garbage_per_frame_changed();
// Populate the test selection dropdown.
var select = document.getElementById("test-selection");
for (var [name, test] of tests) {
test.name = name;
var option = document.createElement("option");
option.id = name;
option.text = name;
option.title = test.description;
select.add(option);
}
// Load the initial test.
change_load("noAllocation");
// Polyfill rAF.
var requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
// Acquire our canvas.
var canvas = document.getElementById("graph");
latencyGraph = new LatencyGraph(canvas.getContext("2d"));
if (!performance.mozMemory) {
document.getElementById("memgraph-disabled").style.display = "block";
document.getElementById("track-sizes-div").style.display = "none";
}
trackHeapSizes(document.getElementById("track-sizes").checked);
update_load_state();
// Start drawing.
reset_draw_state();
window.requestAnimationFrame(handler);
}
function run_one_test() {
start_test_cycle([activeTest.name]);
}
function run_all_tests() {
start_test_cycle(tests.keys());
}
function start_test_cycle(tests_to_run) {
// Convert from an iterable to an array for pop.
testQueue = [];
for (var key of tests_to_run) {
testQueue.push(key);
}
testState = "running";
testStart = performance.now();
gHistogram.clear();
start_test(testQueue.shift());
reset_draw_state();
}
function update_load_state() {
if (activeTest.name == "noAllocation") {
loadState = "(none)";
} else if (gFrameTimer.is_stopped() || load_paused) {
loadState = "(inactive)";
} else {
loadState = "(active)";
}
document.getElementById("load-running").textContent = loadState;
}
function start_test(testName) {
change_load(testName);
console.log(`Running test: ${testName}`);
document.getElementById("test-selection").value = testName;
update_load_state();
}
function end_test(timestamp) {
document.getElementById("test-progress").textContent = "(not running)";
report_test_result(activeTest, gHistogram);
gHistogram.clear();
console.log(`Ending test ${activeTest.name}`);
if (testQueue.length) {
start_test(testQueue.shift());
testStart = timestamp;
} else {
testState = "idle";
testStart = 0;
}
reset_draw_state();
}
function report_test_result(test, histogram) {
var resultList = document.getElementById("results-display");
var resultElem = document.createElement("div");
var score = compute_test_score(histogram);
var sparks = compute_test_spark_histogram(histogram);
var params = `(${format_units(test.garbagePerFrame)},${format_units(
test.garbageTotal
)})`;
resultElem.innerHTML = `${score.toFixed(3)} ms/s : ${sparks} : ${
test.name
}${params} - ${test.description}`;
resultList.appendChild(resultElem);
}
// Compute a score based on the total ms we missed frames by per second.
function compute_test_score(histogram) {
var score = 0;
for (let [delay, count] of histogram) {
score += Math.abs((delay - 1000/60) * count);
score += Math.abs((delay - 1000 / 60) * count);
}
score = score / (testDuration / 1000);
return Math.round(score * 1000) / 1000;
}
// Build a spark-lines histogram for the test results to show with the aggregate score.
function compute_test_spark_histogram(histogram) {
function compute_spark_histogram_percents(histogram) {
var ranges = [
[-99999999, 16.6],
[16.6, 16.8],
@ -690,7 +178,7 @@ function compute_test_spark_histogram(histogram) {
[300, 99999999],
];
var rescaled = new Map();
for (let [delay,] of histogram) {
for (let [delay] of histogram) {
for (var i = 0; i < ranges.length; ++i) {
var low = ranges[i][0];
var high = ranges[i][1];
@ -704,122 +192,21 @@ function compute_test_spark_histogram(histogram) {
for (const [, count] of rescaled) {
total += count;
}
var sparks = "▁▂▃▄▅▆▇█";
var colors = [
"#aaaa00",
"#007700",
"#dd0000",
"#ff0000",
"#ff0000",
"#ff0000",
"#ff0000",
"#ff0000",
];
var line = "";
var spark = [];
for (let i = 0; i < ranges.length; ++i) {
const amt = rescaled.has(i) ? rescaled.get(i) : 0;
var spark = sparks.charAt(parseInt((amt / total) * 8));
line += `<span style="color:${colors[i]}">${spark}</span>`;
spark.push(amt / total);
}
return line;
return spark;
}
function reload_active_test() {
activeTest.unload();
activeTest.load(activeTest.garbageTotal);
}
function change_load(new_test_name) {
if (activeTest) {
activeTest.unload();
}
activeTest = tests.get(new_test_name);
if (!activeTest.garbagePerFrame) {
activeTest.garbagePerFrame = parse_units(
activeTest.defaultGarbagePerFrame || globalDefaultGarbagePerFrame
);
}
if (!activeTest.garbageTotal) {
activeTest.garbageTotal = parse_units(
activeTest.defaultGarbageTotal || globalDefaultGarbageTotal
);
}
document.getElementById("garbage-per-frame").value = format_units(
activeTest.garbagePerFrame
);
document.getElementById("garbage-total").value = format_units(
activeTest.garbageTotal
);
activeTest.load(activeTest.garbageTotal);
update_load_state();
}
function duration_changed() {
var durationInput = document.getElementById("test-duration");
testDuration = parseInt(durationInput.value) * 1000;
console.log(`Updated test duration to: ${testDuration / 1000} seconds`);
}
function test_changed() {
var select = document.getElementById("test-selection");
console.log(`Switching to test: ${select.value}`);
change_load(select.value);
gHistogram.clear();
reset_draw_state();
}
function format_units(n) {
n = String(n);
if (n.length > 9 && n.substr(-9) == "000000000") {
return n.substr(0, n.length - 9) + "G";
} else if (n.length > 9 && n.substr(-6) == "000000") {
return n.substr(0, n.length - 6) + "M";
} else if (n.length > 3 && n.substr(-3) == "000") {
return n.substr(0, n.length - 3) + "K";
}
return String(n);
}
function garbage_total_changed() {
var value = parse_units(document.getElementById("garbage-total").value);
if (isNaN(value)) {
return;
}
if (activeTest) {
activeTest.garbageTotal = value;
console.log(`Updated garbage-total to ${activeTest.garbageTotal} items`);
reload_active_test();
}
gHistogram.clear();
reset_draw_state();
}
function garbage_per_frame_changed() {
var value = parse_units(document.getElementById("garbage-per-frame").value);
if (isNaN(value)) {
return;
}
if (activeTest) {
activeTest.garbagePerFrame = value;
console.log(
`Updated garbage-per-frame to ${activeTest.garbagePerFrame} items`
);
}
}
function trackHeapSizes(track) {
features.trackingSizes = track;
var canvas = document.getElementById("memgraph");
if (features.trackingSizes) {
canvas.style.display = "block";
memoryGraph = new MemoryGraph(canvas.getContext("2d"));
} else {
canvas.style.display = "none";
memoryGraph = null;
}
function change_load_internal(new_load_name) {
if (gActiveLoad) {
gActiveLoad.stop();
}
gActiveLoad = new AllocationLoad(tests.get(new_load_name));
gActiveLoad.start();
}

Просмотреть файл

@ -7,8 +7,9 @@
<title>GC uBench</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Benchmark harness -->
<!-- Benchmark harness and UI -->
<script src="harness.js"></script>
<script src="ui.js"></script>
<!-- List of garbage-creating test loads -->
<script src="test_list.js"></script>
@ -45,7 +46,7 @@
<div>
Allocation load:
<select id="test-selection" required onchange="test_changed()"></select>
<select id="test-selection" required onchange="onLoadChange()"></select>
<span id="load-running">(init)</span>
</div>

Просмотреть файл

@ -0,0 +1,713 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var features = {
trackingSizes: "mozMemory" in performance,
showingGCs: "mozMemory" in performance,
};
var stroke = {
gcslice: "rgb(255,100,0)",
minor: "rgb(0,255,100)",
initialMajor: "rgb(180,60,255)",
};
// Per-frame time sampling infra. Also GC'd: hopefully will not perturb things too badly.
var numSamples = 500;
var sampleIndex = 0;
var sampleTime = 16; // ms
var gHistogram = new Map(); // {ms: count}
var delays = new Array(numSamples);
var gcs = new Array(numSamples);
var minorGCs = new Array(numSamples);
var majorGCs = new Array(numSamples);
var slices = new Array(numSamples);
var gcBytes = new Array(numSamples);
var mallocBytes = new Array(numSamples);
var latencyGraph;
var memoryGraph;
var ctx;
var memoryCtx;
var loadState = "(init)"; // One of '(active)', '(inactive)', '(N/A)'
var testState = "idle"; // One of 'idle' or 'running'.
var load_paused = false;
function parse_units(v) {
if (!v.length) {
return NaN;
}
var lastChar = v[v.length - 1].toLowerCase();
if (!isNaN(parseFloat(lastChar))) {
return parseFloat(v);
}
var units = parseFloat(v.substr(0, v.length - 1));
if (lastChar == "k") {
return units * 1e3;
}
if (lastChar == "m") {
return units * 1e6;
}
if (lastChar == "g") {
return units * 1e9;
}
return NaN;
}
function Graph(ctx) {
this.ctx = ctx;
var { height } = ctx.canvas;
this.layout = {
xAxisLabel_Y: height - 20,
};
}
Graph.prototype.xpos = index => index * 2;
Graph.prototype.clear = function() {
var { width, height } = this.ctx.canvas;
this.ctx.clearRect(0, 0, width, height);
};
Graph.prototype.drawScale = function(delay) {
this.drawHBar(delay, `${delay}ms`, "rgb(150,150,150)");
};
Graph.prototype.draw60fps = function() {
this.drawHBar(1000 / 60, "60fps", "#00cf61", 25);
};
Graph.prototype.draw30fps = function() {
this.drawHBar(1000 / 30, "30fps", "#cf0061", 25);
};
Graph.prototype.drawAxisLabels = function(x_label, y_label) {
var ctx = this.ctx;
var { width, height } = ctx.canvas;
ctx.fillText(x_label, width / 2, this.layout.xAxisLabel_Y);
ctx.save();
ctx.rotate(Math.PI / 2);
var start = height / 2 - ctx.measureText(y_label).width / 2;
ctx.fillText(y_label, start, -width + 20);
ctx.restore();
};
Graph.prototype.drawFrame = function() {
var ctx = this.ctx;
var { width, height } = ctx.canvas;
// Draw frame to show size
ctx.strokeStyle = "rgb(0,0,0)";
ctx.fillStyle = "rgb(0,0,0)";
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(width, 0);
ctx.lineTo(width, height);
ctx.lineTo(0, height);
ctx.closePath();
ctx.stroke();
};
function LatencyGraph(ctx) {
Graph.call(this, ctx);
console.log(this.ctx);
}
LatencyGraph.prototype = Object.create(Graph.prototype);
Object.defineProperty(LatencyGraph.prototype, "constructor", {
enumerable: false,
value: LatencyGraph,
});
LatencyGraph.prototype.ypos = function(delay) {
var { height } = this.ctx.canvas;
var r = height + 100 - Math.log(delay) * 64;
if (r < 5) {
return 5;
}
return r;
};
LatencyGraph.prototype.drawHBar = function(
delay,
label,
color = "rgb(0,0,0)",
label_offset = 0
) {
var ctx = this.ctx;
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.fillText(
label,
this.xpos(numSamples) + 4 + label_offset,
this.ypos(delay) + 3
);
ctx.beginPath();
ctx.moveTo(this.xpos(0), this.ypos(delay));
ctx.lineTo(this.xpos(numSamples) + label_offset, this.ypos(delay));
ctx.stroke();
ctx.strokeStyle = "rgb(0,0,0)";
ctx.fillStyle = "rgb(0,0,0)";
};
LatencyGraph.prototype.draw = function() {
var ctx = this.ctx;
this.clear();
this.drawFrame();
for (var delay of [10, 20, 30, 50, 100, 200, 400, 800]) {
this.drawScale(delay);
}
this.draw60fps();
this.draw30fps();
var worst = 0,
worstpos = 0;
ctx.beginPath();
for (var i = 0; i < numSamples; i++) {
ctx.lineTo(this.xpos(i), this.ypos(delays[i]));
if (delays[i] >= worst) {
worst = delays[i];
worstpos = i;
}
}
ctx.stroke();
// Draw vertical lines marking minor and major GCs
if (features.showingGCs) {
ctx.strokeStyle = stroke.gcslice;
var idx = sampleIndex % numSamples;
const count = {
major: majorGCs[idx],
minor: 0,
slice: slices[idx],
};
for (let i = 0; i < numSamples; i++) {
idx = (sampleIndex + i) % numSamples;
const isMajorStart = count.major < majorGCs[idx];
if (count.slice < slices[idx]) {
if (isMajorStart) {
ctx.strokeStyle = stroke.initialMajor;
}
ctx.beginPath();
ctx.moveTo(this.xpos(idx), 0);
ctx.lineTo(this.xpos(idx), this.layout.xAxisLabel_Y);
ctx.stroke();
if (isMajorStart) {
ctx.strokeStyle = stroke.gcslice;
}
}
count.major = majorGCs[idx];
count.slice = slices[idx];
}
ctx.strokeStyle = stroke.minor;
idx = sampleIndex % numSamples;
count.minor = gcs[idx];
for (let i = 0; i < numSamples; i++) {
idx = (sampleIndex + i) % numSamples;
if (count.minor < minorGCs[idx]) {
ctx.beginPath();
ctx.moveTo(this.xpos(idx), 0);
ctx.lineTo(this.xpos(idx), 20);
ctx.stroke();
}
count.minor = minorGCs[idx];
}
}
ctx.fillStyle = "rgb(255,0,0)";
if (worst) {
ctx.fillText(
`${worst.toFixed(2)}ms`,
this.xpos(worstpos) - 10,
this.ypos(worst) - 14
);
}
// Mark and label the slowest frame
ctx.beginPath();
var where = sampleIndex % numSamples;
ctx.arc(this.xpos(where), this.ypos(delays[where]), 5, 0, Math.PI * 2, true);
ctx.fill();
ctx.fillStyle = "rgb(0,0,0)";
this.drawAxisLabels("Time", "Pause between frames (log scale)");
};
function MemoryGraph(ctx) {
Graph.call(this, ctx);
this.worstEver = this.bestEver = performance.mozMemory.zone.gcBytes;
this.limit = Math.max(
this.worstEver,
performance.mozMemory.zone.gcAllocTrigger
);
}
MemoryGraph.prototype = Object.create(Graph.prototype);
Object.defineProperty(MemoryGraph.prototype, "constructor", {
enumerable: false,
value: MemoryGraph,
});
MemoryGraph.prototype.ypos = function(size) {
var { height } = this.ctx.canvas;
var range = this.limit - this.bestEver;
var percent = (size - this.bestEver) / range;
return (1 - percent) * height * 0.9 + 20;
};
MemoryGraph.prototype.drawHBar = function(
size,
label,
color = "rgb(150,150,150)"
) {
var ctx = this.ctx;
var y = this.ypos(size);
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.fillText(label, this.xpos(numSamples) + 4, y + 3);
ctx.beginPath();
ctx.moveTo(this.xpos(0), y);
ctx.lineTo(this.xpos(numSamples), y);
ctx.stroke();
ctx.strokeStyle = "rgb(0,0,0)";
ctx.fillStyle = "rgb(0,0,0)";
};
MemoryGraph.prototype.draw = function() {
var ctx = this.ctx;
this.clear();
this.drawFrame();
var worst = 0,
worstpos = 0;
for (let i = 0; i < numSamples; i++) {
if (gcBytes[i] >= worst) {
worst = gcBytes[i];
worstpos = i;
}
if (gcBytes[i] < this.bestEver) {
this.bestEver = gcBytes[i];
}
}
if (this.worstEver < worst) {
this.worstEver = worst;
this.limit = Math.max(
this.worstEver,
performance.mozMemory.zone.gcAllocTrigger
);
}
this.drawHBar(
this.bestEver,
`${format_gcBytes(this.bestEver)} min`,
"#00cf61"
);
this.drawHBar(
this.worstEver,
`${format_gcBytes(this.worstEver)} max`,
"#cc1111"
);
this.drawHBar(
performance.mozMemory.zone.gcAllocTrigger,
`${format_gcBytes(performance.mozMemory.zone.gcAllocTrigger)} trigger`,
"#cc11cc"
);
ctx.fillStyle = "rgb(255,0,0)";
if (worst) {
ctx.fillText(
format_gcBytes(worst),
this.xpos(worstpos) - 10,
this.ypos(worst) - 14
);
}
ctx.beginPath();
var where = sampleIndex % numSamples;
ctx.arc(this.xpos(where), this.ypos(gcBytes[where]), 5, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
for (let i = 0; i < numSamples; i++) {
if (i == (sampleIndex + 1) % numSamples) {
ctx.moveTo(this.xpos(i), this.ypos(gcBytes[i]));
} else {
ctx.lineTo(this.xpos(i), this.ypos(gcBytes[i]));
}
if (i == where) {
ctx.stroke();
}
}
ctx.stroke();
this.drawAxisLabels("Time", "Heap Memory Usage");
};
function stopstart() {
const do_graph = document.getElementById("do-graph");
if (do_graph.checked) {
window.requestAnimationFrame(handler);
gFrameTimer.resume();
} else {
gFrameTimer.stop();
}
update_load_state_indicator();
}
function do_load() {
const do_load = document.getElementById("do-load");
load_paused = !do_load.checked;
console.log(`load paused: ${load_paused}`);
update_load_state_indicator();
}
var previous = 0;
function handler(timestamp) {
if (gFrameTimer.is_stopped()) {
return;
}
if (testState === "running" && timestamp - testStart > testDuration) {
end_test(timestamp);
}
if (testState == "running") {
document.getElementById("test-progress").textContent =
((testDuration - (timestamp - testStart)) / 1000).toFixed(1) + " sec";
}
if (!load_paused) {
gActiveLoad.tick();
}
const delay = gFrameTimer.record_frame_callback(timestamp);
update_histogram(gHistogram, delay);
// Total time elapsed while the active test has been running.
var t = timestamp - gFrameTimer.start;
var newIndex = Math.round(t / sampleTime);
while (sampleIndex < newIndex) {
sampleIndex++;
var idx = sampleIndex % numSamples;
delays[idx] = delay;
if (features.trackingSizes) {
gcBytes[idx] = performance.mozMemory.gcBytes;
}
if (features.showingGCs) {
gcs[idx] = performance.mozMemory.gcNumber;
minorGCs[idx] = performance.mozMemory.minorGCCount;
majorGCs[idx] = performance.mozMemory.majorGCCount;
// Previous versions lacking sliceCount will fall back to assuming
// any GC activity was a major GC slice, even though that
// incorrectly includes minor GCs. Although this file is versioned
// with the code that implements the new sliceCount field, it is
// common to load the gc-ubench index.html with different browser
// versions.
slices[idx] =
performance.mozMemory.sliceCount || performance.mozMemory.gcNumber;
}
}
latencyGraph.draw();
if (memoryGraph) {
memoryGraph.draw();
}
window.requestAnimationFrame(handler);
}
// For interactive debugging.
//
// ['a', 'b', 'b', 'b', 'c', 'c'] => ['a', 'b x 3', 'c x 2']
function summarize(arr) {
if (!arr.length) {
return [];
}
var result = [];
var run_start = 0;
var prev = arr[0];
for (var i = 1; i <= arr.length; i++) {
if (i == arr.length || arr[i] != prev) {
if (i == run_start + 1) {
result.push(arr[i]);
} else {
result.push(prev + " x " + (i - run_start));
}
run_start = i;
}
if (i != arr.length) {
prev = arr[i];
}
}
return result;
}
function reset_draw_state() {
for (var i = 0; i < numSamples; i++) {
delays[i] = 0;
}
gFrameTimer.start_recording();
sampleIndex = 0;
}
function onunload() {
if (gActiveLoad) {
gActiveLoad.stop();
}
gActiveLoad = undefined;
}
function onload() {
// The order of `tests` is currently based on their asynchronous load
// order, rather than the listed order. Rearrange by extracting the test
// names from their filenames, which is kind of gross.
_tests = tests;
tests = new Map();
foreach_test_file(fn => {
// "benchmarks/foo.js" => "foo"
const name = fn.split(/\//)[1].split(/\./)[0];
tests.set(name, _tests.get(name));
});
_tests = undefined;
// Load initial test duration.
duration_changed();
// Load initial garbage size.
garbage_total_changed();
garbage_per_frame_changed();
// Populate the test selection dropdown.
var select = document.getElementById("test-selection");
for (var [name, test] of tests) {
test.name = name;
var option = document.createElement("option");
option.id = name;
option.text = name;
option.title = test.description;
select.add(option);
}
// Load the initial test.
change_load("noAllocation");
document.getElementById("test-selection").value = "noAllocation";
// Polyfill rAF.
var requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
// Acquire our canvas.
var canvas = document.getElementById("graph");
latencyGraph = new LatencyGraph(canvas.getContext("2d"));
if (!performance.mozMemory) {
document.getElementById("memgraph-disabled").style.display = "block";
document.getElementById("track-sizes-div").style.display = "none";
}
trackHeapSizes(document.getElementById("track-sizes").checked);
update_load_state_indicator();
// Start drawing.
reset_draw_state();
window.requestAnimationFrame(handler);
}
function run_one_test() {
start_test_cycle([gActiveLoad.name]);
}
function run_all_tests() {
start_test_cycle(tests.keys());
}
function start_test_cycle(tests_to_run) {
// Convert from an iterable to an array for pop.
testQueue = [];
for (var key of tests_to_run) {
testQueue.push(key);
}
testState = "running";
testStart = performance.now();
gHistogram.clear();
start_test(testQueue.shift());
reset_draw_state();
}
function update_load_state_indicator() {
if (gActiveLoad.is_dummy_load()) {
loadState = "(none)";
} else if (gFrameTimer.is_stopped() || load_paused) {
loadState = "(inactive)";
} else {
loadState = "(active)";
}
document.getElementById("load-running").textContent = loadState;
}
function start_test(testName) {
change_load(testName);
console.log(`Running test: ${testName}`);
document.getElementById("test-selection").value = testName;
update_load_state_indicator();
}
function end_test(timestamp) {
document.getElementById("test-progress").textContent = "(not running)";
report_test_result(gActiveLoad, gHistogram);
gHistogram.clear();
console.log(`Ending test ${gActiveLoad.name}`);
if (testQueue.length) {
start_test(testQueue.shift());
testStart = timestamp;
} else {
testState = "idle";
testStart = 0;
}
reset_draw_state();
}
function compute_test_spark_histogram(histogram) {
const percents = compute_spark_histogram_percents(histogram);
var sparks = "▁▂▃▄▅▆▇█";
var colors = [
"#aaaa00",
"#007700",
"#dd0000",
"#ff0000",
"#ff0000",
"#ff0000",
"#ff0000",
"#ff0000",
];
var line = "";
for (let i = 0; i < percents.length; ++i) {
var spark = sparks.charAt(parseInt(percents[i] * sparks.length));
line += `<span style="color:${colors[i]}">${spark}</span>`;
}
return line;
}
function format_units(n) {
n = String(n);
if (n.length > 9 && n.substr(-9) == "000000000") {
return n.substr(0, n.length - 9) + "G";
} else if (n.length > 9 && n.substr(-6) == "000000") {
return n.substr(0, n.length - 6) + "M";
} else if (n.length > 3 && n.substr(-3) == "000") {
return n.substr(0, n.length - 3) + "K";
}
return String(n);
}
function report_test_result(load, histogram) {
var resultList = document.getElementById("results-display");
var resultElem = document.createElement("div");
var score = compute_test_score(histogram);
var sparks = compute_test_spark_histogram(histogram);
var params = `(${format_units(load.garbagePerFrame)},${format_units(
load.garbageTotal
)})`;
resultElem.innerHTML = `${score.toFixed(3)} ms/s : ${sparks} : ${
load.name
}${params} - ${load.description}`;
resultList.appendChild(resultElem);
}
function change_load(new_load_name) {
console.log(`change_load(${new_load_name})`);
change_load_internal(new_load_name);
document.getElementById("garbage-per-frame").value = format_units(
gActiveLoad.garbagePerFrame
);
document.getElementById("garbage-total").value = format_units(
gActiveLoad.garbageTotal
);
update_load_state_indicator();
}
function duration_changed() {
var durationInput = document.getElementById("test-duration");
testDuration = parseInt(durationInput.value) * 1000;
console.log(`Updated test duration to: ${testDuration / 1000} seconds`);
}
function onLoadChange() {
var select = document.getElementById("test-selection");
console.log(`Switching to test: ${select.value}`);
change_load(select.value);
gHistogram.clear();
reset_draw_state();
}
function garbage_total_changed() {
var value = parse_units(document.getElementById("garbage-total").value);
if (isNaN(value)) {
return;
}
if (gActiveLoad) {
gActiveLoad.garbageTotal = value;
console.log(`Updated garbage-total to ${gActiveLoad.garbageTotal} items`);
gActiveLoad.reload();
}
gHistogram.clear();
reset_draw_state();
}
function garbage_per_frame_changed() {
var value = parse_units(document.getElementById("garbage-per-frame").value);
if (isNaN(value)) {
return;
}
if (gActiveLoad) {
gActiveLoad.garbagePerFrame = value;
console.log(
`Updated garbage-per-frame to ${gActiveLoad.garbagePerFrame} items`
);
}
}
function trackHeapSizes(track) {
features.trackingSizes = track;
var canvas = document.getElementById("memgraph");
if (features.trackingSizes) {
canvas.style.display = "block";
memoryGraph = new MemoryGraph(canvas.getContext("2d"));
} else {
canvas.style.display = "none";
memoryGraph = null;
}
}