summaryrefslogtreecommitdiffstats
path: root/js/src/devtools/gc-ubench
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--js/src/devtools/gc-ubench/argparse.js127
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/bigTextNodes.js42
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/deepWeakMap.js40
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/events.js40
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/expandoEvents.js41
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayArrayLiteral.js32
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayBuffer.js36
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayFgFinalized.js37
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayLargeArray.js34
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayLargeObject.js36
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayNewObject.js33
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayObjectLiteral.js32
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayReallocArray.js34
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/largeArrayPropertyAndElements.js40
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/noAllocation.js10
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/pairCyclicWeakMap.js46
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/propertyTreeSplitting.js24
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/selfCyclicWeakMap.js42
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/textNodes.js38
-rw-r--r--js/src/devtools/gc-ubench/harness.js328
-rw-r--r--js/src/devtools/gc-ubench/index.html90
-rw-r--r--js/src/devtools/gc-ubench/perf.js217
-rw-r--r--js/src/devtools/gc-ubench/scheduler.js64
-rw-r--r--js/src/devtools/gc-ubench/sequencer.js298
-rw-r--r--js/src/devtools/gc-ubench/shell-bench.js147
-rw-r--r--js/src/devtools/gc-ubench/spidermonkey.js57
-rw-r--r--js/src/devtools/gc-ubench/test_list.js20
-rw-r--r--js/src/devtools/gc-ubench/ui.js700
-rw-r--r--js/src/devtools/gc-ubench/v8.js42
29 files changed, 2727 insertions, 0 deletions
diff --git a/js/src/devtools/gc-ubench/argparse.js b/js/src/devtools/gc-ubench/argparse.js
new file mode 100644
index 0000000000..454b53fb71
--- /dev/null
+++ b/js/src/devtools/gc-ubench/argparse.js
@@ -0,0 +1,127 @@
+/* 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/. */
+
+// Command-line argument parser, modeled after but not identical to Python's
+// argparse.
+
+var ArgParser = class {
+ constructor(desc) {
+ this._params = [];
+ this._doc = desc;
+
+ this.add_argument("--help", {
+ help: "display this help message",
+ });
+ }
+
+ // name is '--foo', '-f', or an array of aliases.
+ //
+ // spec is an options object with keys:
+ // dest: key name to store the result in (optional for long options)
+ // default: value to use if not passed on command line (optional)
+ // help: description of the option to show in --help
+ // options: array of valid choices
+ //
+ // Prefixes of long option names are allowed. If a prefix is ambiguous, it
+ // will match the first parameter added to the ArgParser.
+ add_argument(name, spec) {
+ const names = Array.isArray(name) ? name : [name];
+
+ spec = Object.assign({}, spec);
+ spec.aliases = names;
+ for (const name of names) {
+ if (!name.startsWith("-")) {
+ throw new Error(`unhandled argument syntax '${name}'`);
+ }
+ if (name.startsWith("--")) {
+ spec.dest = spec.dest || name.substr(2);
+ }
+ this._params.push({ name, spec });
+ }
+ }
+
+ parse_args(args) {
+ const opts = {};
+ const rest = [];
+
+ for (const { spec } of this._params) {
+ if (spec.default !== undefined) {
+ opts[spec.dest] = spec.default;
+ }
+ }
+
+ const seen = new Set();
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+ if (!arg.startsWith("-")) {
+ rest.push(arg);
+ continue;
+ } else if (arg === "--") {
+ rest.push(args.slice(i+1));
+ break;
+ }
+
+ if (arg == "--help" || arg == "-h") {
+ this.help();
+ }
+
+ let parameter;
+ let [passed, value] = arg.split("=");
+ for (const { name, spec } of this._params) {
+ if (passed.startsWith("--")) {
+ if (name.startsWith(passed)) {
+ parameter = spec;
+ }
+ } else if (passed.startsWith("-") && passed === name) {
+ parameter = spec;
+ }
+ if (parameter) {
+ if (value === undefined) {
+ value = args[++i];
+ }
+ opts[parameter.dest] = value;
+ break;
+ }
+ }
+
+ if (parameter) {
+ if (seen.has(parameter)) {
+ throw new Error(`${parameter.aliases[0]} given multiple times`);
+ }
+ seen.add(parameter);
+ } else {
+ throw new Error(`invalid command-line argument '${arg}'`);
+ }
+ }
+
+ for (const { name, spec } of this._params) {
+ if (spec.options && !spec.options.includes(opts[spec.dest])) {
+ throw new Error(`invalid ${name} value '${opts[spec.dest]}'`);
+ opts[spec.dest] = spec.default;
+ }
+ }
+
+ return { opts, rest };
+ }
+
+ help() {
+ print(`Usage: ${this._doc}`);
+ const specs = new Set(this._params.map(p => p.spec));
+ const optstrs = [...specs].map(p => p.aliases.join(", "));
+ let maxlen = Math.max(...optstrs.map(s => s.length));
+ for (const spec of specs) {
+ const name = spec.aliases[0];
+ let helptext = spec.help ?? "undocumented";
+ if ("options" in spec) {
+ helptext += ` (one of ${spec.options.map(x => `'${x}'`).join(", ")})`;
+ }
+ if ("default" in spec) {
+ helptext += ` (default '${spec.default}')`;
+ }
+ const optstr = spec.aliases.join(", ");
+ print(` ${optstr.padEnd(maxlen)} ${helptext}`);
+ }
+ quit(0);
+ }
+};
diff --git a/js/src/devtools/gc-ubench/benchmarks/bigTextNodes.js b/js/src/devtools/gc-ubench/benchmarks/bigTextNodes.js
new file mode 100644
index 0000000000..0a66be03d3
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/bigTextNodes.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+tests.set(
+ "bigTextNodes",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [ textNode, textNode, ... ]",
+
+ enabled: "document" in globalThis,
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "8",
+ defaultGarbagePiles: "8",
+
+ makeGarbage: N => {
+ var a = [];
+ var s = "x";
+ for (var i = 0; i < 16; i++) {
+ s = s + s;
+ }
+ for (var i = 0; i < N; i++) {
+ a.push(document.createTextNode(s));
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/deepWeakMap.js b/js/src/devtools/gc-ubench/benchmarks/deepWeakMap.js
new file mode 100644
index 0000000000..bf46d1d97b
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/deepWeakMap.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+tests.set(
+ "deepWeakMap",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "o={wm,k}; w.mk[k]=o2={wm2,k2}; wm2[k2]=....",
+
+ defaultGarbagePerFrame: "1K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ makeGarbage: M => {
+ var initial = {};
+ var prev = initial;
+ for (var i = 0; i < M; i++) {
+ var obj = [new WeakMap(), Object.create(null)];
+ obj[0].set(obj[1], prev);
+ prev = obj;
+ }
+ garbage[garbageIndex++] = initial;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/events.js b/js/src/devtools/gc-ubench/benchmarks/events.js
new file mode 100644
index 0000000000..0fe91b7596
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/events.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+tests.set(
+ "events",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [ textNode, textNode, ... ]",
+
+ enabled: "document" in globalThis,
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "100K",
+ defaultGarbagePiles: "8",
+
+ makeGarbage: N => {
+ var a = [];
+ for (var i = 0; i < N; i++) {
+ var e = document.createEvent("Events");
+ e.initEvent("TestEvent", true, true);
+ a.push(e);
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/expandoEvents.js b/js/src/devtools/gc-ubench/benchmarks/expandoEvents.js
new file mode 100644
index 0000000000..4c6a53c8fe
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/expandoEvents.js
@@ -0,0 +1,41 @@
+/* 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/. */
+
+tests.set(
+ "expandoEvents",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [ textNode, textNode, ... ]",
+
+ enabled: "document" in globalThis,
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "100K",
+ defaultGarbagePiles: "8",
+
+ makeGarbage: N => {
+ var a = [];
+ for (var i = 0; i < N; i++) {
+ var e = document.createEvent("Events");
+ e.initEvent("TestEvent", true, true);
+ e.color = ["tuna"];
+ a.push(e);
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayArrayLiteral.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayArrayLiteral.js
new file mode 100644
index 0000000000..a9003d8be6
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayArrayLiteral.js
@@ -0,0 +1,32 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayArrayLiteral",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [[], ....]",
+ defaultGarbagePerFrame: "1M",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+ makeGarbage: N => {
+ for (var i = 0; i < N; i++) {
+ garbage[garbageIndex++] = ["foo", "bar", "baz", "baa"];
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayBuffer.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayBuffer.js
new file mode 100644
index 0000000000..06bf73eea8
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayBuffer.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayBuffer",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = ArrayBuffer(N); # (large malloc data)",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "4M",
+ defaultGarbagePiles: "8K",
+
+ makeGarbage: N => {
+ var ab = new ArrayBuffer(N);
+ var view = new Uint8Array(ab);
+ view[0] = 1;
+ view[N - 1] = 2;
+ garbage[garbageIndex++] = ab;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayFgFinalized.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayFgFinalized.js
new file mode 100644
index 0000000000..d61d56b81c
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayFgFinalized.js
@@ -0,0 +1,37 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayFgFinalized",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description:
+ "var foo = [ new Map, new Map, ... ]; # (foreground finalized)",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePiles: "8K",
+ defaultGarbagePerFrame: "48K",
+
+ makeGarbage: N => {
+ var arr = [];
+ for (var i = 0; i < N; i++) {
+ arr.push(new Map());
+ }
+ garbage[garbageIndex++] = arr;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeArray.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeArray.js
new file mode 100644
index 0000000000..38e4f2a85b
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeArray.js
@@ -0,0 +1,34 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayLargeArray",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [[...], ....]",
+ defaultGarbagePerFrame: "3M",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+ makeGarbage: N => {
+ var a = new Array(N);
+ for (var i = 0; i < N; i++) {
+ a[i] = N - i;
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeObject.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeObject.js
new file mode 100644
index 0000000000..ed1c38b271
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeObject.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayLargeObject",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = { LARGE }; # (large slots)",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePiles: "8K",
+ defaultGarbagePerFrame: "64K",
+
+ makeGarbage: N => {
+ var obj = {};
+ for (var i = 0; i < N; i++) {
+ obj["key" + i] = i;
+ }
+ garbage[garbageIndex++] = obj;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayNewObject.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayNewObject.js
new file mode 100644
index 0000000000..04487aac20
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayNewObject.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayNewObject",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [new Object(), ....]",
+ defaultGarbagePerFrame: "128K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ makeGarbage: N => {
+ for (var i = 0; i < N; i++) {
+ garbage[garbageIndex++] = new Object();
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayObjectLiteral.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayObjectLiteral.js
new file mode 100644
index 0000000000..a31e76bdc6
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayObjectLiteral.js
@@ -0,0 +1,32 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayObjectLiteral",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [{}, ....]",
+ defaultGarbagePerFrame: "384K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+ makeGarbage: N => {
+ for (var i = 0; i < N; i++) {
+ garbage[garbageIndex++] = { a: "foo", b: "bar", 0: "foo", 1: "bar" };
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayReallocArray.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayReallocArray.js
new file mode 100644
index 0000000000..54316736e1
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayReallocArray.js
@@ -0,0 +1,34 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayReallocArray",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [[,,,], ....]",
+ defaultGarbagePerFrame: "2M",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+ makeGarbage: N => {
+ var a = [];
+ for (var i = 0; i < N; i++) {
+ a[i] = N - i;
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/largeArrayPropertyAndElements.js b/js/src/devtools/gc-ubench/benchmarks/largeArrayPropertyAndElements.js
new file mode 100644
index 0000000000..0202f56e40
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/largeArrayPropertyAndElements.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+tests.set(
+ "largeArrayPropertyAndElements",
+ (function() {
+ var garbage;
+ var index;
+
+ return {
+ description: "Large array with both properties and elements",
+
+ load: n => {
+ garbage = new Array(n);
+ garbage.fill(null);
+ index = 0;
+ },
+
+ unload: () => {
+ garbage = null;
+ index = 0;
+ },
+
+ defaultGarbagePiles: "100K",
+ defaultGarbagePerFrame: "48K",
+
+ makeGarbage: n => {
+ for (var i = 0; i < n; i++) {
+ index++;
+ index %= garbage.length;
+
+ var obj = {};
+ garbage[index] = obj;
+ garbage["key-" + index] = obj;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/noAllocation.js b/js/src/devtools/gc-ubench/benchmarks/noAllocation.js
new file mode 100644
index 0000000000..8e6ba53943
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/noAllocation.js
@@ -0,0 +1,10 @@
+/* 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/. */
+
+tests.set("noAllocation", {
+ description: "Do not generate any garbage.",
+ load: N => {},
+ unload: () => {},
+ makeGarbage: N => {},
+});
diff --git a/js/src/devtools/gc-ubench/benchmarks/pairCyclicWeakMap.js b/js/src/devtools/gc-ubench/benchmarks/pairCyclicWeakMap.js
new file mode 100644
index 0000000000..ac43325b6f
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/pairCyclicWeakMap.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+tests.set(
+ "pairCyclicWeakMap",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "wm1[k1] = k2; wm2[k2] = k3; wm1[k3] = k4; wm2[k4] = ...",
+
+ defaultGarbagePerFrame: "10K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ makeGarbage: M => {
+ var wm1 = new WeakMap();
+ var wm2 = new WeakMap();
+ var initialKey = {};
+ var key = initialKey;
+ var value = {};
+ for (var i = 0; i < M / 2; i++) {
+ wm1.set(key, value);
+ key = value;
+ value = {};
+ wm2.set(key, value);
+ key = value;
+ value = {};
+ }
+ garbage[garbageIndex++] = [initialKey, wm1, wm2];
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/propertyTreeSplitting.js b/js/src/devtools/gc-ubench/benchmarks/propertyTreeSplitting.js
new file mode 100644
index 0000000000..9fc62b29e2
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/propertyTreeSplitting.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+tests.set(
+ "propertyTreeSplitting",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ var obj = {};
+ return {
+ description: "use delete to generate Shape garbage (piles are unused)",
+ load: N => {},
+ unload: () => {},
+ makeGarbage: N => {
+ for (var a = 0; a < N; ++a) {
+ obj.x = 1;
+ obj.y = 2;
+ delete obj.x;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/selfCyclicWeakMap.js b/js/src/devtools/gc-ubench/benchmarks/selfCyclicWeakMap.js
new file mode 100644
index 0000000000..c7736c72b9
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/selfCyclicWeakMap.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+tests.set(
+ "selfCyclicWeakMap",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var wm = new WeakMap(); wm[k1] = k2; wm[k2] = k3; ...",
+
+ defaultGarbagePerFrame: "10K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ makeGarbage: M => {
+ var wm = new WeakMap();
+ var initialKey = {};
+ var key = initialKey;
+ var value = {};
+ for (var i = 0; i < M; i++) {
+ wm.set(key, value);
+ key = value;
+ value = {};
+ }
+ garbage[garbageIndex++] = [initialKey, wm];
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/textNodes.js b/js/src/devtools/gc-ubench/benchmarks/textNodes.js
new file mode 100644
index 0000000000..07fd07e7b7
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/textNodes.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+tests.set(
+ "textNodes",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [ textNode, textNode, ... ]",
+
+ enabled: "document" in globalThis,
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "100K",
+ defaultGarbagePiles: "8",
+
+ makeGarbage: N => {
+ var a = [];
+ for (var i = 0; i < N; i++) {
+ a.push(document.createTextNode("t" + i));
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/harness.js b/js/src/devtools/gc-ubench/harness.js
new file mode 100644
index 0000000000..db7fa06d63
--- /dev/null
+++ b/js/src/devtools/gc-ubench/harness.js
@@ -0,0 +1,328 @@
+/* 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/. */
+
+// Global defaults
+
+// Allocate this much "garbage" per frame. This might correspond exactly to a
+// number of objects/values, or it might be some set of objects, depending on
+// the mutator in question.
+var gDefaultGarbagePerFrame = "8K";
+
+// In order to avoid a performance cliff between when the per-frame garbage
+// fits in the nursery and when it doesn't, most mutators will collect multiple
+// "piles" of garbage and round-robin through them, so that the per-frame
+// garbage stays alive for some number of frames. There will still be some
+// internal temporary allocations that don't end up in the piles; presumably,
+// the nursery will take care of those.
+//
+// If the per-frame garbage is K and the number of piles is P, then some of the
+// garbage will start getting tenured as long as P*K > size(nursery).
+var gDefaultGarbagePiles = "8";
+
+var gDefaultTestDuration = 8.0;
+
+// The Host interface that provides functionality needed by the test harnesses
+// (web + various shells). Subclasses should override with the appropriate
+// functionality. The methods that throw an error must be implemented. The ones
+// that return undefined are optional.
+//
+// Note that currently the web UI doesn't really use the scheduling pieces of
+// this.
+var Host = class {
+ constructor() {}
+ start_turn() {
+ throw new Error("unimplemented");
+ }
+ end_turn() {
+ throw new Error("unimplemented");
+ }
+ suspend(duration) {
+ throw new Error("unimplemented");
+ } // Shell driver only
+ now() {
+ return performance.now();
+ }
+
+ minorGCCount() {
+ return undefined;
+ }
+ majorGCCount() {
+ return undefined;
+ }
+ GCSliceCount() {
+ return undefined;
+ }
+
+ features = {
+ haveMemorySizes: false,
+ haveGCCounts: false,
+ };
+};
+
+function percent(x) {
+ return `${(x*100).toFixed(2)}%`;
+}
+
+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 AllocationLoad = class {
+ constructor(info, name) {
+ this.load = info;
+ this.load.name = this.load.name ?? name;
+
+ this._garbagePerFrame =
+ info.garbagePerFrame ||
+ parse_units(info.defaultGarbagePerFrame || gDefaultGarbagePerFrame);
+ this._garbagePiles =
+ info.garbagePiles ||
+ parse_units(info.defaultGarbagePiles || gDefaultGarbagePiles);
+ }
+
+ get name() {
+ return this.load.name;
+ }
+ get description() {
+ return this.load.description;
+ }
+ get garbagePerFrame() {
+ return this._garbagePerFrame;
+ }
+ set garbagePerFrame(amount) {
+ this._garbagePerFrame = amount;
+ }
+ get garbagePiles() {
+ return this._garbagePiles;
+ }
+ set garbagePiles(amount) {
+ this._garbagePiles = amount;
+ }
+
+ start() {
+ this.load.load(this._garbagePiles);
+ }
+
+ stop() {
+ this.load.unload();
+ }
+
+ reload() {
+ this.stop();
+ this.start();
+ }
+
+ tick() {
+ this.load.makeGarbage(this._garbagePerFrame);
+ }
+
+ is_dummy_load() {
+ return this.load.name == "noAllocation";
+ }
+};
+
+var AllocationLoadManager = class {
+ constructor(tests) {
+ this._loads = new Map();
+ for (const [name, info] of tests.entries()) {
+ this._loads.set(name, new AllocationLoad(info, name));
+ }
+ this._active = undefined;
+ this._paused = false;
+
+ // Public API
+ this.sequencer = null;
+ this.testDurationMS = gDefaultTestDuration * 1000;
+ }
+
+ getByName(name) {
+ const mutator = this._loads.get(name);
+ if (!mutator) {
+ throw new Error(`invalid mutator '${name}'`);
+ }
+ return mutator;
+ }
+
+ activeLoad() {
+ return this._active;
+ }
+
+ setActiveLoad(mutator) {
+ if (this._active) {
+ this._active.stop();
+ }
+ this._active = mutator;
+ this._active.start();
+ }
+
+ deactivateLoad() {
+ this._active.stop();
+ this._active = undefined;
+ }
+
+ get paused() {
+ return this._paused;
+ }
+ set paused(pause) {
+ this._paused = pause;
+ }
+
+ load_running() {
+ return this._active;
+ }
+
+ change_garbagePiles(amount) {
+ if (this._active) {
+ this._active.garbagePiles = amount;
+ this._active.reload();
+ }
+ }
+
+ change_garbagePerFrame(amount) {
+ if (this._active) {
+ this._active.garbagePerFrame = amount;
+ }
+ }
+
+ tick(now = gHost.now()) {
+ this.lastActive = this._active;
+ let completed = false;
+
+ if (this.sequencer) {
+ if (this.sequencer.tick(now)) {
+ completed = true;
+ if (this.sequencer.current) {
+ this.setActiveLoad(this.sequencer.current);
+ } else {
+ this.deactivateLoad();
+ }
+ if (this.sequencer.done()) {
+ this.sequencer = null;
+ }
+ }
+ }
+
+ if (this._active && !this._paused) {
+ this._active.tick();
+ }
+
+ return completed;
+ }
+
+ startSequencer(sequencer, now = gHost.now()) {
+ this.sequencer = sequencer;
+ this.sequencer.start(now);
+ this.setActiveLoad(this.sequencer.current);
+ }
+
+ stopped() {
+ return !this.sequencer || this.sequencer.done();
+ }
+
+ currentLoadRemaining(now = gHost.now()) {
+ if (this.stopped()) {
+ return 0;
+ }
+
+ // TODO: The web UI displays a countdown to the end of the current mutator.
+ // This won't work for potential future things like "run until 3 major GCs
+ // have been seen", so the API will need to be modified to provide
+ // information in that case.
+ return this.testDurationMS - this.sequencer.currentLoadElapsed(now);
+ }
+};
+
+// Current test state.
+var gLoadMgr = undefined;
+
+function format_with_units(n, label, shortlabel, kbase) {
+ if (n < kbase * 4) {
+ return `${n} ${label}`;
+ } else if (n < kbase ** 2 * 4) {
+ return `${(n / kbase).toFixed(2)}K${shortlabel}`;
+ } else if (n < kbase ** 3 * 4) {
+ return `${(n / kbase ** 2).toFixed(2)}M${shortlabel}`;
+ }
+ return `${(n / kbase ** 3).toFixed(2)}G${shortlabel}`;
+}
+
+function format_bytes(bytes) {
+ return format_with_units(bytes, "bytes", "B", 1024);
+}
+
+function format_num(n) {
+ return format_with_units(n, "", "", 1000);
+}
+
+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.
+ delay = Math.round(delay * 100) / 100;
+ var current = histogram.has(delay) ? histogram.get(delay) : 0;
+ histogram.set(delay, ++current);
+}
+
+// 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 = score / (gLoadMgr.testDurationMS / 1000);
+ return Math.round(score * 1000) / 1000;
+}
+
+// Build a spark-lines histogram for the test results to show with the aggregate score.
+function compute_spark_histogram_percents(histogram) {
+ var ranges = [
+ [-99999999, 16.6],
+ [16.6, 16.8],
+ [16.8, 25],
+ [25, 33.4],
+ [33.4, 60],
+ [60, 100],
+ [100, 300],
+ [300, 99999999],
+ ];
+ var rescaled = new Map();
+ for (let [delay] of histogram) {
+ for (var i = 0; i < ranges.length; ++i) {
+ var low = ranges[i][0];
+ var high = ranges[i][1];
+ if (low <= delay && delay < high) {
+ update_histogram(rescaled, i);
+ break;
+ }
+ }
+ }
+ var total = 0;
+ for (const [, count] of rescaled) {
+ total += count;
+ }
+
+ var spark = [];
+ for (let i = 0; i < ranges.length; ++i) {
+ const amt = rescaled.has(i) ? rescaled.get(i) : 0;
+ spark.push(amt / total);
+ }
+
+ return spark;
+}
diff --git a/js/src/devtools/gc-ubench/index.html b/js/src/devtools/gc-ubench/index.html
new file mode 100644
index 0000000000..4abce385f2
--- /dev/null
+++ b/js/src/devtools/gc-ubench/index.html
@@ -0,0 +1,90 @@
+<!-- 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/. -->
+
+<html>
+<head>
+ <title>GC uBench</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+ <!-- Benchmark harness and UI -->
+ <script src="harness.js"></script>
+ <script src="perf.js"></script>
+ <script src="sequencer.js"></script>
+ <script src="ui.js"></script>
+
+ <!-- List of garbage-creating test loads -->
+ <script src="test_list.js"></script>
+
+ <!-- Collect all test loads into a `tests` Map -->
+ <script>
+ var tests = new Map();
+ foreach_test_file(path => import("./" + path));
+ </script>
+
+</head>
+
+<body onload="onload()" onunload="onunload()">
+
+<canvas id="graph" width="1080" height="400" style="padding-left:10px"></canvas>
+<canvas id="memgraph" width="1080" height="400" style="padding-left:10px"></canvas>
+<div id="memgraph-disabled" style="display: none"><i>No performance.mozMemory object available. If running Firefox, set dom.enable_memory_stats to True to see heap size info.</i></div>
+
+<hr>
+
+<div id='track-sizes-div'>
+ Show heap size graph: <input id='track-sizes' type='checkbox' onclick="trackHeapSizes(this.checked)">
+</div>
+
+<div>
+ Update display:
+ <input type="checkbox" id="do-graph" onchange="onUpdateDisplayChanged()" checked></input>
+</div>
+
+<div>
+ Run allocation load
+ <input type="checkbox" id="do-load" onchange="onDoLoadChange()" checked></input>
+</div>
+
+<div>
+ Allocation load:
+ <select id="test-selection" required onchange="onLoadChange()"></select>
+ <span id="load-running">(init)</span>
+</div>
+
+<div>
+ &nbsp;&nbsp;&nbsp;&nbsp;Garbage items per frame:
+ <input type="text" id="garbage-per-frame" size="5" value="8K"
+ onchange="garbage_per_frame_changed()"></input>
+</div>
+<div>
+ &nbsp;&nbsp;&nbsp;&nbsp;Garbage piles:
+ <input type="text" id="garbage-piles" size="5" value="8"
+ onchange="garbage_piles_changed()"></input>
+</div>
+
+<hr>
+
+<div>
+ Duration: <input type="text" id="test-duration" size="3" value="8" onchange="duration_changed()"></input>s
+ <input type="button" id="test-one" value="Run Test" onclick="run_one_test()"></input>
+ <input type="button" id="test-all" value="Run All Tests" onclick="run_all_tests()"></input>
+</div>
+
+<div>
+ &nbsp;&nbsp;&nbsp;&nbsp;Time remaining: <span id="test-progress">(not running)</span>
+</div
+
+<div>
+ &nbsp;&nbsp;&nbsp;&nbsp;60 fps: <span id="pct60">n/a</span>
+ &nbsp;&nbsp;&nbsp;&nbsp;45 fps: <span id="pct45">n/a</span>
+ &nbsp;&nbsp;&nbsp;&nbsp;30 fps: <span id="pct30">n/a</span>
+</div
+
+<div id="results-Area">
+ Test Results:
+ <div id="results-display" style="padding-left: 10px; border: 1px solid black;"></div>
+</div>
+
+</body>
+</html>
diff --git a/js/src/devtools/gc-ubench/perf.js b/js/src/devtools/gc-ubench/perf.js
new file mode 100644
index 0000000000..dc370ee0da
--- /dev/null
+++ b/js/src/devtools/gc-ubench/perf.js
@@ -0,0 +1,217 @@
+/* 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/. */
+
+// Performance monitoring and calculation.
+
+function round_up(val, interval) {
+ return val + (interval - (val % interval));
+}
+
+// Class for inter-frame timing, which handles being paused and resumed.
+var FrameTimer = class {
+ constructor() {
+ // Start time of the current active test, adjusted for any time spent
+ // stopped (so `now - this.start` is how long the current active test
+ // has run for.)
+ this.start = undefined;
+
+ // Timestamp of callback following the previous frame.
+ this.prev = undefined;
+
+ // Timestamp when drawing was paused, or zero if drawing is active.
+ this.stopped = 0;
+ }
+
+ is_stopped() {
+ return this.stopped != 0;
+ }
+
+ start_recording(now = gHost.now()) {
+ this.start = this.prev = now;
+ }
+
+ on_frame_finished(now = gHost.now()) {
+ const delay = now - this.prev;
+ this.prev = now;
+ return delay;
+ }
+
+ pause(now = gHost.now()) {
+ this.stopped = now;
+ // Abuse this.prev to store the time elapsed since the previous frame.
+ // This will be used to adjust this.prev when we resume.
+ this.prev = now - this.prev;
+ }
+
+ resume(now = gHost.now()) {
+ this.prev = now - this.prev;
+ const stop_duration = now - this.stopped;
+ this.start += stop_duration;
+ this.stopped = 0;
+ }
+};
+
+// Per-frame time sampling infra.
+var sampleTime = 16.666667; // ms
+var sampleIndex = 0;
+
+// Class for maintaining a rolling window of per-frame GC-related counters:
+// inter-frame delay, minor/major/slice GC counts, cumulative bytes, etc.
+var FrameHistory = class {
+ constructor(numSamples) {
+ // Private
+ this._frameTimer = new FrameTimer();
+ this._numSamples = numSamples;
+
+ // Public API
+ this.delays = new Array(numSamples);
+ this.gcBytes = new Array(numSamples);
+ this.mallocBytes = new Array(numSamples);
+ this.gcs = new Array(numSamples);
+ this.minorGCs = new Array(numSamples);
+ this.majorGCs = new Array(numSamples);
+ this.slices = new Array(numSamples);
+
+ sampleIndex = 0;
+ this.reset();
+ }
+
+ start(now = gHost.now()) {
+ this._frameTimer.start_recording(now);
+ }
+
+ reset() {
+ this.delays.fill(0);
+ this.gcBytes.fill(0);
+ this.mallocBytes.fill(0);
+ this.gcs.fill(this.gcs[sampleIndex]);
+ this.minorGCs.fill(this.minorGCs[sampleIndex]);
+ this.majorGCs.fill(this.majorGCs[sampleIndex]);
+ this.slices.fill(this.slices[sampleIndex]);
+
+ sampleIndex = 0;
+ }
+
+ get numSamples() {
+ return this._numSamples;
+ }
+
+ findMax(collection) {
+ // Depends on having at least one non-negative entry, and unfilled
+ // entries being <= max.
+ var maxIndex = 0;
+ for (let i = 0; i < this._numSamples; i++) {
+ if (collection[i] >= collection[maxIndex]) {
+ maxIndex = i;
+ }
+ }
+ return maxIndex;
+ }
+
+ findMaxDelay() {
+ return this.findMax(this.delays);
+ }
+
+ on_frame(now = gHost.now()) {
+ const delay = this._frameTimer.on_frame_finished(now);
+
+ // Total time elapsed while the active test has been running.
+ var t = now - this._frameTimer.start;
+ var newIndex = Math.round(t / sampleTime);
+ while (sampleIndex < newIndex) {
+ sampleIndex++;
+ var idx = sampleIndex % this._numSamples;
+ this.delays[idx] = delay;
+ if (gHost.features.haveMemorySizes) {
+ this.gcBytes[idx] = gHost.gcBytes;
+ this.mallocBytes[idx] = gHost.mallocBytes;
+ }
+ if (gHost.features.haveGCCounts) {
+ this.minorGCs[idx] = gHost.minorGCCount;
+ this.majorGCs[idx] = gHost.majorGCCount;
+ this.slices[idx] = gHost.GCSliceCount;
+ }
+ }
+
+ return delay;
+ }
+
+ pause() {
+ this._frameTimer.pause();
+ }
+
+ resume() {
+ this._frameTimer.resume();
+ }
+
+ is_stopped() {
+ return this._frameTimer.is_stopped();
+ }
+};
+
+var PerfTracker = class {
+ constructor() {
+ // Private
+ this._currentLoadStart = undefined;
+ this._frameCount = undefined;
+ this._mutating_ms = undefined;
+ this._suspend_sec = undefined;
+ this._minorGCs = undefined;
+ this._majorGCs = undefined;
+
+ // Public
+ this.results = [];
+ }
+
+ on_load_start(load, now = gHost.now()) {
+ this._currentLoadStart = now;
+ this._frameCount = 0;
+ this._mutating_ms = 0;
+ this._suspend_sec = 0;
+ this._majorGCs = gHost.majorGCCount;
+ this._minorGCs = gHost.minorGCCount;
+ }
+
+ on_load_end(load, now = gHost.now()) {
+ const elapsed_time = (now - this._currentLoadStart) / 1000;
+ const full_time = round_up(elapsed_time, 1 / 60);
+ const frame_60fps_limit = Math.round(full_time * 60);
+ const dropped_60fps_frames = frame_60fps_limit - this._frameCount;
+ const dropped_60fps_fraction = dropped_60fps_frames / frame_60fps_limit;
+
+ const mutating_and_gc_fraction = this._mutating_ms / (full_time * 1000);
+
+ const result = {
+ load,
+ elapsed_time,
+ mutating: this._mutating_ms / 1000,
+ mutating_and_gc_fraction,
+ suspended: this._suspend_sec,
+ full_time,
+ frames: this._frameCount,
+ dropped_60fps_frames,
+ dropped_60fps_fraction,
+ majorGCs: gHost.majorGCCount - this._majorGCs,
+ minorGCs: gHost.minorGCCount - this._minorGCs,
+ };
+ this.results.push(result);
+
+ this._currentLoadStart = undefined;
+ this._frameCount = 0;
+
+ return result;
+ }
+
+ after_suspend(wait_sec) {
+ this._suspend_sec += wait_sec;
+ }
+
+ before_mutator(now = gHost.now()) {
+ this._frameCount++;
+ }
+
+ after_mutator(start_time, end_time = gHost.now()) {
+ this._mutating_ms += end_time - start_time;
+ }
+};
diff --git a/js/src/devtools/gc-ubench/scheduler.js b/js/src/devtools/gc-ubench/scheduler.js
new file mode 100644
index 0000000000..8f4a483b33
--- /dev/null
+++ b/js/src/devtools/gc-ubench/scheduler.js
@@ -0,0 +1,64 @@
+/* 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/. */
+
+// Frame schedulers: executing a frame's worth of mutation, and possibly
+// waiting for a later frame. (These schedulers will halt the main thread,
+// allowing background threads to continue working.)
+
+var Scheduler = class {
+ constructor(perfMonitor) {
+ this._perfMonitor = perfMonitor;
+ }
+
+ start(loadMgr, timestamp) {
+ return loadMgr.start(timestamp);
+ }
+ tick(loadMgr, timestamp) {}
+ wait_for_next_frame(t0, tick_start, tick_end) {}
+};
+
+// "Sync to vsync" scheduler: after the mutator is done for a frame, wait until
+// the beginning of the next 60fps frame.
+var VsyncScheduler = class extends Scheduler {
+ tick(loadMgr, timestamp) {
+ this._perfMonitor.before_mutator(timestamp);
+ gHost.start_turn();
+ const completed = loadMgr.tick(timestamp);
+ gHost.end_turn();
+ this._perfMonitor.after_mutator(timestamp);
+ return completed;
+ }
+
+ wait_for_next_frame(t0, tick_start, tick_end) {
+ // Compute how long until the next 60fps vsync event, and wait that long.
+ const elapsed = (tick_end - t0) / 1000;
+ const period = 1 / FPS;
+ const used = elapsed % period;
+ const delay = period - used;
+ gHost.suspend(delay);
+ this._perfMonitor.after_suspend(delay);
+ }
+};
+
+// Try to maintain 60fps, but if we overrun a frame, do more processing
+// immediately to make the next frame come up as soon as possible.
+var OptimizeForFrameRate = class extends Scheduler {
+ tick(loadMgr, timestamp) {
+ this._perfMonitor.before_mutator(timestamp);
+ gHost.start_turn();
+ const completed = loadMgr.tick(timestamp);
+ gHost.end_turn();
+ this._perfMonitor.after_mutator(timestamp);
+ return completed;
+ }
+
+ wait_for_next_frame(t0, tick_start, tick_end) {
+ const next_frame_ms = round_up(tick_start, 1000 / FPS);
+ if (tick_end < next_frame_ms) {
+ const delay = (next_frame_ms - tick_end) / 1000;
+ gHost.suspend(delay);
+ this._perfMonitor.after_suspend(delay);
+ }
+ }
+};
diff --git a/js/src/devtools/gc-ubench/sequencer.js b/js/src/devtools/gc-ubench/sequencer.js
new file mode 100644
index 0000000000..0271140c04
--- /dev/null
+++ b/js/src/devtools/gc-ubench/sequencer.js
@@ -0,0 +1,298 @@
+/* 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/. */
+
+// A Sequencer handles transitioning between different mutators. Typically, it
+// will base the decision to transition on things like elapsed time, number of
+// GCs observed, or similar. However, they might also implement a search for
+// some result value by running for some time while measuring, tweaking
+// parameters, and re-running until an in-range result is found.
+
+var Sequencer = class {
+ // Return the current mutator (of class AllocationLoad).
+ get current() {
+ throw new Error("unimplemented");
+ }
+
+ start(now = gHost.now()) {
+ this.started = now;
+ }
+
+ // Called by user to handle advancing time. Subclasses will normally override
+ // do_tick() instead. Returns the results of a trial if complete (the mutator
+ // reached its allotted time or otherwise determined that its timing data
+ // should be valid), and falsy otherwise.
+ tick(now = gHost.now()) {
+ if (this.done()) {
+ throw new Error("tick() called on completed sequencer");
+ }
+
+ return this.do_tick(now);
+ }
+
+ // Implement in subclass to handle time advancing. Must return trial's result
+ // if complete. Called by tick(), above.
+ do_tick(now = gHost.now()) {
+ throw new Error("unimplemented");
+ }
+
+ // Returns whether this sequencer is done running trials.
+ done() {
+ throw new Error("unimplemented");
+ }
+
+ restart(now = gHost.now()) {
+ this.reset();
+ this.start(now);
+ }
+
+ // Returns how long the current load has been running.
+ currentLoadElapsed(now = gHost.now()) {
+ return now - this.started;
+ }
+};
+
+// Run a single trial of a mutator and be done.
+var SingleMutatorSequencer = class extends Sequencer {
+ constructor(mutator, perf, duration_sec) {
+ super();
+ this.mutator = mutator;
+ this.perf = perf;
+ if (!(duration_sec > 0)) {
+ throw new Error(`invalid duration '${duration_sec}'`);
+ }
+ this.duration = duration_sec * 1000;
+ this.state = 'init'; // init -> running -> done
+ this.lastResult = undefined;
+ }
+
+ get current() {
+ return this.state === 'done' ? undefined : this.mutator;
+ }
+
+ reset() {
+ this.state = 'init';
+ }
+
+ start(now = gHost.now()) {
+ if (this.state !== 'init') {
+ throw new Error("cannot restart a single-mutator sequencer");
+ }
+ super.start(now);
+ this.state = 'running';
+ this.perf.on_load_start(this.current, now);
+ }
+
+ do_tick(now) {
+ if (this.currentLoadElapsed(now) < this.duration) {
+ return false;
+ }
+
+ const load = this.current;
+ this.state = 'done';
+ return this.perf.on_load_end(load, now);
+ }
+
+ done() {
+ return this.state === 'done';
+ }
+};
+
+// For each of series of sequencers, run until done.
+var ChainSequencer = class extends Sequencer {
+ constructor(sequencers) {
+ super();
+ this.sequencers = sequencers;
+ this.idx = -1;
+ this.state = sequencers.length ? 'init' : 'done'; // init -> running -> done
+ }
+
+ get current() {
+ return this.idx >= 0 ? this.sequencers[this.idx].current : undefined;
+ }
+
+ reset() {
+ this.state = 'init';
+ this.idx = -1;
+ }
+
+ start(now = gHost.now()) {
+ super.start(now);
+ if (this.sequencers.length === 0) {
+ this.state = 'done';
+ return;
+ }
+
+ this.idx = 0;
+ this.sequencers[0].start(now);
+ this.state = 'running';
+ }
+
+ do_tick(now) {
+ const sequencer = this.sequencers[this.idx];
+ const trial_result = sequencer.do_tick(now);
+ if (!trial_result) {
+ return false; // Trial is still going.
+ }
+
+ if (!sequencer.done()) {
+ // A single trial has completed, but the sequencer is not yet done.
+ return trial_result;
+ }
+
+ this.idx++;
+ if (this.idx < this.sequencers.length) {
+ this.sequencers[this.idx].start();
+ } else {
+ this.idx = -1;
+ this.state = 'done';
+ }
+
+ return trial_result;
+ }
+
+ done() {
+ return this.state === 'done';
+ }
+};
+
+var RunUntilSequencer = class extends Sequencer {
+ constructor(sequencer, loadMgr) {
+ super();
+ this.loadMgr = loadMgr;
+ this.sequencer = sequencer;
+
+ // init -> running -> done
+ this.state = sequencer.done() ? 'done' : 'init';
+ }
+
+ get current() {
+ return this.sequencer?.current;
+ }
+
+ reset() {
+ this.sequencer.reset();
+ this.state = 'init';
+ }
+
+ start(now) {
+ super.start(now);
+ this.sequencer.start(now);
+ this.initSearch(now);
+ this.state = 'running';
+ }
+
+ initSearch(now) {}
+
+ done() {
+ return this.state === 'done';
+ }
+
+ do_tick(now) {
+ const trial_result = this.sequencer.do_tick(now);
+ if (trial_result) {
+ if (this.searchComplete(trial_result)) {
+ this.state = 'done';
+ } else {
+ this.sequencer.restart(now);
+ }
+ }
+ return trial_result;
+ }
+
+ // Take the result of the last mutator run into account (only notified after
+ // a mutator is complete, so cannot be used to decide when to end the
+ // mutator.)
+ searchComplete(result) {
+ throw new Error("must implement in subclass");
+ }
+};
+
+// Run trials, adjusting garbagePerFrame, until 50% of the frames are dropped.
+var Find50Sequencer = class extends RunUntilSequencer {
+ constructor(sequencer, loadMgr, goal=0.5, low_range=0.45, high_range=0.55) {
+ super(sequencer, loadMgr);
+
+ // Run trials with varying garbagePerFrame, looking for a setting that
+ // drops 50% of the frames, until we have been searching in the range for
+ // `persistence` times.
+ this.low_range = low_range;
+ this.goal = goal;
+ this.high_range = high_range;
+ this.persistence = 3;
+
+ this.clear();
+ }
+
+ reset() {
+ super.reset();
+ this.clear();
+ }
+
+ clear() {
+ this.garbagePerFrame = undefined;
+
+ this.good = undefined;
+ this.goodAt = undefined;
+ this.bad = undefined;
+ this.badAt = undefined;
+
+ this.numInRange = 0;
+ }
+
+ start(now) {
+ super.start(now);
+ if (!this.done()) {
+ this.garbagePerFrame = this.sequencer.current.garbagePerFrame;
+ }
+ }
+
+ searchComplete(result) {
+ print(
+ `Saw ${percent(result.dropped_60fps_fraction)} with garbagePerFrame=${this.garbagePerFrame}`
+ );
+
+ // This is brittle with respect to noise. It might be better to do a linear
+ // regression and stop at an error threshold.
+ if (result.dropped_60fps_fraction < this.goal) {
+ if (this.goodAt === undefined || this.goodAt < this.garbagePerFrame) {
+ this.goodAt = this.garbagePerFrame;
+ this.good = result.dropped_60fps_fraction;
+ }
+ if (this.badAt !== undefined) {
+ this.garbagePerFrame = Math.trunc(
+ (this.garbagePerFrame + this.badAt) / 2
+ );
+ } else {
+ this.garbagePerFrame *= 2;
+ }
+ } else {
+ if (this.badAt === undefined || this.badAt > this.garbagePerFrame) {
+ this.badAt = this.garbagePerFrame;
+ this.bad = result.dropped_60fps_fraction;
+ }
+ if (this.goodAt !== undefined) {
+ this.garbagePerFrame = Math.trunc(
+ (this.garbagePerFrame + this.goodAt) / 2
+ );
+ } else {
+ this.garbagePerFrame = Math.trunc(this.garbagePerFrame / 2);
+ }
+ }
+
+ if (
+ this.low_range < result.dropped_60fps_fraction &&
+ result.dropped_60fps_fraction < this.high_range
+ ) {
+ this.numInRange++;
+ if (this.numInRange >= this.persistence) {
+ return true;
+ }
+ }
+
+ print(`next run with ${this.garbagePerFrame}`);
+ this.loadMgr.change_garbagePerFrame(this.garbagePerFrame);
+
+ return false;
+ }
+};
diff --git a/js/src/devtools/gc-ubench/shell-bench.js b/js/src/devtools/gc-ubench/shell-bench.js
new file mode 100644
index 0000000000..9640cddce9
--- /dev/null
+++ b/js/src/devtools/gc-ubench/shell-bench.js
@@ -0,0 +1,147 @@
+/* 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 FPS = 60;
+var gNumSamples = 500;
+
+// This requires a gHost to have been created that provides host-specific
+// facilities. See eg spidermonkey.js.
+
+loadRelativeToScript("argparse.js");
+loadRelativeToScript("harness.js");
+loadRelativeToScript("sequencer.js");
+loadRelativeToScript("scheduler.js");
+loadRelativeToScript("perf.js");
+loadRelativeToScript("test_list.js");
+
+var gPerf = new PerfTracker();
+
+var tests = new Map();
+foreach_test_file(f => loadRelativeToScript(f));
+for (const [name, info] of tests.entries()) {
+ if ("enabled" in info && !info.enabled) {
+ tests.delete(name);
+ }
+}
+
+function tick(loadMgr, timestamp) {
+ gPerf.before_mutator(timestamp);
+ gHost.start_turn();
+ const events = loadMgr.tick(timestamp);
+ gHost.end_turn();
+ gPerf.after_mutator(timestamp);
+ return events;
+}
+
+function run(opts, loads) {
+ const sequence = [];
+ for (const mut of loads) {
+ if (tests.has(mut)) {
+ sequence.push(mut);
+ } else if (mut === "all") {
+ sequence.push(...tests.keys());
+ } else {
+ sequence.push(...[...tests.keys()].filter(t => t.includes(mut)));
+ }
+ }
+ if (loads.length === 0) {
+ sequence.push(...tests.keys());
+ }
+
+ const loadMgr = new AllocationLoadManager(tests);
+ const perf = new FrameHistory(gNumSamples);
+
+ const mutators = sequence.map(name => new SingleMutatorSequencer(loadMgr.getByName(name), gPerf, opts.duration));
+ let sequencer;
+ if (opts.sequencer == 'cycle') {
+ sequencer = new ChainSequencer(mutators);
+ } else if (opts.sequencer == 'find50') {
+ const seekers = mutators.map(s => new Find50Sequencer(s, loadMgr));
+ sequencer = new ChainSequencer(seekers);
+ }
+
+ const schedulerCtors = {
+ keepup: OptimizeForFrameRate,
+ vsync: VsyncScheduler,
+ };
+ const scheduler = new schedulerCtors[opts.sched](gPerf);
+
+ perf.start();
+
+ const t0 = gHost.now();
+
+ let possible = 0;
+ let frames = 0;
+ loadMgr.startSequencer(sequencer);
+ print(`${loadMgr.activeLoad().name} starting`);
+ while (loadMgr.load_running()) {
+ const timestamp = gHost.now();
+ const completed = scheduler.tick(loadMgr, timestamp);
+ const after_tick = gHost.now();
+
+ perf.on_frame(timestamp);
+
+ if (completed) {
+ print(`${loadMgr.lastActive.name} ended`);
+ if (loadMgr.load_running()) {
+ print(`${loadMgr.activeLoad().name} starting`);
+ }
+ }
+
+ frames++;
+ if (completed) {
+ possible += (loadMgr.testDurationMS / 1000) * FPS;
+ const elapsed = ((after_tick - t0) / 1000).toFixed(2);
+ print(` observed ${frames} / ${possible} frames in ${elapsed} seconds`);
+ }
+
+ scheduler.wait_for_next_frame(t0, timestamp, after_tick);
+ }
+}
+
+function report_results() {
+ for (const result of gPerf.results) {
+ const {
+ load,
+ elapsed_time,
+ mutating,
+ mutating_and_gc_fraction,
+ suspended,
+ full_time,
+ frames,
+ dropped_60fps_frames,
+ dropped_60fps_fraction,
+ minorGCs,
+ majorGCs,
+ } = result;
+
+ const drop_pct = percent(dropped_60fps_fraction);
+ const mut_pct = percent(mutating_and_gc_fraction);
+ const mut_sec = mutating.toFixed(2);
+ const full_sec = full_time.toFixed(2);
+ const susp_sec = suspended.toFixed(2);
+ print(`${load.name}:
+ ${frames} (60fps) frames seen out of expected ${Math.floor(full_time * 60)}
+ ${dropped_60fps_frames} = ${drop_pct} 60fps frames dropped
+ ${mut_pct} of run spent mutating and GCing (${mut_sec}sec out of ${full_sec}sec vs ${susp_sec} sec waiting)
+ ${minorGCs} minor GCs, ${majorGCs} major GCs
+`);
+ }
+}
+
+var argparse = new ArgParser("JS shell microbenchmark runner");
+argparse.add_argument(["--duration", "-d"], {
+ default: gDefaultTestDuration,
+ help: "how long to run mutators for (in seconds)"
+});
+argparse.add_argument("--sched", {
+ default: "keepup",
+ options: ["keepup", "vsync"],
+ help: "frame scheduler"
+});
+argparse.add_argument("--sequencer", {
+ default: "cycle",
+ options: ["cycle", "find50"],
+ help: "mutator sequencer"
+});
diff --git a/js/src/devtools/gc-ubench/spidermonkey.js b/js/src/devtools/gc-ubench/spidermonkey.js
new file mode 100644
index 0000000000..0ad0aa9771
--- /dev/null
+++ b/js/src/devtools/gc-ubench/spidermonkey.js
@@ -0,0 +1,57 @@
+/* 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/. */
+
+// SpiderMonkey JS shell benchmark script
+//
+// Usage: run $JS spidermonkey.js --help
+
+loadRelativeToScript("shell-bench.js");
+
+var SpiderMonkey = class extends Host {
+ start_turn() {}
+
+ end_turn() {
+ clearKeptObjects();
+ maybegc();
+ drainJobQueue();
+ }
+
+ suspend(duration) {
+ sleep(duration);
+ }
+
+ get minorGCCount() {
+ return performance.mozMemory.gc.minorGCCount;
+ }
+ get majorGCCount() {
+ return performance.mozMemory.gc.majorGCCount;
+ }
+ get GCSliceCount() {
+ return performance.mozMemory.gc.sliceCount;
+ }
+ get gcBytes() {
+ return performance.mozMemory.gc.zone.gcBytes;
+ }
+ get gcAllocTrigger() {
+ return performance.mozMemory.gc.zone.gcAllocTrigger;
+ }
+
+ features = {
+ haveMemorySizes: true,
+ haveGCCounts: true,
+ };
+};
+
+var gHost = new SpiderMonkey();
+var { opts, rest: mutators } = argparse.parse_args(scriptArgs);
+run(opts, mutators);
+
+print("\nTest results:\n");
+report_results();
+
+var outfile = "spidermonkey-results.json";
+var origOut = redirect(outfile);
+print(JSON.stringify(gPerf.results));
+redirect(origOut);
+print(`Wrote detailed results to ${outfile}`);
diff --git a/js/src/devtools/gc-ubench/test_list.js b/js/src/devtools/gc-ubench/test_list.js
new file mode 100644
index 0000000000..03ed30cf9e
--- /dev/null
+++ b/js/src/devtools/gc-ubench/test_list.js
@@ -0,0 +1,20 @@
+function foreach_test_file(callback) {
+ callback("benchmarks/noAllocation.js");
+ callback("benchmarks/globalArrayNewObject.js");
+ callback("benchmarks/globalArrayArrayLiteral.js");
+ callback("benchmarks/globalArrayLargeArray.js");
+ callback("benchmarks/globalArrayLargeObject.js");
+ callback("benchmarks/globalArrayObjectLiteral.js");
+ callback("benchmarks/globalArrayReallocArray.js");
+ callback("benchmarks/globalArrayBuffer.js");
+ callback("benchmarks/globalArrayFgFinalized.js");
+ callback("benchmarks/largeArrayPropertyAndElements.js");
+ callback("benchmarks/selfCyclicWeakMap.js");
+ callback("benchmarks/pairCyclicWeakMap.js");
+ callback("benchmarks/deepWeakMap.js");
+ callback("benchmarks/textNodes.js");
+ callback("benchmarks/bigTextNodes.js");
+ callback("benchmarks/events.js");
+ callback("benchmarks/expandoEvents.js");
+ callback("benchmarks/propertyTreeSplitting.js");
+}
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;
+ }
+}
diff --git a/js/src/devtools/gc-ubench/v8.js b/js/src/devtools/gc-ubench/v8.js
new file mode 100644
index 0000000000..0c46c2b4c4
--- /dev/null
+++ b/js/src/devtools/gc-ubench/v8.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+// V8 JS shell benchmark script
+//
+// Usage: run d8 v8.js -- --help
+
+globalThis.loadRelativeToScript = load;
+
+load("shell-bench.js");
+
+var V8 = class extends Host {
+ constructor() {
+ super();
+ this.waitTA = new Int32Array(new SharedArrayBuffer(4));
+ }
+
+ start_turn() {}
+
+ end_turn() {}
+
+ suspend(duration) {
+ const response = Atomics.wait(this.waitTA, 0, 0, duration * 1000);
+ if (response !== 'timed-out') {
+ throw new Exception(`unexpected response from Atomics.wait: ${response}`);
+ }
+ }
+
+ features = {
+ haveMemorySizes: false,
+ haveGCCounts: false
+ };
+};
+
+var gHost = new V8();
+
+var { opts, rest: mutators } = argparse.parse_args(arguments);
+run(opts, mutators);
+
+print("\nTest results:\n");
+report_results();