summaryrefslogtreecommitdiffstats
path: root/devtools/shared/test-helpers
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/shared/test-helpers
parentInitial commit. (diff)
downloadfirefox-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 'devtools/shared/test-helpers')
-rw-r--r--devtools/shared/test-helpers/.eslintrc.js10
-rw-r--r--devtools/shared/test-helpers/allocation-tracker.js249
-rw-r--r--devtools/shared/test-helpers/browser.ini7
-rw-r--r--devtools/shared/test-helpers/browser_allocation_tracker.js82
4 files changed, 348 insertions, 0 deletions
diff --git a/devtools/shared/test-helpers/.eslintrc.js b/devtools/shared/test-helpers/.eslintrc.js
new file mode 100644
index 0000000000..47885ab596
--- /dev/null
+++ b/devtools/shared/test-helpers/.eslintrc.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/. */
+
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/shared/test-helpers/allocation-tracker.js b/devtools/shared/test-helpers/allocation-tracker.js
new file mode 100644
index 0000000000..510fdbd29b
--- /dev/null
+++ b/devtools/shared/test-helpers/allocation-tracker.js
@@ -0,0 +1,249 @@
+/* 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/. */
+
+/*
+ * This file helps tracking Javascript object allocations.
+ * It is only included in local builds as a debugging helper.
+ *
+ * It is typicaly used when running DevTools tests (either mochitests or DAMP).
+ * To use it, you need to set the following environment variable:
+ * DEBUG_DEVTOOLS_ALLOCATIONS="normal"
+ * This will only print the number of JS objects created during your test.
+ * DEBUG_DEVTOOLS_ALLOCATIONS="verbose"
+ * This will print the allocation sites of all the JS objects created during your
+ * test. i.e. from which files and lines the objects have been created.
+ * In both cases, look for "DEVTOOLS ALLOCATION" in your terminal to see tracker's
+ * output.
+ *
+ * But you can also import it from your test script if you want to focus on one
+ * particular piece of code:
+ * const { allocationTracker } =
+ * require("devtools/shared/test-helpers/allocation-tracker");
+ * // Calling `allocationTracker` will immediately start recording allocations
+ * let tracker = allocationTracker();
+ *
+ * // Do something
+ *
+ * // If you want to log all the allocation sites, call this method:
+ * tracker.logAllocationSites();
+ * // Or, if you want to only print the number of objects being allocated, call this:
+ * tracker.logCount();
+ * // Once you are done, stop the tracker as it slow down execution a lot.
+ * tracker.stop();
+ */
+
+"use strict";
+
+const { Cu } = require("chrome");
+const ChromeUtils = require("ChromeUtils");
+
+const global = Cu.getGlobalForObject(this);
+const { addDebuggerToGlobal } = ChromeUtils.import(
+ "resource://gre/modules/jsdebugger.jsm"
+);
+addDebuggerToGlobal(global);
+
+/**
+ * Start recording JS object allocations.
+ *
+ * @param Object watchGlobal
+ * One global object to observe. Only allocation made from this global
+ * will be recorded.
+ * @param Boolean watchAllGlobals
+ * If true, allocations from everywhere are going to be recorded.
+ * @param Boolean watchAllGlobals
+ * If true, only allocations made from DevTools contexts are going to be recorded.
+ */
+exports.allocationTracker = function({
+ watchGlobal,
+ watchAllGlobals,
+ watchDevToolsGlobals,
+} = {}) {
+ dump("DEVTOOLS ALLOCATION: Start logging allocations\n");
+ let dbg = new global.Debugger();
+
+ // Enable allocation site tracking, to have the stack for each allocation
+ dbg.memory.trackingAllocationSites = true;
+ // Force saving *all* the allocation sites
+ dbg.memory.allocationSamplingProbability = 1.0;
+ // Bumps the default buffer size, which may prevent recording all the test allocations
+ dbg.memory.maxAllocationsLogLength = 5000000;
+
+ let acceptGlobal;
+ if (watchGlobal) {
+ acceptGlobal = () => false;
+ dbg.addDebuggee(watchGlobal);
+ } else if (watchAllGlobals) {
+ acceptGlobal = () => true;
+ } else if (watchDevToolsGlobals) {
+ // Only accept globals related to DevTools
+ acceptGlobal = g => {
+ // self-hosting-global crashes when trying to call unsafeDereference
+ if (g.class == "self-hosting-global") {
+ return false;
+ }
+ const ref = g.unsafeDereference();
+ const location = Cu.getRealmLocation(ref);
+ const accept = !!location.match(/devtools/i);
+ dump(
+ "TRACKER NEW GLOBAL: " + (accept ? "+" : "-") + " : " + location + "\n"
+ );
+ return accept;
+ };
+ }
+
+ // Watch all globals
+ if (watchAllGlobals || watchDevToolsGlobals) {
+ dbg.addAllGlobalsAsDebuggees();
+
+ for (const g of dbg.getDebuggees()) {
+ if (!acceptGlobal(g)) {
+ dbg.removeDebuggee(g);
+ }
+ }
+ }
+
+ // Remove this global to ignore all its object/JS
+ dbg.removeDebuggee(global);
+
+ // addAllGlobalsAsDebuggees won't automatically track new ones,
+ // so ensure tracking all new globals
+ dbg.onNewGlobalObject = function(g) {
+ if (acceptGlobal(g)) {
+ dbg.addDebuggee(g);
+ }
+ };
+
+ return {
+ get overflowed() {
+ return dbg.memory.allocationsLogOverflowed;
+ },
+
+ /**
+ * Print to stdout data about all recorded allocations
+ *
+ * It prints an array of allocations per file, sorted by files allocating the most
+ * objects. And get detail of allocation per line.
+ *
+ * [{ src: "chrome://devtools/content/framework/toolbox.js",
+ * count: 210, // Total # of allocs for toolbox.js
+ * lines: [
+ * "10: 200", // toolbox.js allocation 200 objects on line 10
+ * "124: 10
+ * ]
+ * },
+ * { src: "chrome://devtools/content/inspector/inspector.js",
+ * count: 12,
+ * lines: [
+ * "20: 12",
+ * ]
+ * }]
+ *
+ * @param first Number
+ * Retrieve only the top $first script allocation the most
+ * objects
+ */
+ logAllocationSites({ first = 5 } = {}) {
+ // Fetch all allocation sites from Debugger API
+ const allocations = dbg.memory.drainAllocationsLog();
+
+ // Process Debugger API data to store allocations by file
+ // sources = {
+ // "chrome://devtools/content/framework/toolbox.js": {
+ // count: 10, // total # of allocs for toolbox.js
+ // lines: {
+ // 10: 200, // total # of allocs for toolbox.js line 10
+ // 124: 10, // same, for line 124
+ // ..
+ // }
+ // }
+ // }
+ const sources = {};
+ for (const alloc of allocations) {
+ const { frame } = alloc;
+ let src = "UNKNOWN";
+ let line = -1;
+ try {
+ if (frame) {
+ src = frame.source || "UNKNOWN";
+ line = frame.line || -1;
+ }
+ } catch (e) {
+ // For some frames accessing source throws
+ }
+
+ let item = sources[src];
+ if (!item) {
+ item = sources[src] = { count: 0, lines: {} };
+ }
+ item.count++;
+ if (line != -1) {
+ if (!item.lines[line]) {
+ item.lines[line] = 0;
+ }
+ item.lines[line]++;
+ }
+ }
+
+ const allocationList = Object.entries(sources)
+ // Sort by number of total object
+ .sort(([srcA, itemA], [srcB, itemB]) => itemA.count < itemB.count)
+ // Keep only the first 5 sources, with the most allocations
+ .filter((_, i) => i < first)
+ .map(([src, item]) => {
+ const lines = [];
+ Object.entries(item.lines)
+ .filter(([line, count]) => count > 5)
+ .sort(([lineA, countA], [lineB, countB]) => {
+ if (countA != countB) {
+ return countA < countB;
+ }
+ return lineA < lineB;
+ })
+ .forEach(([line, count]) => {
+ // Compress the data to make it readable on stdout
+ lines.push(line + ": " + count);
+ });
+ return { src, count: item.count, lines };
+ });
+ dump(
+ "DEVTOOLS ALLOCATION: Javascript object allocations: " +
+ allocations.length +
+ "\n" +
+ JSON.stringify(allocationList, null, 2) +
+ "\n"
+ );
+ },
+
+ logCount() {
+ dump(
+ "DEVTOOLS ALLOCATION: Javascript object allocations: " +
+ this.countAllocations() +
+ "\n"
+ );
+ },
+
+ countAllocations() {
+ // Fetch all allocation sites from Debugger API
+ const allocations = dbg.memory.drainAllocationsLog();
+ return allocations.length;
+ },
+
+ flushAllocations() {
+ dbg.memory.drainAllocationsLog();
+ },
+
+ stillAllocatedObjects() {
+ const sensus = dbg.memory.takeCensus({ breakdown: { by: "count" } });
+ return sensus.count;
+ },
+
+ stop() {
+ dump("DEVTOOLS ALLOCATION: Stop logging allocations\n");
+ dbg.onNewGlobalObject = undefined;
+ dbg.removeAllDebuggees();
+ dbg = null;
+ },
+ };
+};
diff --git a/devtools/shared/test-helpers/browser.ini b/devtools/shared/test-helpers/browser.ini
new file mode 100644
index 0000000000..c729f4d473
--- /dev/null
+++ b/devtools/shared/test-helpers/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ allocation-tracker.js
+
+[browser_allocation_tracker.js]
diff --git a/devtools/shared/test-helpers/browser_allocation_tracker.js b/devtools/shared/test-helpers/browser_allocation_tracker.js
new file mode 100644
index 0000000000..8fe26dac1b
--- /dev/null
+++ b/devtools/shared/test-helpers/browser_allocation_tracker.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Load the tracker in a dedicated loader using invisibleToDebugger and freshCompartment
+// so that it can inspect any other module/compartment, even DevTools, chrome,
+// and this script!
+const { DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+);
+const loader = new DevToolsLoader({
+ invisibleToDebugger: true,
+ freshCompartment: true,
+});
+const { allocationTracker } = loader.require(
+ "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker"
+);
+
+add_task(async function() {
+ // Use a sandbox to allocate test javascript object in order to avoid any
+ // external noise
+ const global = Cu.Sandbox("http://example.com");
+
+ const tracker = allocationTracker({ watchGlobal: global });
+ const before = tracker.stillAllocatedObjects();
+
+ /* eslint-disable no-undef */
+ Cu.evalInSandbox(
+ "let list; new " +
+ function() {
+ list = [];
+ for (let i = 0; i < 1000; i++) {
+ list.push({});
+ }
+ },
+ global,
+ undefined,
+ "test-file.js",
+ 1,
+ /* enforceFilenameRestrictions */ false
+ );
+ /* eslint-enable no-undef */
+
+ const allocations = tracker.countAllocations();
+ ok(
+ allocations > 1000,
+ `At least 1000 objects are reported as created (${allocations})`
+ );
+
+ // Uncomment this and comment the call to `countAllocations` to debug the allocations.
+ // The call to `countAllocations` will reset the allocation record.
+ // tracker.logAllocationSites();
+
+ const afterCreation = tracker.stillAllocatedObjects();
+ ok(
+ afterCreation - before > 1000,
+ `At least 1000 more objects are reported still allocated (${before}` +
+ ` + ${afterCreation - before} -> ${afterCreation})`
+ );
+
+ Cu.evalInSandbox(
+ "list = null;",
+ global,
+ undefined,
+ "test-file.js",
+ 7,
+ /* enforceFilenameRestrictions */ false
+ );
+
+ Cu.forceGC();
+ Cu.forceCC();
+
+ const afterGC = tracker.stillAllocatedObjects();
+ ok(
+ afterCreation - afterGC > 1000,
+ `At least 1000 less objects are reported still allocated (${afterCreation}` +
+ ` -(${afterGC - afterCreation})-> ${afterGC})`
+ );
+
+ tracker.stop();
+});