summaryrefslogtreecommitdiffstats
path: root/devtools/shared/test-helpers/allocation-tracker.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/test-helpers/allocation-tracker.js')
-rw-r--r--devtools/shared/test-helpers/allocation-tracker.js249
1 files changed, 249 insertions, 0 deletions
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;
+ },
+ };
+};