tor-browser

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

head_heapsnapshot.js (16543B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 /* exported Cr, CC, Match, Census, Task, DevToolsUtils, HeapAnalysesClient,
      6  assertThrows, getFilePath, saveHeapSnapshotAndTakeCensus,
      7  saveHeapSnapshotAndComputeDominatorTree, compareCensusViewData, assertDiff,
      8  assertLabelAndShallowSize, makeTestDominatorTreeNode,
      9  assertDominatorTreeNodeInsertion, assertDeduplicatedPaths,
     10  assertCountToBucketBreakdown, pathEntry */
     11 
     12 var CC = Components.Constructor;
     13 
     14 const { require } = ChromeUtils.importESModule(
     15  "resource://devtools/shared/loader/Loader.sys.mjs"
     16 );
     17 const { Match } = ChromeUtils.importESModule("resource://test/Match.sys.mjs");
     18 const { Census } = ChromeUtils.importESModule("resource://test/Census.sys.mjs");
     19 const { addDebuggerToGlobal } = ChromeUtils.importESModule(
     20  "resource://gre/modules/jsdebugger.sys.mjs"
     21 );
     22 
     23 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
     24 const HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js");
     25 const {
     26  censusReportToCensusTreeNode,
     27 } = require("resource://devtools/shared/heapsnapshot/census-tree-node.js");
     28 const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
     29 const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js");
     30 const {
     31  deduplicatePaths,
     32 } = require("resource://devtools/shared/heapsnapshot/shortest-paths.js");
     33 const { LabelAndShallowSizeVisitor } = DominatorTreeNode;
     34 
     35 // Always log packets when running tests. runxpcshelltests.py will throw
     36 // the output away anyway, unless you give it the --verbose flag.
     37 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
     38  Services.prefs.setBoolPref("devtools.debugger.log", true);
     39  Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
     40  registerCleanupFunction(() => {
     41    Services.prefs.clearUserPref("devtools.debugger.log");
     42    Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
     43  });
     44 }
     45 
     46 const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance(
     47  Ci.nsIPrincipal
     48 );
     49 
     50 function dumpn(msg) {
     51  dump("HEAPSNAPSHOT-TEST: " + msg + "\n");
     52 }
     53 
     54 function addTestingFunctionsToGlobal(global) {
     55  global.eval(
     56    `
     57    const testingFunctions = Components.utils.getJSTestingFunctions();
     58    for (let k in testingFunctions) {
     59      this[k] = testingFunctions[k];
     60    }
     61    `
     62  );
     63  if (!global.print) {
     64    global.print = info;
     65  }
     66  if (!global.newGlobal) {
     67    global.newGlobal = newGlobal;
     68  }
     69  if (!global.Debugger) {
     70    addDebuggerToGlobal(global);
     71  }
     72 }
     73 
     74 addTestingFunctionsToGlobal(this);
     75 
     76 /**
     77 * Create a new global, with all the JS shell testing functions. Similar to the
     78 * newGlobal function exposed to JS shells, and useful for porting JS shell
     79 * tests to xpcshell tests.
     80 */
     81 function newGlobal() {
     82  const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
     83  addTestingFunctionsToGlobal(global);
     84  return global;
     85 }
     86 
     87 function assertThrows(f, val, msg) {
     88  let fullmsg;
     89  try {
     90    f();
     91  } catch (exc) {
     92    if (exc === val && (val !== 0 || 1 / exc === 1 / val)) {
     93      return;
     94    } else if (exc instanceof Error && exc.message === val) {
     95      return;
     96    }
     97    fullmsg = "Assertion failed: expected exception " + val + ", got " + exc;
     98  }
     99  if (fullmsg === undefined) {
    100    fullmsg =
    101      "Assertion failed: expected exception " + val + ", no exception thrown";
    102  }
    103  if (msg !== undefined) {
    104    fullmsg += " - " + msg;
    105  }
    106  throw new Error(fullmsg);
    107 }
    108 
    109 /**
    110 * Returns the full path of the file with the specified name in a
    111 * platform-independent and URL-like form.
    112 */
    113 function getFilePath(
    114  name,
    115  allowMissing = false,
    116  usePlatformPathSeparator = false
    117 ) {
    118  const file = do_get_file(name, allowMissing);
    119  let path = Services.io.newFileURI(file).spec;
    120  let filePrePath = "file://";
    121  if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) {
    122    filePrePath += "/";
    123  }
    124 
    125  path = path.slice(filePrePath.length);
    126 
    127  if (usePlatformPathSeparator && path.match(/^\w:/)) {
    128    path = path.replace(/\//g, "\\");
    129  }
    130 
    131  return path;
    132 }
    133 
    134 function saveNewHeapSnapshot(opts = { runtime: true }) {
    135  const filePath = ChromeUtils.saveHeapSnapshot(opts);
    136  ok(filePath, "Should get a file path to save the core dump to.");
    137  ok(true, "Saved a heap snapshot to " + filePath);
    138  return filePath;
    139 }
    140 
    141 function readHeapSnapshot(filePath) {
    142  const snapshot = ChromeUtils.readHeapSnapshot(filePath);
    143  ok(snapshot, "Should have read a heap snapshot back from " + filePath);
    144  ok(
    145    HeapSnapshot.isInstance(snapshot),
    146    "snapshot should be an instance of HeapSnapshot"
    147  );
    148  return snapshot;
    149 }
    150 
    151 /**
    152 * Save a heap snapshot to the file with the given name in the current
    153 * directory, read it back as a HeapSnapshot instance, and then take a census of
    154 * the heap snapshot's serialized heap graph with the provided census options.
    155 *
    156 * @param {object | undefined} censusOptions
    157 *        Options that should be passed through to the takeCensus method. See
    158 *        js/src/doc/Debugger/Debugger.Memory.md for details.
    159 *
    160 * @param {Debugger|null} dbg
    161 *        If a Debugger object is given, only serialize the subgraph covered by
    162 *        the Debugger's debuggees. If null, serialize the whole heap graph.
    163 *
    164 * @param {string} fileName
    165 *        The file name to save the heap snapshot's core dump file to, within
    166 *        the current directory.
    167 *
    168 * @returns Census
    169 */
    170 function saveHeapSnapshotAndTakeCensus(dbg = null, censusOptions = undefined) {
    171  const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
    172  const filePath = saveNewHeapSnapshot(snapshotOptions);
    173  const snapshot = readHeapSnapshot(filePath);
    174 
    175  equal(
    176    typeof snapshot.takeCensus,
    177    "function",
    178    "snapshot should have a takeCensus method"
    179  );
    180 
    181  return snapshot.takeCensus(censusOptions);
    182 }
    183 
    184 /**
    185 * Save a heap snapshot to disk, read it back as a HeapSnapshot instance, and
    186 * then compute its dominator tree.
    187 *
    188 * @param {Debugger|null} dbg
    189 *        If a Debugger object is given, only serialize the subgraph covered by
    190 *        the Debugger's debuggees. If null, serialize the whole heap graph.
    191 *
    192 * @returns {DominatorTree}
    193 */
    194 function saveHeapSnapshotAndComputeDominatorTree(dbg = null) {
    195  const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
    196  const filePath = saveNewHeapSnapshot(snapshotOptions);
    197  const snapshot = readHeapSnapshot(filePath);
    198 
    199  equal(
    200    typeof snapshot.computeDominatorTree,
    201    "function",
    202    "snapshot should have a `computeDominatorTree` method"
    203  );
    204 
    205  const dominatorTree = snapshot.computeDominatorTree();
    206 
    207  ok(dominatorTree, "Should be able to compute a dominator tree");
    208  ok(
    209    DominatorTree.isInstance(dominatorTree),
    210    "Should be an instance of DominatorTree"
    211  );
    212 
    213  return dominatorTree;
    214 }
    215 
    216 function isSavedFrame(obj) {
    217  return Object.prototype.toString.call(obj) === "[object SavedFrame]";
    218 }
    219 
    220 function savedFrameReplacer(key, val) {
    221  if (isSavedFrame(val)) {
    222    return `<SavedFrame '${val.toString().split(/\n/g).shift()}'>`;
    223  }
    224  return val;
    225 }
    226 
    227 /**
    228 * Assert that creating a CensusTreeNode from the given `report` with the
    229 * specified `breakdown` creates the given `expected` CensusTreeNode.
    230 *
    231 * @param {object} breakdown
    232 *        The census breakdown.
    233 *
    234 * @param {object} report
    235 *        The census report.
    236 *
    237 * @param {object} expected
    238 *        The expected CensusTreeNode result.
    239 *
    240 * @param {object} options
    241 *        The options to pass through to `censusReportToCensusTreeNode`.
    242 */
    243 function compareCensusViewData(breakdown, report, expected, options) {
    244  dumpn("Generating CensusTreeNode from report:");
    245  dumpn("breakdown: " + JSON.stringify(breakdown, null, 4));
    246  dumpn("report: " + JSON.stringify(report, null, 4));
    247  dumpn("expected: " + JSON.stringify(expected, savedFrameReplacer, 4));
    248 
    249  const actual = censusReportToCensusTreeNode(breakdown, report, options);
    250  dumpn("actual: " + JSON.stringify(actual, savedFrameReplacer, 4));
    251 
    252  assertStructurallyEquivalent(actual, expected);
    253 }
    254 
    255 // Deep structural equivalence that can handle Map objects in addition to plain
    256 // objects.
    257 function assertStructurallyEquivalent(actual, expected, path = "root") {
    258  if (actual === expected) {
    259    equal(actual, expected, "actual and expected are the same");
    260    return;
    261  }
    262 
    263  equal(typeof actual, typeof expected, `${path}: typeof should be the same`);
    264 
    265  if (actual && typeof actual === "object") {
    266    const actualProtoString = Object.prototype.toString.call(actual);
    267    const expectedProtoString = Object.prototype.toString.call(expected);
    268    equal(
    269      actualProtoString,
    270      expectedProtoString,
    271      `${path}: Object.prototype.toString.call() should be the same`
    272    );
    273 
    274    if (actualProtoString === "[object Map]") {
    275      const expectedKeys = new Set([...expected.keys()]);
    276 
    277      for (const key of actual.keys()) {
    278        ok(
    279          expectedKeys.has(key),
    280          `${path}: every key in actual is expected: ${String(key).slice(
    281            0,
    282            10
    283          )}`
    284        );
    285        expectedKeys.delete(key);
    286 
    287        assertStructurallyEquivalent(
    288          actual.get(key),
    289          expected.get(key),
    290          path + ".get(" + String(key).slice(0, 20) + ")"
    291        );
    292      }
    293 
    294      equal(
    295        expectedKeys.size,
    296        0,
    297        `${path}: every key in expected should also exist in actual,\
    298        did not see ${[...expectedKeys]}`
    299      );
    300    } else if (actualProtoString === "[object Set]") {
    301      const expectedItems = new Set([...expected]);
    302 
    303      for (const item of actual) {
    304        ok(
    305          expectedItems.has(item),
    306          `${path}: every set item in actual should exist in expected: ${item}`
    307        );
    308        expectedItems.delete(item);
    309      }
    310 
    311      equal(
    312        expectedItems.size,
    313        0,
    314        `${path}: every set item in expected should also exist in actual,\
    315        did not see ${[...expectedItems]}`
    316      );
    317    } else {
    318      const expectedKeys = new Set(Object.keys(expected));
    319 
    320      for (const key of Object.keys(actual)) {
    321        ok(
    322          expectedKeys.has(key),
    323          `${path}: every key in actual should exist in expected: ${key}`
    324        );
    325        expectedKeys.delete(key);
    326 
    327        assertStructurallyEquivalent(
    328          actual[key],
    329          expected[key],
    330          path + "." + key
    331        );
    332      }
    333 
    334      equal(
    335        expectedKeys.size,
    336        0,
    337        `${path}: every key in expected should also exist in actual,\
    338        did not see ${[...expectedKeys]}`
    339      );
    340    }
    341  } else {
    342    equal(actual, expected, `${path}: primitives should be equal`);
    343  }
    344 }
    345 
    346 /**
    347 * Assert that creating a diff of the `first` and `second` census reports
    348 * creates the `expected` delta-report.
    349 *
    350 * @param {object} breakdown
    351 *        The census breakdown.
    352 *
    353 * @param {object} first
    354 *        The first census report.
    355 *
    356 * @param {object} second
    357 *        The second census report.
    358 *
    359 * @param {object} expected
    360 *        The expected delta-report.
    361 */
    362 function assertDiff(breakdown, first, second, expected) {
    363  dumpn("Diffing census reports:");
    364  dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
    365  dumpn("First census report: " + JSON.stringify(first, null, 4));
    366  dumpn("Second census report: " + JSON.stringify(second, null, 4));
    367  dumpn("Expected delta-report: " + JSON.stringify(expected, null, 4));
    368 
    369  const actual = CensusUtils.diff(breakdown, first, second);
    370  dumpn("Actual delta-report: " + JSON.stringify(actual, null, 4));
    371 
    372  assertStructurallyEquivalent(actual, expected);
    373 }
    374 
    375 /**
    376 * Assert that creating a label and getting a shallow size from the given node
    377 * description with the specified breakdown is as expected.
    378 *
    379 * @param {object} breakdown
    380 * @param {object} givenDescription
    381 * @param {number} expectedShallowSize
    382 * @param {object} expectedLabel
    383 */
    384 function assertLabelAndShallowSize(
    385  breakdown,
    386  givenDescription,
    387  expectedShallowSize,
    388  expectedLabel
    389 ) {
    390  dumpn("Computing label and shallow size from node description:");
    391  dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
    392  dumpn("Given description: " + JSON.stringify(givenDescription, null, 4));
    393 
    394  const visitor = new LabelAndShallowSizeVisitor();
    395  CensusUtils.walk(breakdown, givenDescription, visitor);
    396 
    397  dumpn("Expected shallow size: " + expectedShallowSize);
    398  dumpn("Actual shallow size: " + visitor.shallowSize());
    399  equal(
    400    visitor.shallowSize(),
    401    expectedShallowSize,
    402    "Shallow size should be correct"
    403  );
    404 
    405  dumpn("Expected label: " + JSON.stringify(expectedLabel, null, 4));
    406  dumpn("Actual label: " + JSON.stringify(visitor.label(), null, 4));
    407  assertStructurallyEquivalent(visitor.label(), expectedLabel);
    408 }
    409 
    410 // Counter for mock DominatorTreeNode ids.
    411 let TEST_NODE_ID_COUNTER = 0;
    412 
    413 /**
    414 * Create a mock DominatorTreeNode for testing, with sane defaults. Override any
    415 * property by providing it on `opts`. Optionally pass child nodes as well.
    416 *
    417 * @param {object} opts
    418 * @param {Array<DominatorTreeNode>?} children
    419 *
    420 * @returns {DominatorTreeNode}
    421 */
    422 function makeTestDominatorTreeNode(opts, children) {
    423  const nodeId = TEST_NODE_ID_COUNTER++;
    424 
    425  const node = Object.assign(
    426    {
    427      nodeId,
    428      label: undefined,
    429      shallowSize: 1,
    430      retainedSize: (children || []).reduce(
    431        (size, c) => size + c.retainedSize,
    432        1
    433      ),
    434      parentId: undefined,
    435      children,
    436      moreChildrenAvailable: true,
    437    },
    438    opts
    439  );
    440 
    441  if (children && children.length) {
    442    children.map(c => (c.parentId = node.nodeId));
    443  }
    444 
    445  return node;
    446 }
    447 
    448 /**
    449 * Insert `newChildren` into the given dominator `tree` as specified by the
    450 * `path` from the root to the node the `newChildren` should be inserted
    451 * beneath. Assert that the resulting tree matches `expected`.
    452 */
    453 function assertDominatorTreeNodeInsertion(
    454  tree,
    455  path,
    456  newChildren,
    457  moreChildrenAvailable,
    458  expected
    459 ) {
    460  dumpn("Inserting new children into a dominator tree:");
    461  dumpn("Dominator tree: " + JSON.stringify(tree, null, 2));
    462  dumpn("Path: " + JSON.stringify(path, null, 2));
    463  dumpn("New children: " + JSON.stringify(newChildren, null, 2));
    464  dumpn("Expected resulting tree: " + JSON.stringify(expected, null, 2));
    465 
    466  const actual = DominatorTreeNode.insert(
    467    tree,
    468    path,
    469    newChildren,
    470    moreChildrenAvailable
    471  );
    472  dumpn("Actual resulting tree: " + JSON.stringify(actual, null, 2));
    473 
    474  assertStructurallyEquivalent(actual, expected);
    475 }
    476 
    477 function assertDeduplicatedPaths({
    478  target,
    479  paths,
    480  expectedNodes,
    481  expectedEdges,
    482 }) {
    483  dumpn("Deduplicating paths:");
    484  dumpn("target = " + target);
    485  dumpn("paths = " + JSON.stringify(paths, null, 2));
    486  dumpn("expectedNodes = " + expectedNodes);
    487  dumpn("expectedEdges = " + JSON.stringify(expectedEdges, null, 2));
    488 
    489  const { nodes, edges } = deduplicatePaths(target, paths);
    490 
    491  dumpn("Actual nodes = " + nodes);
    492  dumpn("Actual edges = " + JSON.stringify(edges, null, 2));
    493 
    494  equal(
    495    nodes.length,
    496    expectedNodes.length,
    497    "actual number of nodes is equal to the expected number of nodes"
    498  );
    499 
    500  equal(
    501    edges.length,
    502    expectedEdges.length,
    503    "actual number of edges is equal to the expected number of edges"
    504  );
    505 
    506  const expectedNodeSet = new Set(expectedNodes);
    507  const nodeSet = new Set(nodes);
    508  Assert.strictEqual(
    509    nodeSet.size,
    510    nodes.length,
    511    "each returned node should be unique"
    512  );
    513 
    514  for (const node of nodes) {
    515    ok(expectedNodeSet.has(node), `the ${node} node was expected`);
    516  }
    517 
    518  for (const expectedEdge of expectedEdges) {
    519    let count = 0;
    520    for (const edge of edges) {
    521      if (
    522        edge.from === expectedEdge.from &&
    523        edge.to === expectedEdge.to &&
    524        edge.name === expectedEdge.name
    525      ) {
    526        count++;
    527      }
    528    }
    529    equal(
    530      count,
    531      1,
    532      "should have exactly one matching edge for the expected edge = " +
    533        JSON.stringify(expectedEdge)
    534    );
    535  }
    536 }
    537 
    538 function assertCountToBucketBreakdown(breakdown, expected) {
    539  dumpn("count => bucket breakdown");
    540  dumpn("Initial breakdown = ", JSON.stringify(breakdown, null, 2));
    541  dumpn("Expected results = ", JSON.stringify(expected, null, 2));
    542 
    543  const actual = CensusUtils.countToBucketBreakdown(breakdown);
    544  dumpn("Actual results = ", JSON.stringify(actual, null, 2));
    545 
    546  assertStructurallyEquivalent(actual, expected);
    547 }
    548 
    549 /**
    550 * Create a mock path entry for the given predecessor and edge.
    551 */
    552 function pathEntry(predecessor, edge) {
    553  return { predecessor, edge };
    554 }