trace-objects.sys.mjs (11388B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { CCAnalyzer } from "chrome://mochitests/content/browser/devtools/shared/test-helpers/cc-analyzer.sys.mjs"; 6 7 /** 8 * Print information about why a list of objects are being held in memory. 9 * 10 * @param Array<Object {weakRef: Object, ubiNodeId: String> leakedObjects 11 * List of object with 'weakRef' attribute pointing to a leaked object, 12 * and 'ubiNodeId' attribute refering to a ubi::Node's id. 13 * ubi::Node::Id can be retrieved via ChromeUtils.getObjectNodeId. 14 * (we don't compute the ubi::Node::Id from this method as it may be computed from 15 * the content process and 'weakRef' may be null here) 16 * @param String snapshotFile 17 * Absolute path to a Heap snapshot file retrieved via this.getSnapshotFile. 18 * This is used to trace content process objects. We have to record the snapshot 19 * from the content process, but can only read it from the parent process because 20 * of I/O restrictions in content processes. 21 */ 22 export async function traceObjects(leakedObjects, snapshotFile) { 23 // Initialize the CycleCollector logic 24 let ccLeaks = []; 25 // Only run the Cycle Collector if we have a 'weakRef' 26 if (leakedObjects.some(o => o.weakRef?.get())) { 27 // Put the leaked objects in an array to easily find them in the GC graph. 28 const objects = leakedObjects.map(o => o.weakRef?.get()); 29 30 // /!\ Workaround as we can't easily get a CycleCollector CCObject for a given JS Object. 31 // Put a somewhat unique attribute name on the `objects` array (the JS Object). 32 // So that we can later look at all CycleCollector edge names and find this 33 // attribute name. The CCObject which is the source of the edge will represent `objects`. 34 // 35 // We store a JS Object as attribute's value to ensure an edge exists. 36 // 37 // Ideally the CycleCollector API would help identify the address or a given JS Object. 38 const uniqueAttributeName = "allocation-tracker-leak-array"; 39 objects[uniqueAttributeName] = {}; 40 41 // For some reason, being in the local scope prevent the CC from seeing `objects`. 42 // Workaround this by transiently registering it in the global scope. 43 // eslint-disable-next-line mozilla/reject-globalThis-modification 44 globalThis.ccAnalyzerCCObjects = objects; 45 46 const analyzer = new CCAnalyzer(); 47 await analyzer.run(true); 48 49 delete globalThis.ccAnalyzerCCObjects; 50 51 dump(" # objects: " + analyzer.count + "\n"); 52 dump(" # edges: " + analyzer.edges.length + "\n"); 53 54 // Find `objects` in the graph via its unique attribute name 55 const { from: array } = analyzer.edges.find( 56 e => e.name === uniqueAttributeName 57 ); 58 59 ccLeaks = getCCArrayElements(array); 60 } 61 62 // Initialized the Memory API logic 63 let snapshot, tree; 64 if (leakedObjects.some(o => o.ubiNodeId)) { 65 // There is no API to get the heap snapshot at runtime, 66 // the only way is to save it to disk and then load it from disk 67 snapshot = ChromeUtils.readHeapSnapshot(snapshotFile); 68 tree = snapshot.computeDominatorTree(); 69 } 70 71 let i = 1; 72 for (const { ubiNodeId } of leakedObjects) { 73 logTracker(`### Tracing leaked object #${i}:\n`); 74 75 if (ccLeaks[i]) { 76 printShortestCCPath(ccLeaks[i]); 77 } 78 79 if (ubiNodeId) { 80 // Print the path from the global object down to leaked object. 81 // This print the allocation site of each object which has a reference 82 // to another object, ultimately leading to our leaked object. 83 printShortestNodePath(snapshot, tree.root, ubiNodeId); 84 85 /** 86 * This happens to be somewhat redundant with printPath, but printed the other way around. 87 * 88 * Print the dominators. 89 * i.e. from the leaked object, print all parent objects whichs 90 */ 91 // printDominators(snapshot, tree, ubiNodeId); 92 93 /** 94 * In case you are not able to figure out what the object is. 95 * This will print all what it keeps allocated, 96 * kinds of list of attributes 97 */ 98 // printDominated(snapshot, tree, ubiNodeId); 99 } 100 i++; 101 } 102 } 103 104 // eslint-disable-next-line no-unused-vars 105 function printDominators(snapshot, tree, ubiNodeId) { 106 logTracker("### Dominators:"); 107 logTracker(" " + getNodeObjectDescription(snapshot, ubiNodeId)); 108 while ((ubiNodeId = tree.getImmediateDominator(ubiNodeId))) { 109 logTracker(" ^-- " + getNodeObjectDescription(snapshot, ubiNodeId)); 110 } 111 } 112 113 // eslint-disable-next-line no-unused-vars 114 function printDominated(snapshot, tree, ubiNodeId) { 115 logTracker("### Dominateds:"); 116 logTracker(" " + getNodeObjectDescription(snapshot, ubiNodeId)); 117 for (const n of tree.getImmediatelyDominated(ubiNodeId)) { 118 logTracker(" --> " + getNodeObjectDescription(snapshot, n)); 119 } 120 } 121 122 /** 123 * Return the elements of a given array. 124 * 125 * @param {CCObject} array 126 * @returns {CCObject} 127 */ 128 function getCCArrayElements(array) { 129 return array.edges 130 .filter(edge => edge.name.startsWith("objectElements")) 131 .map(edge => edge.to); 132 } 133 134 /** 135 * Return the shortest path between a given CCObject and a CCObject root. 136 * 137 * @param {CCObject} object 138 * @param {Array} path (shouldn't be passed from the top callsite) 139 * @param {set} processed (shouldn't be passed from the top callsite) 140 * @returns {Array<Object{from:CCObject, name:String}>} List of CC Edges from object to root. 141 */ 142 function shortestCCPathToRoot(object, path = [], processed = new Set()) { 143 if (object.root) { 144 return path; 145 } 146 // Ignore the path if that goes through the CC Analyzer logic. 147 if (object.name == "JS Object (Function - #runCC/<)") { 148 return null; 149 } 150 if (processed.has(object)) { 151 return null; 152 } 153 processed.add(object); 154 let shortestPath = null; 155 for (const edge of object.owners) { 156 const edgePath = shortestCCPathToRoot( 157 edge.from, 158 [...path, edge], 159 processed 160 ); 161 if (!edgePath) { 162 continue; 163 } 164 if (!shortestPath || edgePath.length < shortestPath.length) { 165 shortestPath = edgePath; 166 } 167 } 168 return shortestPath; 169 } 170 171 /** 172 * Print the shortest retaining path between a given CCObject and a CCObject root. 173 * 174 * @param {CCObject} object 175 */ 176 function printShortestCCPath(leak) { 177 const path = shortestCCPathToRoot(leak); 178 let indent = 0; 179 // Reverse the path to print from the root down to the leaked object 180 for (const edge of path.reverse()) { 181 logTracker( 182 " ".repeat(indent) + edge.from.name + " :: " + edge.name + " -> \n" 183 ); 184 indent++; 185 } 186 } 187 188 /** 189 * Return a meaningful description for a given ubi::Node. 190 * 191 * @param {object} snapshot 192 * Memory API snapshot object. 193 * @param {string} id 194 * Memory API ubi::Node's id 195 * @param {number} prefix 196 * Current indentation in the description, if called recursively. 197 * @returns {string} 198 */ 199 function getNodeObjectDescription(snapshot, id, prefix = 0) { 200 prefix = " ".repeat(prefix); 201 if (!id) { 202 return prefix + "<null>"; 203 } 204 try { 205 let stack = [...snapshot.describeNode({ by: "allocationStack" }, id)]; 206 if (stack) { 207 stack = stack.find(([src]) => src != "noStack"); 208 if (stack) { 209 const { line, column, source } = stack[0]; 210 if (source) { 211 const lines = getFileContent(source); 212 const lineBefore = lines[line - 2]; 213 const lineText = lines[line - 1]; 214 const lineAfter = lines[line]; 215 const filename = source.substr(source.lastIndexOf("/") + 1); 216 217 stack = "allocated at " + source + ":\n"; 218 // Print one line before and after for context 219 if (lineBefore.trim().length) { 220 stack += prefix + ` ${filename} @ ${line - 1} \u007C`; 221 stack += "\x1b[2m" + lineBefore + "\n"; 222 } 223 stack += prefix + ` ${filename} @ ${line} > \u007C`; 224 // Grey out the beginning of the line, before frame's column, 225 // and display an arrow before displaying the rest of the line. 226 stack += 227 "\x1b[2m" + 228 lineText.substr(0, column - 1) + 229 "\x1b[0m" + 230 "\u21A6 " + 231 lineText.substr(column - 1) + 232 "\n"; 233 if (lineAfter.trim().length) { 234 stack += prefix + ` ${filename} @ ${line + 1} \u007C`; 235 stack += lineAfter; 236 } 237 } else { 238 stack = "(missing source)"; 239 } 240 } else { 241 stack = "(without allocation stack)"; 242 } 243 } else { 244 stack = "(without description)"; 245 } 246 let objectClass = Object.entries( 247 snapshot.describeNode({ by: "objectClass" }, id) 248 )[0][0]; 249 if (objectClass == "other") { 250 objectClass = Object.entries( 251 snapshot.describeNode({ by: "internalType" }, id) 252 )[0][0]; 253 } 254 const arrow = prefix > 0 ? "\\--> " : ""; 255 return prefix + arrow + objectClass + " " + stack; 256 } catch (e) { 257 if (e.name == "NS_ERROR_ILLEGAL_VALUE") { 258 return prefix + "<not-in-memory-snapshot:is-from-untracked-global?>"; 259 } 260 return prefix + "<invalid:" + id + ":" + e + ">"; 261 } 262 } 263 264 /** 265 * Print the shortest retaining path between two ubi::Node objects. 266 * 267 * @param {object} snapshot 268 * Memory API snapshot object. 269 * @param {string} src 270 * Memory API ubi::Node's id 271 * @param {string} dst 272 * Memory API ubi::Node's id 273 */ 274 function printShortestNodePath(snapshot, src, dst) { 275 let paths; 276 try { 277 paths = snapshot.computeShortestPaths(src, [dst], 10); 278 } catch (e) {} 279 if (paths && paths.has(dst)) { 280 let pathLength = Infinity; 281 let n = 0; 282 for (const path of paths.get(dst)) { 283 n++; 284 // Only print the smaller paths. 285 // The longer ones will only repeat the smaller ones, with some extra edges. 286 if (path.length > pathLength + 1) { 287 continue; 288 } 289 pathLength = path.length; 290 logTracker( 291 `Path #${n}:\n` + 292 path 293 .map(({ predecessor, edge }, i) => { 294 return ( 295 getNodeObjectDescription(snapshot, predecessor, i) + 296 "\n" + 297 " ".repeat(i) + 298 "Holds the following object via '" + 299 edge + 300 "' attribute:\n" 301 ); 302 }) 303 .join("") + 304 getNodeObjectDescription(snapshot, dst, path.length) 305 ); 306 } 307 } else { 308 logTracker("NO-PATH"); 309 } 310 } 311 312 const fileContents = new Map(); 313 314 function getFileContent(url) { 315 let content = fileContents.get(url); 316 if (content) { 317 return content; 318 } 319 content = readURI(url).split("\n"); 320 fileContents.set(url, content); 321 return content; 322 } 323 324 function readURI(uri) { 325 const { NetUtil } = ChromeUtils.importESModule( 326 "resource://gre/modules/NetUtil.sys.mjs", 327 { global: "contextual" } 328 ); 329 const stream = NetUtil.newChannel({ 330 uri: NetUtil.newURI(uri, "UTF-8"), 331 loadUsingSystemPrincipal: true, 332 }).open(); 333 const count = stream.available(); 334 const data = NetUtil.readInputStreamToString(stream, count, { 335 charset: "UTF-8", 336 }); 337 338 stream.close(); 339 return data; 340 } 341 342 function logTracker(message) { 343 // Use special characters to grey out [TRACKER] and make allocation logs stands out. 344 dump(` \x1b[2m[TRACKER]\x1b[0m ${message}\n`); 345 }