Source code

Revision control

Copy as Markdown

Other Tools

/* 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 stroke = {
gcslice: "rgb(255,100,0)",
minor: "rgb(0,255,100)",
initialMajor: "rgb(180,60,255)",
};
var numSamples = 500;
var tests = new Map();
var gHistogram = new Map(); // {ms: count}
var gHistory = new FrameHistory(numSamples);
var gPerf = new PerfTracker();
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 enabled = { trackingSizes: false };
var gMemory = performance.mozMemory?.gc || performance.mozMemory || {};
var Firefox = class extends Host {
start_turn() {
// Handled by Gecko.
}
end_turn() {
// Handled by Gecko.
}
suspend(duration) {
// Not used; requestAnimationFrame takes its place.
throw new Error("unimplemented");
}
get minorGCCount() {
return gMemory.minorGCCount;
}
get majorGCCount() {
return gMemory.majorGCCount;
}
get GCSliceCount() {
return gMemory.sliceCount;
}
get gcBytes() {
return gMemory.zone.gcBytes;
}
get mallocBytes() {
return gMemory.zone.mallocBytes;
}
get gcAllocTrigger() {
return gMemory.zone.gcAllocTrigger;
}
get mallocTrigger() {
return gMemory.zone.mallocTriggerBytes;
}
features = {
haveMemorySizes: 'gcBytes' in gMemory,
haveGCCounts: 'majorGCCount' in gMemory,
};
};
var gHost = new Firefox();
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;
}
var Graph = class {
constructor(canvas) {
this.ctx = canvas.getContext('2d');
// Adjust scale for high-DPI displays.
this.scale = window.devicePixelRatio || 1;
let rect = canvas.getBoundingClientRect();
canvas.width = Math.floor(rect.width * this.scale);
canvas.height = Math.floor(rect.height * this.scale);
canvas.style.width = rect.width;
canvas.style.height = rect.height;
// Record canvas size to draw into.
this.width = canvas.width;
this.height = canvas.height;
this.layout = {
xAxisLabel_Y: this.height - 20 * this.scale,
};
}
xpos(index) {
return (index / numSamples) * (this.width - 100 * this.scale);
}
clear() {
this.ctx.clearRect(0, 0, this.width, this.height);
}
drawScale(delay) {
this.drawHBar(delay, `${delay}ms`, "rgb(150,150,150)");
}
draw60fps() {
this.drawHBar(1000 / 60, "60fps", "#00cf61", 25);
}
draw30fps() {
this.drawHBar(1000 / 30, "30fps", "#cf0061", 25);
}
drawAxisLabels(x_label, y_label) {
const ctx = this.ctx;
ctx.font = `${10 * this.scale}px sans-serif`;
ctx.fillText(x_label, this.width / 2, this.layout.xAxisLabel_Y);
ctx.save();
ctx.rotate(Math.PI / 2);
var start = this.height / 2 - ctx.measureText(y_label).width / 2;
ctx.fillText(y_label, start, -this.width + 20 * this.scale);
ctx.restore();
}
drawFrame() {
const ctx = this.ctx;
const width = this.width;
const height = this.height;
// 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();
}
};
var LatencyGraph = class extends Graph {
constructor(ctx) {
super(ctx);
}
ypos(delay) {
return this.height + this.scale * (100 - Math.log(delay) * 64);
}
drawHBar(delay, label, color = "rgb(0,0,0)", label_offset = 0) {
const ctx = this.ctx;
let y = this.ypos(delay);
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)";
}
draw() {
const 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 (let i = 0; i < numSamples; i++) {
ctx.lineTo(this.xpos(i), this.ypos(gHistory.delays[i]));
if (gHistory.delays[i] >= worst) {
worst = gHistory.delays[i];
worstpos = i;
}
}
ctx.stroke();
// Draw vertical lines marking minor and major GCs
if (gHost.features.haveGCCounts) {
ctx.strokeStyle = stroke.gcslice;
let idx = sampleIndex % numSamples;
const count = {
major: gHistory.majorGCs[idx],
minor: 0,
slice: gHistory.slices[idx],
};
for (let i = 0; i < numSamples; i++) {
idx = (sampleIndex + i) % numSamples;
const isMajorStart = count.major < gHistory.majorGCs[idx];
if (count.slice < gHistory.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 = gHistory.majorGCs[idx];
count.slice = gHistory.slices[idx];
}
ctx.strokeStyle = stroke.minor;
idx = sampleIndex % numSamples;
count.minor = gHistory.minorGCs[idx];
for (let i = 0; i < numSamples; i++) {
idx = (sampleIndex + i) % numSamples;
if (count.minor < gHistory.minorGCs[idx]) {
ctx.beginPath();
ctx.moveTo(this.xpos(idx), 0);
ctx.lineTo(this.xpos(idx), 20);
ctx.stroke();
}
count.minor = gHistory.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(gHistory.delays[where]),
5,
0,
Math.PI * 2,
true
);
ctx.fill();
ctx.fillStyle = "rgb(0,0,0)";
this.drawAxisLabels("Time", "Pause between frames (log scale)");
}
};
var MemoryGraph = class extends Graph {
constructor(ctx) {
super(ctx);
this.range = 1;
}
ypos(size) {
const percent = size / this.range;
return (1 - percent) * this.height * 0.9 + this.scale * 20;
}
drawHBarForBytes(size, name, color) {
this.drawHBar(size, `${format_bytes(size)} ${name}`, color)
}
drawHBar(size, label, color) {
const ctx = this.ctx;
const 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)";
}
draw() {
const ctx = this.ctx;
this.clear();
this.drawFrame();
let gcMaxPos = 0;
let mallocMaxPos = 0;
let gcMax = 0;
let mallocMax = 0;
for (let i = 0; i < numSamples; i++) {
if (gHistory.gcBytes[i] >= gcMax) {
gcMax = gHistory.gcBytes[i];
gcMaxPos = i;
}
if (gHistory.mallocBytes[i] >= mallocMax) {
mallocMax = gHistory.mallocBytes[i];
mallocMaxPos = i;
}
}
this.range = Math.max(gcMax, mallocMax, gHost.gcAllocTrigger, gHost.mallocTrigger);
this.drawHBarForBytes(gcMax, "GC max", "#00cf61");
this.drawHBarForBytes(mallocMax, "Malloc max", "#cc1111");
this.drawHBarForBytes(gHost.gcAllocTrigger, "GC trigger", "#cc11cc");
this.drawHBarForBytes(gHost.mallocTrigger, "Malloc trigger", "#cc11cc");
ctx.fillStyle = "rgb(255,0,0)";
if (gcMax !== 0) {
ctx.fillText(
format_bytes(gcMax),
this.xpos(gcMaxPos) - 10,
this.ypos(gcMax) - 14
);
}
if (mallocMax !== 0) {
ctx.fillText(
format_bytes(mallocMax),
this.xpos(mallocMaxPos) - 10,
this.ypos(mallocMax) - 14
);
}
const where = sampleIndex % numSamples;
ctx.beginPath();
ctx.arc(
this.xpos(where),
this.ypos(gHistory.gcBytes[where]),
5,
0,
Math.PI * 2,
true
);
ctx.fill();
ctx.beginPath();
ctx.arc(
this.xpos(where),
this.ypos(gHistory.mallocBytes[where]),
5,
0,
Math.PI * 2,
true
);
ctx.fill();
ctx.beginPath();
for (let i = 0; i < numSamples; i++) {
let x = this.xpos(i);
let y = this.ypos(gHistory.gcBytes[i]);
if (i == (sampleIndex + 1) % numSamples) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
if (i == where) {
ctx.stroke();
}
}
ctx.stroke();
ctx.beginPath();
for (let i = 0; i < numSamples; i++) {
let x = this.xpos(i);
let y = this.ypos(gHistory.mallocBytes[i]);
if (i == (sampleIndex + 1) % numSamples) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
if (i == where) {
ctx.stroke();
}
}
ctx.stroke();
ctx.fillStyle = "rgb(0,0,0)";
this.drawAxisLabels("Time", "Heap Memory Usage");
}
};
function onUpdateDisplayChanged() {
const do_graph = document.getElementById("do-graph");
if (do_graph.checked) {
window.requestAnimationFrame(handler);
gHistory.resume();
} else {
gHistory.pause();
}
update_load_state_indicator();
}
function onDoLoadChange() {
const do_load = document.getElementById("do-load");
gLoadMgr.paused = !do_load.checked;
console.log(`load paused: ${gLoadMgr.paused}`);
update_load_state_indicator();
}
var previous = 0;
function handler(timestamp) {
if (gHistory.is_stopped()) {
return;
}
const completed = gLoadMgr.tick(timestamp);
if (completed) {
end_test(timestamp, gLoadMgr.lastActive);
if (!gLoadMgr.stopped()) {
start_test();
}
update_load_display();
}
if (testState == "running") {
document.getElementById("test-progress").textContent =
(gLoadMgr.currentLoadRemaining(timestamp) / 1000).toFixed(1) + " sec";
}
const delay = gHistory.on_frame(timestamp);
update_histogram(gHistogram, delay);
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 (let 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() {
gHistory.reset();
}
function onunload() {
if (gLoadMgr) {
gLoadMgr.deactivateLoad();
}
}
async function onload() {
// Collect all test loads into the `tests` Map.
let imports = [];
foreach_test_file(path => imports.push(import("./" + path)));
await Promise.all(imports);
// 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;
gLoadMgr = new AllocationLoadManager(tests);
// Load initial test duration.
duration_changed();
// Load initial garbage size.
garbage_piles_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.
gLoadMgr.setActiveLoad(gLoadMgr.getByName("noAllocation"));
update_load_display();
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);
if (!gHost.features.haveMemorySizes) {
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();
gHistory.start();
// Start drawing.
reset_draw_state();
window.requestAnimationFrame(handler);
}
function run_one_test() {
start_test_cycle([gLoadMgr.activeLoad().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.
const duration = gLoadMgr.testDurationMS / 1000;
const mutators = tests_to_run.map(name => new SingleMutatorSequencer(gLoadMgr.getByName(name), gPerf, duration));
const sequencer = new ChainSequencer(mutators);
gLoadMgr.startSequencer(sequencer);
testState = "running";
gHistogram.clear();
reset_draw_state();
}
function update_load_state_indicator() {
if (
!gLoadMgr.load_running() ||
gLoadMgr.activeLoad().name == "noAllocation"
) {
loadState = "(none)";
} else if (gHistory.is_stopped() || gLoadMgr.paused) {
loadState = "(inactive)";
} else {
loadState = "(active)";
}
document.getElementById("load-running").textContent = loadState;
}
function start_test() {
console.log(`Running test: ${gLoadMgr.activeLoad().name}`);
document.getElementById("test-selection").value = gLoadMgr.activeLoad().name;
update_load_state_indicator();
}
function end_test(timestamp, load) {
document.getElementById("test-progress").textContent = "(not running)";
report_test_result(load, gHistogram);
gHistogram.clear();
console.log(`Ending test ${load.name}`);
if (gLoadMgr.stopped()) {
testState = "idle";
}
update_load_state_indicator();
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 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_num(load.garbagePerFrame)},${format_num(
load.garbagePiles
)})`;
resultElem.innerHTML = `${score.toFixed(3)} ms/s : ${sparks} : ${
load.name
}${params} - ${load.description}`;
resultList.appendChild(resultElem);
}
function update_load_display() {
const garbage = gLoadMgr.activeLoad()
? gLoadMgr.activeLoad().garbagePerFrame
: parse_units(gDefaultGarbagePerFrame);
document.getElementById("garbage-per-frame").value = format_num(garbage);
const piles = gLoadMgr.activeLoad()
? gLoadMgr.activeLoad().garbagePiles
: parse_units(gDefaultGarbagePiles);
document.getElementById("garbage-piles").value = format_num(piles);
update_load_state_indicator();
}
function duration_changed() {
var durationInput = document.getElementById("test-duration");
gLoadMgr.testDurationMS = parseInt(durationInput.value) * 1000;
console.log(
`Updated test duration to: ${gLoadMgr.testDurationMS / 1000} seconds`
);
}
function onLoadChange() {
var select = document.getElementById("test-selection");
console.log(`Switching to test: ${select.value}`);
gLoadMgr.setActiveLoad(gLoadMgr.getByName(select.value));
update_load_display();
gHistogram.clear();
reset_draw_state();
}
function garbage_piles_changed() {
const input = document.getElementById("garbage-piles");
const value = parse_units(input.value);
if (isNaN(value)) {
update_load_display();
return;
}
if (gLoadMgr.load_running()) {
gLoadMgr.change_garbagePiles(value);
console.log(
`Updated garbage-piles to ${gLoadMgr.activeLoad().garbagePiles} items`
);
}
gHistogram.clear();
reset_draw_state();
}
function garbage_per_frame_changed() {
const input = document.getElementById("garbage-per-frame");
var value = parse_units(input.value);
if (isNaN(value)) {
update_load_display();
return;
}
if (gLoadMgr.load_running()) {
gLoadMgr.change_garbagePerFrame(value);
console.log(
`Updated garbage-per-frame to ${
gLoadMgr.activeLoad().garbagePerFrame
} items`
);
}
}
function trackHeapSizes(track) {
enabled.trackingSizes = track && gHost.features.haveMemorySizes;
var canvas = document.getElementById("memgraph");
if (enabled.trackingSizes) {
canvas.style.display = "block";
memoryGraph = new MemoryGraph(canvas);
} else {
canvas.style.display = "none";
memoryGraph = null;
}
}