summaryrefslogtreecommitdiffstats
path: root/js/src/devtools/gc-ubench/ui.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /js/src/devtools/gc-ubench/ui.js
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/devtools/gc-ubench/ui.js')
-rw-r--r--js/src/devtools/gc-ubench/ui.js700
1 files changed, 700 insertions, 0 deletions
diff --git a/js/src/devtools/gc-ubench/ui.js b/js/src/devtools/gc-ubench/ui.js
new file mode 100644
index 0000000000..f4a8357951
--- /dev/null
+++ b/js/src/devtools/gc-ubench/ui.js
@@ -0,0 +1,700 @@
+/* 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 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 gcAllocTrigger() {
+ return gMemory.zone.gcAllocTrigger;
+ }
+
+ 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(ctx) {
+ this.ctx = ctx;
+
+ var { height } = ctx.canvas;
+ this.layout = {
+ xAxisLabel_Y: height - 20,
+ };
+ }
+
+ xpos(index) {
+ return index * 2;
+ }
+
+ clear() {
+ const { width, height } = this.ctx.canvas;
+ this.ctx.clearRect(0, 0, width, 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;
+ const { 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();
+ }
+
+ drawFrame() {
+ const ctx = this.ctx;
+ const { 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();
+ }
+};
+
+var LatencyGraph = class extends Graph {
+ constructor(ctx) {
+ super(ctx);
+ console.log(this.ctx);
+ }
+
+ ypos(delay) {
+ const { height } = this.ctx.canvas;
+
+ const r = height + 100 - Math.log(delay) * 64;
+ if (r < 5) {
+ return 5;
+ }
+ return r;
+ }
+
+ drawHBar(delay, label, color = "rgb(0,0,0)", label_offset = 0) {
+ const 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)";
+ }
+
+ 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.worstEver = this.bestEver = gHost.gcBytes();
+ this.limit = Math.max(this.worstEver, gHost.gcAllocTrigger);
+ }
+
+ ypos(size) {
+ const { height } = this.ctx.canvas;
+
+ const range = this.limit - this.bestEver;
+ const percent = (size - this.bestEver) / range;
+
+ return (1 - percent) * height * 0.9 + 20;
+ }
+
+ drawHBar(size, label, color = "rgb(150,150,150)") {
+ 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();
+
+ var worst = 0,
+ worstpos = 0;
+ for (let i = 0; i < numSamples; i++) {
+ if (gHistory.gcBytes[i] >= worst) {
+ worst = gHistory.gcBytes[i];
+ worstpos = i;
+ }
+ if (gHistory.gcBytes[i] < this.bestEver) {
+ this.bestEver = gHistory.gcBytes[i];
+ }
+ }
+
+ if (this.worstEver < worst) {
+ this.worstEver = worst;
+ this.limit = Math.max(this.worstEver, gHost.gcAllocTrigger);
+ }
+
+ this.drawHBar(
+ this.bestEver,
+ `${format_bytes(this.bestEver)} min`,
+ "#00cf61"
+ );
+ this.drawHBar(
+ this.worstEver,
+ `${format_bytes(this.worstEver)} max`,
+ "#cc1111"
+ );
+ this.drawHBar(
+ gHost.gcAllocTrigger,
+ `${format_bytes(gHost.gcAllocTrigger)} trigger`,
+ "#cc11cc"
+ );
+
+ ctx.fillStyle = "rgb(255,0,0)";
+ if (worst) {
+ ctx.fillText(
+ format_bytes(worst),
+ this.xpos(worstpos) - 10,
+ this.ypos(worst) - 14
+ );
+ }
+
+ ctx.beginPath();
+ var where = sampleIndex % numSamples;
+ ctx.arc(
+ this.xpos(where),
+ this.ypos(gHistory.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(gHistory.gcBytes[i]));
+ } else {
+ ctx.lineTo(this.xpos(i), this.ypos(gHistory.gcBytes[i]));
+ }
+ if (i == where) {
+ ctx.stroke();
+ }
+ }
+ ctx.stroke();
+
+ 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() {
+ gLoadMgr.deactivateLoad();
+}
+
+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;
+
+ 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.getContext("2d"));
+
+ 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.getContext("2d"));
+ } else {
+ canvas.style.display = "none";
+ memoryGraph = null;
+ }
+}