tor-browser

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

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 }