commit a3839a84f3d73173bbf09f6ff958d9f0b0b665ef
parent ad0f7f9dcf1f2f0833ecfb1ccb938c91308d8b9b
Author: Alexandre Poirot <poirot.alex@gmail.com>
Date: Mon, 20 Oct 2025 08:35:27 +0000
Bug 1993014 - [devtools] Also debug leaks via the the cycle collector. r=devtools-reviewers,nchevobbe
I'm moving the logic to understand the memory graphs in a dedicated module
in order to keep allocation tracker focused on the recording.
Differential Revision: https://phabricator.services.mozilla.com/D267822
Diffstat:
7 files changed, 562 insertions(+), 218 deletions(-)
diff --git a/devtools/client/framework/test/allocations/head.js b/devtools/client/framework/test/allocations/head.js
@@ -69,6 +69,12 @@ ChromeUtils.defineLazyGetter(this, "TrackedObjects", () => {
);
});
+ChromeUtils.defineLazyGetter(this, "TraceObjects", () => {
+ return ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs"
+ );
+});
+
// So that PERFHERDER data can be extracted from the logs.
SimpleTest.requestCompleteLog();
@@ -160,9 +166,9 @@ async function stopRecordingAllocations(
const parentProcessData =
await tracker.stopRecordingAllocations(DEBUG_ALLOCATIONS);
- const objectNodeIds = TrackedObjects.getAllNodeIds();
- if (objectNodeIds.length) {
- tracker.traceObjects(objectNodeIds);
+ const leakedObjects = TrackedObjects.getStillAllocatedObjects();
+ if (leakedObjects.length) {
+ await TraceObjects.traceObjects(leakedObjects, tracker.getSnapshotFile());
}
let contentProcessData = null;
@@ -187,12 +193,23 @@ async function stopRecordingAllocations(
const trackedObjectsInContent = await SpecialPowers.spawn(
gBrowser.selectedBrowser,
[],
- () => {
+ async () => {
const TrackedObjects = ChromeUtils.importESModule(
"resource://devtools/shared/test-helpers/tracked-objects.sys.mjs"
);
- const objectNodeIds = TrackedObjects.getAllNodeIds();
- if (objectNodeIds.length) {
+ const leakedObjects = TrackedObjects.getStillAllocatedObjects();
+ if (leakedObjects.length) {
+ const TraceObjects = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs"
+ );
+ // Only pass 'weakRef' as Memory API and 'ubiNodeId' can only be inspected in the parent process
+ await TraceObjects.traceObjects(
+ leakedObjects.map(e => {
+ return {
+ weakRef: e.weakRef,
+ };
+ })
+ );
const { DevToolsLoader } = ChromeUtils.importESModule(
"resource://devtools/shared/loader/Loader.sys.mjs"
);
@@ -202,14 +219,25 @@ async function stopRecordingAllocations(
// As only the parent process can read the file because
// of sandbox restrictions made to content processes regarding file I/O.
const snapshotFile = tracker.getSnapshotFile();
- return { snapshotFile, objectNodeIds };
+ return {
+ snapshotFile,
+ // Only pass ubi::Node::Id from this content process to the parent process.
+ // `leakedObjects`'s `weakRef` attributes can't be transferred across processes.
+ // TraceObjects.traceObjects in the parent process will only log leaks
+ // via the Memory API (and Node Id's).
+ objectUbiNodeIds: leakedObjects.map(e => {
+ return {
+ ubiNodeId: e.ubiNodeId,
+ };
+ }),
+ };
}
return null;
}
);
if (trackedObjectsInContent) {
- tracker.traceObjects(
- trackedObjectsInContent.objectNodeIds,
+ TraceObjects.traceObjects(
+ trackedObjectsInContent.objectUbiNodeIds,
trackedObjectsInContent.snapshotFile
);
}
diff --git a/devtools/shared/test-helpers/allocation-tracker.js b/devtools/shared/test-helpers/allocation-tracker.js
@@ -492,200 +492,6 @@ exports.allocationTracker = function ({
return ChromeUtils.saveHeapSnapshot({ debugger: dbg });
},
- /**
- * Print information about why a list of objects are being held in memory.
- *
- * @param Array<NodeId> objects
- * List of NodeId's of objects to debug. NodeIds can be retrieved
- * via ChromeUtils.getObjectNodeId.
- * @param String snapshotFile
- * Absolute path to a Heap snapshot file retrieved via this.getSnapshotFile.
- * This is used to trace content process objects. We have to record the snapshot
- * from the content process, but can only read it from the parent process because
- * of I/O restrictions in content processes.
- */
- traceObjects(objects, snapshotFile) {
- // There is no API to get the heap snapshot at runtime,
- // the only way is to save it to disk and then load it from disk
- if (!snapshotFile) {
- snapshotFile = this.getSnapshotFile();
- }
- const snapshot = ChromeUtils.readHeapSnapshot(snapshotFile);
-
- function getObjectDescription(id, prefix = 0) {
- prefix = " ".repeat(prefix);
- if (!id) {
- return prefix + "<null>";
- }
- try {
- let stack = [...snapshot.describeNode({ by: "allocationStack" }, id)];
- if (stack) {
- stack = stack.find(([src]) => src != "noStack");
- if (stack) {
- const { line, column, source } = stack[0];
- if (source) {
- const lines = getFileContent(source);
- const lineBefore = lines[line - 2];
- const lineText = lines[line - 1];
- const lineAfter = lines[line];
- const filename = source.substr(source.lastIndexOf("/") + 1);
-
- stack = "allocated at " + source + ":\n";
- // Print one line before and after for context
- if (lineBefore.trim().length) {
- stack += prefix + ` ${filename} @ ${line - 1} \u007C`;
- stack += "\x1b[2m" + lineBefore + "\n";
- }
- stack += prefix + ` ${filename} @ ${line} > \u007C`;
- // Grey out the beginning of the line, before frame's column,
- // and display an arrow before displaying the rest of the line.
- stack +=
- "\x1b[2m" +
- lineText.substr(0, column - 1) +
- "\x1b[0m" +
- "\u21A6 " +
- lineText.substr(column - 1) +
- "\n";
- if (lineAfter.trim().length) {
- stack += prefix + ` ${filename} @ ${line + 1} \u007C`;
- stack += lineAfter;
- }
- } else {
- stack = "(missing source)";
- }
- } else {
- stack = "(without allocation stack)";
- }
- } else {
- stack = "(without description)";
- }
- let objectClass = Object.entries(
- snapshot.describeNode({ by: "objectClass" }, id)
- )[0][0];
- if (objectClass == "other") {
- objectClass = Object.entries(
- snapshot.describeNode({ by: "internalType" }, id)
- )[0][0];
- }
- const arrow = prefix > 0 ? "\\--> " : "";
- return prefix + arrow + objectClass + " " + stack;
- } catch (e) {
- if (e.name == "NS_ERROR_ILLEGAL_VALUE") {
- return (
- prefix + "<not-in-memory-snapshot:is-from-untracked-global?>"
- );
- }
- return prefix + "<invalid:" + id + ":" + e + ">";
- }
- }
-
- const fileContents = new Map();
-
- function getFileContent(url) {
- let content = fileContents.get(url);
- if (content) {
- return content;
- }
- content = readURI(url).split("\n");
- fileContents.set(url, content);
- return content;
- }
-
- function readURI(uri) {
- const { NetUtil } = ChromeUtils.importESModule(
- "resource://gre/modules/NetUtil.sys.mjs",
- { global: "contextual" }
- );
- const stream = NetUtil.newChannel({
- uri: NetUtil.newURI(uri, "UTF-8"),
- loadUsingSystemPrincipal: true,
- }).open();
- const count = stream.available();
- const data = NetUtil.readInputStreamToString(stream, count, {
- charset: "UTF-8",
- });
-
- stream.close();
- return data;
- }
-
- function printPath(src, dst) {
- let paths;
- try {
- paths = snapshot.computeShortestPaths(src, [dst], 10);
- } catch (e) {}
- if (paths && paths.has(dst)) {
- let pathLength = Infinity;
- let n = 0;
- for (const path of paths.get(dst)) {
- n++;
- // Only print the smaller paths.
- // The longer ones will only repeat the smaller ones, with some extra edges.
- if (path.length > pathLength + 1) {
- continue;
- }
- pathLength = path.length;
- logTracker(
- `Path #${n}:\n` +
- path
- .map(({ predecessor, edge }, i) => {
- return (
- getObjectDescription(predecessor, i) +
- "\n" +
- " ".repeat(i) +
- "Holds the following object via '" +
- edge +
- "' attribute:\n"
- );
- })
- .join("") +
- getObjectDescription(dst, path.length)
- );
- }
- } else {
- logTracker("NO-PATH");
- }
- }
-
- const tree = snapshot.computeDominatorTree();
- for (const objectNodeId of objects) {
- logTracker(" # Tracing object #" + objectNodeId + "\n");
-
- // Print the path from the global object down to leaked object.
- // This print the allocation site of each object which has a reference
- // to another object, ultimately leading to our leaked object.
- logTracker("### Path(s) from root:");
- printPath(tree.root, objectNodeId);
-
- /**
- * This happens to be redundant with printPath, but printed the other way around.
- *
- // Print the dominators.
- // i.e. from the leaked object, print all parent objects whichs
- // keeps a reference to the previous object, up to a global object.
- logTracker("### Dominators:");
- let node = objectNodeId,
- logTracker(" " + getObjectDescription(node));
- while ((node = tree.getImmediateDominator(node))) {
- logTracker(" ^-- " + getObjectDescription(node));
- }
- */
-
- /**
- * In case you are not able to figure out what the object is.
- * This will print all what it keeps allocated,
- * kinds of list of attributes
- *
- logTracker("### Dominateds:");
- node = objectNodeId,
- logTracker(" " + getObjectDescription(node));
- for (const n of tree.getImmediatelyDominated(objectNodeId)) {
- logTracker(" --> " + getObjectDescription(n));
- }
- */
- }
- },
-
stop() {
logTracker("Stop logging allocations");
dbg.onNewGlobalObject = undefined;
diff --git a/devtools/shared/test-helpers/browser.toml b/devtools/shared/test-helpers/browser.toml
@@ -1,11 +1,15 @@
[DEFAULT]
tags = "devtools"
subsuite = "devtools"
-support-files = ["allocation-tracker.js"]
+support-files = [
+ "allocation-tracker.js",
+ "cc-analyzer.sys.mjs",
+ "trace-objects.sys.mjs",
+]
["browser_allocation_tracker.js"]
+run-if = ["os == 'linux' && opt"] # Results should be platform agnostic - only run on linux64-opt
skip-if = [
- "os == 'win' && os_version == '11.26100' && processor == 'x86_64' && ccov", # And ccov as this doesn't aim to cover any production code, we are only testing test helpers here.
"verify", # Bug 1730507 - objects without stacks get allocated during the GC of the first test when running multiple times.
]
diff --git a/devtools/shared/test-helpers/browser_allocation_tracker.js b/devtools/shared/test-helpers/browser_allocation_tracker.js
@@ -110,17 +110,26 @@ add_task(async function () {
let transient = {};
TrackedObjects.track(transient);
- is(TrackedObjects.getAllNodeIds().length, 2, "The two objects are reported");
+ is(
+ TrackedObjects.getStillAllocatedObjects().length,
+ 2,
+ "The two objects are reported"
+ );
info("Free the transient object");
transient = null;
Cu.forceGC();
is(
- TrackedObjects.getAllNodeIds().length,
+ TrackedObjects.getStillAllocatedObjects().length,
1,
"We now only have the leaked object"
);
+ is(
+ TrackedObjects.getStillAllocatedObjects()[0].weakRef.get(),
+ leaked,
+ "The still allocated objects is the leaked one"
+ );
TrackedObjects.clear();
});
diff --git a/devtools/shared/test-helpers/cc-analyzer.sys.mjs b/devtools/shared/test-helpers/cc-analyzer.sys.mjs
@@ -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/. */
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+/**
+ * Helper class to retrieve CC/GC Logs via nsICycleCollectorListener interface.
+ */
+export class CCAnalyzer {
+ clear() {
+ this.processingCount = 0;
+ this.graph = {};
+ this.roots = [];
+ this.garbage = [];
+ this.edges = [];
+ this.listener = null;
+ this.count = 0;
+ }
+
+ /**
+ * Run the analyzer by running the CC/GC, which would allow use
+ * to call nsICycleCollectorListener.processNext()
+ * which would call nsICycleCollectorListener.{noteRefCountedObject,noteGCedObject,noteEdge}.
+ *
+ * @param {Boolean} wantAllTraces
+ * See nsICycleCollectorListener.allTraces() jsdoc.
+ */
+ async run(wantAllTraces = false) {
+ this.clear();
+
+ // Instantiate and configure the CC logger
+ this.listener = Cu.createCCLogger();
+ if (wantAllTraces) {
+ dump("CC Analyzer >> all traces!\n");
+ this.listener = this.listener.allTraces();
+ }
+
+ this.listener.disableLog = true;
+ this.listener.wantAfterProcessing = true;
+
+ // Register the CC logger
+ Cu.forceCC(this.listener);
+
+ // Process the entire heap step by step in 10K chunks
+ let done = false;
+ while (!done) {
+ for (let i = 0; i < 10000; i++) {
+ if (!this.listener.processNext(this)) {
+ done = true;
+ break;
+ }
+ }
+ dump("Process CC/GC logs " + this.count + "\n");
+ // Process next chunk after an event loop to avoid freezing the process
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ await new Promise(resolve => setTimeout(resolve, 0));
+ dump("Done!\n");
+ }
+
+ noteRefCountedObject(address, refCount, objectDescription) {
+ const o = this.ensureObject(address);
+ o.address = address;
+ o.refcount = refCount;
+ o.name = objectDescription;
+ }
+
+ noteGCedObject(address, marked, objectDescription, compartmentAddr) {
+ const o = this.ensureObject(address);
+ o.address = address;
+ o.gcmarked = marked;
+ o.name = objectDescription;
+ o.compartment = compartmentAddr;
+ }
+
+ noteEdge(fromAddress, toAddress, edgeName) {
+ const fromObject = this.ensureObject(fromAddress);
+ const toObject = this.ensureObject(toAddress);
+ fromObject.edges.push({ name: edgeName, to: toObject });
+ toObject.owners.push({ name: edgeName, from: fromObject });
+
+ this.edges.push({
+ name: edgeName,
+ from: fromObject,
+ to: toObject,
+ });
+ }
+
+ describeRoot(address, knownEdges) {
+ const o = this.ensureObject(address);
+ o.root = true;
+ o.knownEdges = knownEdges;
+ this.roots.push(o);
+ }
+
+ describeGarbage(address) {
+ const o = this.ensureObject(address);
+ o.garbage = true;
+ this.garbage.push(o);
+ }
+
+ ensureObject(address) {
+ if (!this.graph[address]) {
+ this.count++;
+ this.graph[address] = new CCObject();
+ }
+
+ return this.graph[address];
+ }
+
+ find(text) {
+ const result = [];
+ for (const address in this.graph) {
+ const o = this.graph[address];
+ if (!o.garbage && o.name.includes(text)) {
+ result.push(o);
+ }
+ }
+ return result;
+ }
+
+ findNotJS() {
+ const result = [];
+ for (const address in this.graph) {
+ const o = this.graph[address];
+ if (!o.garbage && o.name.indexOf("JS") != 0) {
+ result.push(o);
+ }
+ }
+ return result;
+ }
+}
+
+class CCObject {
+ constructor() {
+ this.name = "";
+ this.address = null;
+ this.refcount = 0;
+ this.gcmarked = false;
+ this.root = false;
+ this.garbage = false;
+ this.knownEdges = 0;
+ this.edges = [];
+ this.owners = [];
+ }
+}
diff --git a/devtools/shared/test-helpers/trace-objects.sys.mjs b/devtools/shared/test-helpers/trace-objects.sys.mjs
@@ -0,0 +1,345 @@
+/* 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/. */
+
+import { CCAnalyzer } from "chrome://mochitests/content/browser/devtools/shared/test-helpers/cc-analyzer.sys.mjs";
+
+/**
+ * Print information about why a list of objects are being held in memory.
+ *
+ * @param Array<Object {weakRef: Object, ubiNodeId: String> leakedObjects
+ * List of object with 'weakRef' attribute pointing to a leaked object,
+ * and 'ubiNodeId' attribute refering to a ubi::Node's id.
+ * ubi::Node::Id can be retrieved via ChromeUtils.getObjectNodeId.
+ * (we don't compute the ubi::Node::Id from this method as it may be computed from
+ * the content process and 'weakRef' may be null here)
+ * @param String snapshotFile
+ * Absolute path to a Heap snapshot file retrieved via this.getSnapshotFile.
+ * This is used to trace content process objects. We have to record the snapshot
+ * from the content process, but can only read it from the parent process because
+ * of I/O restrictions in content processes.
+ */
+export async function traceObjects(leakedObjects, snapshotFile) {
+ // Initialize the CycleCollector logic
+ let ccLeaks = [];
+ // Only run the Cycle Collector if we have a 'weakRef'
+ if (leakedObjects.some(o => o.weakRef?.get())) {
+ // Put the leaked objects in an array to easily find them in the GC graph.
+ const objects = leakedObjects.map(o => o.weakRef?.get());
+
+ // /!\ Workaround as we can't easily get a CycleCollector CCObject for a given JS Object.
+ // Put a somewhat unique attribute name on the `objects` array (the JS Object).
+ // So that we can later look at all CycleCollector edge names and find this
+ // attribute name. The CCObject which is the source of the edge will represent `objects`.
+ //
+ // We store a JS Object as attribute's value to ensure an edge exists.
+ //
+ // Ideally the CycleCollector API would help identify the address or a given JS Object.
+ const uniqueAttributeName = "allocation-tracker-leak-array";
+ objects[uniqueAttributeName] = {};
+
+ // For some reason, being in the local scope prevent the CC from seeing `objects`.
+ // Workaround this by transiently registering it in the global scope.
+ // eslint-disable-next-line mozilla/reject-globalThis-modification
+ globalThis.ccAnalyzerCCObjects = objects;
+
+ const analyzer = new CCAnalyzer();
+ await analyzer.run(true);
+
+ delete globalThis.ccAnalyzerCCObjects;
+
+ dump(" # objects: " + analyzer.count + "\n");
+ dump(" # edges: " + analyzer.edges.length + "\n");
+
+ // Find `objects` in the graph via its unique attribute name
+ const { from: array } = analyzer.edges.find(
+ e => e.name === uniqueAttributeName
+ );
+
+ ccLeaks = getCCArrayElements(array);
+ }
+
+ // Initialized the Memory API logic
+ let snapshot, tree;
+ if (leakedObjects.some(o => o.ubiNodeId)) {
+ // There is no API to get the heap snapshot at runtime,
+ // the only way is to save it to disk and then load it from disk
+ snapshot = ChromeUtils.readHeapSnapshot(snapshotFile);
+ tree = snapshot.computeDominatorTree();
+ }
+
+ let i = 1;
+ for (const { ubiNodeId } of leakedObjects) {
+ logTracker(`### Tracing leaked object #${i}:\n`);
+
+ if (ccLeaks[i]) {
+ printShortestCCPath(ccLeaks[i]);
+ }
+
+ if (ubiNodeId) {
+ // Print the path from the global object down to leaked object.
+ // This print the allocation site of each object which has a reference
+ // to another object, ultimately leading to our leaked object.
+ printShortestNodePath(snapshot, tree.root, ubiNodeId);
+
+ /**
+ * This happens to be somewhat redundant with printPath, but printed the other way around.
+ *
+ * Print the dominators.
+ * i.e. from the leaked object, print all parent objects whichs
+ */
+ // printDominators(snapshot, tree, ubiNodeId);
+
+ /**
+ * In case you are not able to figure out what the object is.
+ * This will print all what it keeps allocated,
+ * kinds of list of attributes
+ */
+ // printDominated(snapshot, tree, ubiNodeId);
+ }
+ i++;
+ }
+}
+
+// eslint-disable-next-line no-unused-vars
+function printDominators(snapshot, tree, ubiNodeId) {
+ logTracker("### Dominators:");
+ logTracker(" " + getNodeObjectDescription(snapshot, ubiNodeId));
+ while ((ubiNodeId = tree.getImmediateDominator(ubiNodeId))) {
+ logTracker(" ^-- " + getNodeObjectDescription(snapshot, ubiNodeId));
+ }
+}
+
+// eslint-disable-next-line no-unused-vars
+function printDominated(snapshot, tree, ubiNodeId) {
+ logTracker("### Dominateds:");
+ logTracker(" " + getNodeObjectDescription(snapshot, ubiNodeId));
+ for (const n of tree.getImmediatelyDominated(ubiNodeId)) {
+ logTracker(" --> " + getNodeObjectDescription(snapshot, n));
+ }
+}
+
+/**
+ * Return the elements of a given array.
+ *
+ * @param {CCObject} array
+ * @returns {CCObject}
+ */
+function getCCArrayElements(array) {
+ return array.edges
+ .filter(edge => edge.name.startsWith("objectElements"))
+ .map(edge => edge.to);
+}
+
+/**
+ * Return the shortest path between a given CCObject and a CCObject root.
+ *
+ * @param {CCObject} object
+ * @param {array} path (shouldn't be passed from the top callsite)
+ * @param {set} processed (shouldn't be passed from the top callsite)
+ * @returns {Array<Object{from:CCObject, name:String}>} List of CC Edges from object to root.
+ */
+function shortestCCPathToRoot(object, path = [], processed = new Set()) {
+ if (object.root) {
+ return path;
+ }
+ // Ignore the path if that goes through the CC Analyzer logic.
+ if (object.name == "JS Object (Function - #runCC/<)") {
+ return null;
+ }
+ if (processed.has(object)) {
+ return null;
+ }
+ processed.add(object);
+ let shortestPath = null;
+ for (const edge of object.owners) {
+ const edgePath = shortestCCPathToRoot(
+ edge.from,
+ [...path, edge],
+ processed
+ );
+ if (!edgePath) {
+ continue;
+ }
+ if (!shortestPath || edgePath.length < shortestPath.length) {
+ shortestPath = edgePath;
+ }
+ }
+ return shortestPath;
+}
+
+/**
+ * Print the shortest retaining path between a given CCObject and a CCObject root.
+ *
+ * @param {CCObject} object
+ */
+function printShortestCCPath(leak) {
+ const path = shortestCCPathToRoot(leak);
+ let indent = 0;
+ // Reverse the path to print from the root down to the leaked object
+ for (const edge of path.reverse()) {
+ logTracker(
+ " ".repeat(indent) + edge.from.name + " :: " + edge.name + " -> \n"
+ );
+ indent++;
+ }
+}
+
+/**
+ * Return a meaningful description for a given ubi::Node.
+ *
+ * @param {object} snapshot
+ * Memory API snapshot object.
+ * @param {string} id
+ * Memory API ubi::Node's id
+ * @param {number} prefix
+ * Current indentation in the description, if called recursively.
+ * @returns {string}
+ */
+function getNodeObjectDescription(snapshot, id, prefix = 0) {
+ prefix = " ".repeat(prefix);
+ if (!id) {
+ return prefix + "<null>";
+ }
+ try {
+ let stack = [...snapshot.describeNode({ by: "allocationStack" }, id)];
+ if (stack) {
+ stack = stack.find(([src]) => src != "noStack");
+ if (stack) {
+ const { line, column, source } = stack[0];
+ if (source) {
+ const lines = getFileContent(source);
+ const lineBefore = lines[line - 2];
+ const lineText = lines[line - 1];
+ const lineAfter = lines[line];
+ const filename = source.substr(source.lastIndexOf("/") + 1);
+
+ stack = "allocated at " + source + ":\n";
+ // Print one line before and after for context
+ if (lineBefore.trim().length) {
+ stack += prefix + ` ${filename} @ ${line - 1} \u007C`;
+ stack += "\x1b[2m" + lineBefore + "\n";
+ }
+ stack += prefix + ` ${filename} @ ${line} > \u007C`;
+ // Grey out the beginning of the line, before frame's column,
+ // and display an arrow before displaying the rest of the line.
+ stack +=
+ "\x1b[2m" +
+ lineText.substr(0, column - 1) +
+ "\x1b[0m" +
+ "\u21A6 " +
+ lineText.substr(column - 1) +
+ "\n";
+ if (lineAfter.trim().length) {
+ stack += prefix + ` ${filename} @ ${line + 1} \u007C`;
+ stack += lineAfter;
+ }
+ } else {
+ stack = "(missing source)";
+ }
+ } else {
+ stack = "(without allocation stack)";
+ }
+ } else {
+ stack = "(without description)";
+ }
+ let objectClass = Object.entries(
+ snapshot.describeNode({ by: "objectClass" }, id)
+ )[0][0];
+ if (objectClass == "other") {
+ objectClass = Object.entries(
+ snapshot.describeNode({ by: "internalType" }, id)
+ )[0][0];
+ }
+ const arrow = prefix > 0 ? "\\--> " : "";
+ return prefix + arrow + objectClass + " " + stack;
+ } catch (e) {
+ if (e.name == "NS_ERROR_ILLEGAL_VALUE") {
+ return prefix + "<not-in-memory-snapshot:is-from-untracked-global?>";
+ }
+ return prefix + "<invalid:" + id + ":" + e + ">";
+ }
+}
+
+/**
+ * Print the shortest retaining path between two ubi::Node objects.
+ *
+ * @param {object} snapshot
+ * Memory API snapshot object.
+ * @param {string} src
+ * Memory API ubi::Node's id
+ * @param {string} dst
+ * Memory API ubi::Node's id
+ */
+function printShortestNodePath(snapshot, src, dst) {
+ let paths;
+ try {
+ paths = snapshot.computeShortestPaths(src, [dst], 10);
+ } catch (e) {}
+ if (paths && paths.has(dst)) {
+ let pathLength = Infinity;
+ let n = 0;
+ for (const path of paths.get(dst)) {
+ n++;
+ // Only print the smaller paths.
+ // The longer ones will only repeat the smaller ones, with some extra edges.
+ if (path.length > pathLength + 1) {
+ continue;
+ }
+ pathLength = path.length;
+ logTracker(
+ `Path #${n}:\n` +
+ path
+ .map(({ predecessor, edge }, i) => {
+ return (
+ getNodeObjectDescription(snapshot, predecessor, i) +
+ "\n" +
+ " ".repeat(i) +
+ "Holds the following object via '" +
+ edge +
+ "' attribute:\n"
+ );
+ })
+ .join("") +
+ getNodeObjectDescription(snapshot, dst, path.length)
+ );
+ }
+ } else {
+ logTracker("NO-PATH");
+ }
+}
+
+const fileContents = new Map();
+
+function getFileContent(url) {
+ let content = fileContents.get(url);
+ if (content) {
+ return content;
+ }
+ content = readURI(url).split("\n");
+ fileContents.set(url, content);
+ return content;
+}
+
+function readURI(uri) {
+ const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs",
+ { global: "contextual" }
+ );
+ const stream = NetUtil.newChannel({
+ uri: NetUtil.newURI(uri, "UTF-8"),
+ loadUsingSystemPrincipal: true,
+ }).open();
+ const count = stream.available();
+ const data = NetUtil.readInputStreamToString(stream, count, {
+ charset: "UTF-8",
+ });
+
+ stream.close();
+ return data;
+}
+
+function logTracker(message) {
+ // Use special characters to grey out [TRACKER] and make allocation logs stands out.
+ dump(` \x1b[2m[TRACKER]\x1b[0m ${message}\n`);
+}
diff --git a/devtools/shared/test-helpers/tracked-objects.sys.mjs b/devtools/shared/test-helpers/tracked-objects.sys.mjs
@@ -14,7 +14,7 @@ const objects = [];
/**
* Request to track why the given object is kept in memory,
- * later on, when retrieving all the watched object via getAllNodeIds.
+ * later on, when retrieving all the watched object via getStillAllocatedObjects.
*/
export function track(obj) {
// We store a weak reference, so that we do force keeping the object in memory!!
@@ -22,20 +22,25 @@ export function track(obj) {
}
/**
- * Return the NodeId's of all the objects passed via `track()` method.
+ * Return the reference and NodeId's of all the objects passed via `track()` method,
+ * which are still hold in memory.
*
- * NodeId's are used by spidermonkey memory API to designates JS objects in head snapshots.
+ * NodeId's are used by spidermonkey memory API to designates JS objects in heap snapshots.
*/
-export function getAllNodeIds() {
- // Filter out objects which have been freed already
+export function getStillAllocatedObjects() {
return (
objects
- .map(weak => weak.get())
- .filter(obj => !!obj)
- // Convert objects from here instead of from allocation tracker in order
- // to be from the shared system compartment and avoid trying to compute the NodeId
- // of a wrapper!
- .map(ChromeUtils.getObjectNodeId)
+ // Filter out objects which have been freed already
+ .filter(ref => !!ref.get())
+ .map(weakRef => {
+ return {
+ weakRef,
+ // Convert objects from here instead of from allocation tracker in order
+ // to be from the shared system compartment and avoid trying to compute the NodeId
+ // of a wrapper!
+ ubiNodeId: ChromeUtils.getObjectNodeId(weakRef.get()),
+ };
+ })
);
}