tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mdevtools/client/framework/test/allocations/head.js | 46+++++++++++++++++++++++++++++++++++++---------
Mdevtools/shared/test-helpers/allocation-tracker.js | 194-------------------------------------------------------------------------------
Mdevtools/shared/test-helpers/browser.toml | 8++++++--
Mdevtools/shared/test-helpers/browser_allocation_tracker.js | 13+++++++++++--
Adevtools/shared/test-helpers/cc-analyzer.sys.mjs | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adevtools/shared/test-helpers/trace-objects.sys.mjs | 345+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdevtools/shared/test-helpers/tracked-objects.sys.mjs | 27++++++++++++++++-----------
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()), + }; + }) ); }