summaryrefslogtreecommitdiffstats
path: root/js/src/devtools/gc-ubench/harness.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--js/src/devtools/gc-ubench/harness.js328
1 files changed, 328 insertions, 0 deletions
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;
+}