diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /js/src/devtools/gc-ubench | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.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')
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> + Garbage items per frame: + <input type="text" id="garbage-per-frame" size="5" value="8K" + onchange="garbage_per_frame_changed()"></input> +</div> +<div> + 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> + Time remaining: <span id="test-progress">(not running)</span> +</div + +<div> + 60 fps: <span id="pct60">n/a</span> + 45 fps: <span id="pct45">n/a</span> + 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(); |