Skip to content

Commit a3839a8

Browse files
committed
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
1 parent ad0f7f9 commit a3839a8

File tree

7 files changed

+562
-218
lines changed

7 files changed

+562
-218
lines changed

devtools/client/framework/test/allocations/head.js

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ ChromeUtils.defineLazyGetter(this, "TrackedObjects", () => {
6969
);
7070
});
7171

72+
ChromeUtils.defineLazyGetter(this, "TraceObjects", () => {
73+
return ChromeUtils.importESModule(
74+
"chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs"
75+
);
76+
});
77+
7278
// So that PERFHERDER data can be extracted from the logs.
7379
SimpleTest.requestCompleteLog();
7480

@@ -160,9 +166,9 @@ async function stopRecordingAllocations(
160166
const parentProcessData =
161167
await tracker.stopRecordingAllocations(DEBUG_ALLOCATIONS);
162168

163-
const objectNodeIds = TrackedObjects.getAllNodeIds();
164-
if (objectNodeIds.length) {
165-
tracker.traceObjects(objectNodeIds);
169+
const leakedObjects = TrackedObjects.getStillAllocatedObjects();
170+
if (leakedObjects.length) {
171+
await TraceObjects.traceObjects(leakedObjects, tracker.getSnapshotFile());
166172
}
167173

168174
let contentProcessData = null;
@@ -187,12 +193,23 @@ async function stopRecordingAllocations(
187193
const trackedObjectsInContent = await SpecialPowers.spawn(
188194
gBrowser.selectedBrowser,
189195
[],
190-
() => {
196+
async () => {
191197
const TrackedObjects = ChromeUtils.importESModule(
192198
"resource://devtools/shared/test-helpers/tracked-objects.sys.mjs"
193199
);
194-
const objectNodeIds = TrackedObjects.getAllNodeIds();
195-
if (objectNodeIds.length) {
200+
const leakedObjects = TrackedObjects.getStillAllocatedObjects();
201+
if (leakedObjects.length) {
202+
const TraceObjects = ChromeUtils.importESModule(
203+
"chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs"
204+
);
205+
// Only pass 'weakRef' as Memory API and 'ubiNodeId' can only be inspected in the parent process
206+
await TraceObjects.traceObjects(
207+
leakedObjects.map(e => {
208+
return {
209+
weakRef: e.weakRef,
210+
};
211+
})
212+
);
196213
const { DevToolsLoader } = ChromeUtils.importESModule(
197214
"resource://devtools/shared/loader/Loader.sys.mjs"
198215
);
@@ -202,14 +219,25 @@ async function stopRecordingAllocations(
202219
// As only the parent process can read the file because
203220
// of sandbox restrictions made to content processes regarding file I/O.
204221
const snapshotFile = tracker.getSnapshotFile();
205-
return { snapshotFile, objectNodeIds };
222+
return {
223+
snapshotFile,
224+
// Only pass ubi::Node::Id from this content process to the parent process.
225+
// `leakedObjects`'s `weakRef` attributes can't be transferred across processes.
226+
// TraceObjects.traceObjects in the parent process will only log leaks
227+
// via the Memory API (and Node Id's).
228+
objectUbiNodeIds: leakedObjects.map(e => {
229+
return {
230+
ubiNodeId: e.ubiNodeId,
231+
};
232+
}),
233+
};
206234
}
207235
return null;
208236
}
209237
);
210238
if (trackedObjectsInContent) {
211-
tracker.traceObjects(
212-
trackedObjectsInContent.objectNodeIds,
239+
TraceObjects.traceObjects(
240+
trackedObjectsInContent.objectUbiNodeIds,
213241
trackedObjectsInContent.snapshotFile
214242
);
215243
}

devtools/shared/test-helpers/allocation-tracker.js

Lines changed: 0 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -492,200 +492,6 @@ exports.allocationTracker = function ({
492492
return ChromeUtils.saveHeapSnapshot({ debugger: dbg });
493493
},
494494

495-
/**
496-
* Print information about why a list of objects are being held in memory.
497-
*
498-
* @param Array<NodeId> objects
499-
* List of NodeId's of objects to debug. NodeIds can be retrieved
500-
* via ChromeUtils.getObjectNodeId.
501-
* @param String snapshotFile
502-
* Absolute path to a Heap snapshot file retrieved via this.getSnapshotFile.
503-
* This is used to trace content process objects. We have to record the snapshot
504-
* from the content process, but can only read it from the parent process because
505-
* of I/O restrictions in content processes.
506-
*/
507-
traceObjects(objects, snapshotFile) {
508-
// There is no API to get the heap snapshot at runtime,
509-
// the only way is to save it to disk and then load it from disk
510-
if (!snapshotFile) {
511-
snapshotFile = this.getSnapshotFile();
512-
}
513-
const snapshot = ChromeUtils.readHeapSnapshot(snapshotFile);
514-
515-
function getObjectDescription(id, prefix = 0) {
516-
prefix = " ".repeat(prefix);
517-
if (!id) {
518-
return prefix + "<null>";
519-
}
520-
try {
521-
let stack = [...snapshot.describeNode({ by: "allocationStack" }, id)];
522-
if (stack) {
523-
stack = stack.find(([src]) => src != "noStack");
524-
if (stack) {
525-
const { line, column, source } = stack[0];
526-
if (source) {
527-
const lines = getFileContent(source);
528-
const lineBefore = lines[line - 2];
529-
const lineText = lines[line - 1];
530-
const lineAfter = lines[line];
531-
const filename = source.substr(source.lastIndexOf("/") + 1);
532-
533-
stack = "allocated at " + source + ":\n";
534-
// Print one line before and after for context
535-
if (lineBefore.trim().length) {
536-
stack += prefix + ` ${filename} @ ${line - 1} \u007C`;
537-
stack += "\x1b[2m" + lineBefore + "\n";
538-
}
539-
stack += prefix + ` ${filename} @ ${line} > \u007C`;
540-
// Grey out the beginning of the line, before frame's column,
541-
// and display an arrow before displaying the rest of the line.
542-
stack +=
543-
"\x1b[2m" +
544-
lineText.substr(0, column - 1) +
545-
"\x1b[0m" +
546-
"\u21A6 " +
547-
lineText.substr(column - 1) +
548-
"\n";
549-
if (lineAfter.trim().length) {
550-
stack += prefix + ` ${filename} @ ${line + 1} \u007C`;
551-
stack += lineAfter;
552-
}
553-
} else {
554-
stack = "(missing source)";
555-
}
556-
} else {
557-
stack = "(without allocation stack)";
558-
}
559-
} else {
560-
stack = "(without description)";
561-
}
562-
let objectClass = Object.entries(
563-
snapshot.describeNode({ by: "objectClass" }, id)
564-
)[0][0];
565-
if (objectClass == "other") {
566-
objectClass = Object.entries(
567-
snapshot.describeNode({ by: "internalType" }, id)
568-
)[0][0];
569-
}
570-
const arrow = prefix > 0 ? "\\--> " : "";
571-
return prefix + arrow + objectClass + " " + stack;
572-
} catch (e) {
573-
if (e.name == "NS_ERROR_ILLEGAL_VALUE") {
574-
return (
575-
prefix + "<not-in-memory-snapshot:is-from-untracked-global?>"
576-
);
577-
}
578-
return prefix + "<invalid:" + id + ":" + e + ">";
579-
}
580-
}
581-
582-
const fileContents = new Map();
583-
584-
function getFileContent(url) {
585-
let content = fileContents.get(url);
586-
if (content) {
587-
return content;
588-
}
589-
content = readURI(url).split("\n");
590-
fileContents.set(url, content);
591-
return content;
592-
}
593-
594-
function readURI(uri) {
595-
const { NetUtil } = ChromeUtils.importESModule(
596-
"resource://gre/modules/NetUtil.sys.mjs",
597-
{ global: "contextual" }
598-
);
599-
const stream = NetUtil.newChannel({
600-
uri: NetUtil.newURI(uri, "UTF-8"),
601-
loadUsingSystemPrincipal: true,
602-
}).open();
603-
const count = stream.available();
604-
const data = NetUtil.readInputStreamToString(stream, count, {
605-
charset: "UTF-8",
606-
});
607-
608-
stream.close();
609-
return data;
610-
}
611-
612-
function printPath(src, dst) {
613-
let paths;
614-
try {
615-
paths = snapshot.computeShortestPaths(src, [dst], 10);
616-
} catch (e) {}
617-
if (paths && paths.has(dst)) {
618-
let pathLength = Infinity;
619-
let n = 0;
620-
for (const path of paths.get(dst)) {
621-
n++;
622-
// Only print the smaller paths.
623-
// The longer ones will only repeat the smaller ones, with some extra edges.
624-
if (path.length > pathLength + 1) {
625-
continue;
626-
}
627-
pathLength = path.length;
628-
logTracker(
629-
`Path #${n}:\n` +
630-
path
631-
.map(({ predecessor, edge }, i) => {
632-
return (
633-
getObjectDescription(predecessor, i) +
634-
"\n" +
635-
" ".repeat(i) +
636-
"Holds the following object via '" +
637-
edge +
638-
"' attribute:\n"
639-
);
640-
})
641-
.join("") +
642-
getObjectDescription(dst, path.length)
643-
);
644-
}
645-
} else {
646-
logTracker("NO-PATH");
647-
}
648-
}
649-
650-
const tree = snapshot.computeDominatorTree();
651-
for (const objectNodeId of objects) {
652-
logTracker(" # Tracing object #" + objectNodeId + "\n");
653-
654-
// Print the path from the global object down to leaked object.
655-
// This print the allocation site of each object which has a reference
656-
// to another object, ultimately leading to our leaked object.
657-
logTracker("### Path(s) from root:");
658-
printPath(tree.root, objectNodeId);
659-
660-
/**
661-
* This happens to be redundant with printPath, but printed the other way around.
662-
*
663-
// Print the dominators.
664-
// i.e. from the leaked object, print all parent objects whichs
665-
// keeps a reference to the previous object, up to a global object.
666-
logTracker("### Dominators:");
667-
let node = objectNodeId,
668-
logTracker(" " + getObjectDescription(node));
669-
while ((node = tree.getImmediateDominator(node))) {
670-
logTracker(" ^-- " + getObjectDescription(node));
671-
}
672-
*/
673-
674-
/**
675-
* In case you are not able to figure out what the object is.
676-
* This will print all what it keeps allocated,
677-
* kinds of list of attributes
678-
*
679-
logTracker("### Dominateds:");
680-
node = objectNodeId,
681-
logTracker(" " + getObjectDescription(node));
682-
for (const n of tree.getImmediatelyDominated(objectNodeId)) {
683-
logTracker(" --> " + getObjectDescription(n));
684-
}
685-
*/
686-
}
687-
},
688-
689495
stop() {
690496
logTracker("Stop logging allocations");
691497
dbg.onNewGlobalObject = undefined;

devtools/shared/test-helpers/browser.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
[DEFAULT]
22
tags = "devtools"
33
subsuite = "devtools"
4-
support-files = ["allocation-tracker.js"]
4+
support-files = [
5+
"allocation-tracker.js",
6+
"cc-analyzer.sys.mjs",
7+
"trace-objects.sys.mjs",
8+
]
59

610
["browser_allocation_tracker.js"]
11+
run-if = ["os == 'linux' && opt"] # Results should be platform agnostic - only run on linux64-opt
712
skip-if = [
8-
"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.
913
"verify", # Bug 1730507 - objects without stacks get allocated during the GC of the first test when running multiple times.
1014
]
1115

devtools/shared/test-helpers/browser_allocation_tracker.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,26 @@ add_task(async function () {
110110
let transient = {};
111111
TrackedObjects.track(transient);
112112

113-
is(TrackedObjects.getAllNodeIds().length, 2, "The two objects are reported");
113+
is(
114+
TrackedObjects.getStillAllocatedObjects().length,
115+
2,
116+
"The two objects are reported"
117+
);
114118

115119
info("Free the transient object");
116120
transient = null;
117121
Cu.forceGC();
118122

119123
is(
120-
TrackedObjects.getAllNodeIds().length,
124+
TrackedObjects.getStillAllocatedObjects().length,
121125
1,
122126
"We now only have the leaked object"
123127
);
128+
is(
129+
TrackedObjects.getStillAllocatedObjects()[0].weakRef.get(),
130+
leaked,
131+
"The still allocated objects is the leaked one"
132+
);
124133
TrackedObjects.clear();
125134
});
126135

0 commit comments

Comments
 (0)