tor-browser

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

CensusUtils.js (13941B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 const {
      7  flatten,
      8 } = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
      9 
     10 /*** Visitor ****************************************************************/
     11 
     12 /**
     13 * A Visitor visits each node and edge of a census report tree as the census
     14 * report is being traversed by `walk`.
     15 */
     16 class Visitor {
     17  /**
     18   * The `enter` method is called when a new sub-report is entered in traversal.
     19   *
     20   * @param {object} breakdown
     21   *        The breakdown for the sub-report that is being entered by traversal.
     22   *
     23   * @param {object} report
     24   *        The report generated by the given breakdown.
     25   *
     26   * @param {any} edge
     27   *        The edge leading to this sub-report. The edge is null if (but not iff!
     28   *        eg, null allocation stack edges) we are entering the root report.
     29   */
     30  enter() {}
     31  /**
     32   * The `exit` method is called when traversal of a sub-report has finished.
     33   *
     34   * @param {object} breakdown
     35   *        The breakdown for the sub-report whose traversal has finished.
     36   *
     37   * @param {object} report
     38   *        The report generated by the given breakdown.
     39   *
     40   * @param {any} edge
     41   *        The edge leading to this sub-report. The edge is null if (but not iff!
     42   *        eg, null allocation stack edges) we are entering the root report.
     43   */
     44  exit() {}
     45  /**
     46   * The `count` method is called when leaf nodes (reports whose breakdown is
     47   * by: "count") in the report tree are encountered.
     48   *
     49   * @param {object} breakdown
     50   *        The count breakdown for this report.
     51   *
     52   * @param {object} report
     53   *        The report generated by a breakdown by "count".
     54   *
     55   * @param {any|null} edge
     56   *        The edge leading to this count report. The edge is null if we are
     57   *        entering the root report.
     58   */
     59  count() {}
     60 }
     61 exports.Visitor = Visitor;
     62 
     63 /*** getReportEdges *********************************************************/
     64 
     65 const EDGES = Object.create(null);
     66 
     67 EDGES.count = function () {
     68  return [];
     69 };
     70 
     71 EDGES.bucket = function () {
     72  return [];
     73 };
     74 
     75 EDGES.internalType = function (breakdown, report) {
     76  return Object.keys(report).map(key => ({
     77    edge: key,
     78    referent: report[key],
     79    breakdown: breakdown.then,
     80  }));
     81 };
     82 
     83 EDGES.descriptiveType = function (breakdown, report) {
     84  return Object.keys(report).map(key => ({
     85    edge: key,
     86    referent: report[key],
     87    breakdown: breakdown.then,
     88  }));
     89 };
     90 
     91 EDGES.objectClass = function (breakdown, report) {
     92  return Object.keys(report).map(key => ({
     93    edge: key,
     94    referent: report[key],
     95    breakdown: key === "other" ? breakdown.other : breakdown.then,
     96  }));
     97 };
     98 
     99 EDGES.coarseType = function (breakdown, report) {
    100  return [
    101    { edge: "objects", referent: report.objects, breakdown: breakdown.objects },
    102    { edge: "scripts", referent: report.scripts, breakdown: breakdown.scripts },
    103    { edge: "strings", referent: report.strings, breakdown: breakdown.strings },
    104    { edge: "other", referent: report.other, breakdown: breakdown.other },
    105    { edge: "domNode", referent: report.domNode, breakdown: breakdown.domNode },
    106  ];
    107 };
    108 
    109 EDGES.allocationStack = function (breakdown, report) {
    110  const edges = [];
    111  report.forEach((value, key) => {
    112    edges.push({
    113      edge: key,
    114      referent: value,
    115      breakdown: key === "noStack" ? breakdown.noStack : breakdown.then,
    116    });
    117  });
    118  return edges;
    119 };
    120 
    121 EDGES.filename = function (breakdown, report) {
    122  return Object.keys(report).map(key => ({
    123    edge: key,
    124    referent: report[key],
    125    breakdown: key === "noFilename" ? breakdown.noFilename : breakdown.then,
    126  }));
    127 };
    128 
    129 /**
    130 * Get the set of outgoing edges from `report` as specified by the given
    131 * breakdown.
    132 *
    133 * @param {object} breakdown
    134 *        The census breakdown.
    135 *
    136 * @param {object} report
    137 *        The census report.
    138 */
    139 function getReportEdges(breakdown, report) {
    140  return EDGES[breakdown.by](breakdown, report);
    141 }
    142 exports.getReportEdges = getReportEdges;
    143 
    144 /*** walk *******************************************************************/
    145 
    146 function recursiveWalk(breakdown, edge, report, visitor) {
    147  if (breakdown.by === "count") {
    148    visitor.enter(breakdown, report, edge);
    149    visitor.count(breakdown, report, edge);
    150    visitor.exit(breakdown, report, edge);
    151  } else {
    152    visitor.enter(breakdown, report, edge);
    153    for (const {
    154      edge: ed,
    155      referent,
    156      breakdown: subBreakdown,
    157    } of getReportEdges(breakdown, report)) {
    158      recursiveWalk(subBreakdown, ed, referent, visitor);
    159    }
    160    visitor.exit(breakdown, report, edge);
    161  }
    162 }
    163 
    164 /**
    165 * Walk the given `report` that was generated by taking a census with the
    166 * specified `breakdown`.
    167 *
    168 * @param {object} breakdown
    169 *        The census breakdown.
    170 *
    171 * @param {object} report
    172 *        The census report.
    173 *
    174 * @param {Visitor} visitor
    175 *        The Visitor instance to call into while traversing.
    176 */
    177 function walk(breakdown, report, visitor) {
    178  recursiveWalk(breakdown, null, report, visitor);
    179 }
    180 exports.walk = walk;
    181 
    182 /*** diff *******************************************************************/
    183 
    184 /**
    185 * Return true if the object is a Map, false otherwise. Works with Map objects
    186 * from other globals, unlike `instanceof`.
    187 *
    188 * @returns {boolean}
    189 */
    190 function isMap(obj) {
    191  return Object.prototype.toString.call(obj) === "[object Map]";
    192 }
    193 
    194 const basisTotalBytes = (exports.basisTotalBytes = Symbol("basisTotalBytes"));
    195 const basisTotalCount = (exports.basisTotalCount = Symbol("basisTotalCount"));
    196 
    197 /**
    198 * A Visitor for computing the difference between the census report being
    199 * traversed and the given other census.
    200 */
    201 class DiffVisitor extends Visitor {
    202  /**
    203   * @param {object} otherCensus
    204   *        The other census report.
    205   */
    206  constructor(otherCensus) {
    207    super();
    208 
    209    // The other census we are comparing against.
    210    this._otherCensus = otherCensus;
    211 
    212    // The total bytes and count of the basis census we are traversing.
    213    this._totalBytes = 0;
    214    this._totalCount = 0;
    215 
    216    // Stack maintaining the current corresponding sub-report for the other
    217    // census we are comparing against.
    218    this._otherCensusStack = [];
    219 
    220    // Stack maintaining the set of edges visited at each sub-report.
    221    this._edgesVisited = [new Set()];
    222 
    223    // The final delta census. Valid only after traversal.
    224    this._results = null;
    225 
    226    // Stack maintaining the results corresponding to each sub-report we are
    227    // currently traversing.
    228    this._resultsStack = [];
    229  }
    230  /**
    231   * Given a report and an outgoing edge, get the edge's referent.
    232   */
    233  _get(report, edge) {
    234    if (!report) {
    235      return undefined;
    236    }
    237    return isMap(report) ? report.get(edge) : report[edge];
    238  }
    239  /**
    240   * Given a report, an outgoing edge, and a value, set the edge's referent to
    241   * the given value.
    242   */
    243  _set(report, edge, val) {
    244    if (isMap(report)) {
    245      report.set(edge, val);
    246    } else {
    247      report[edge] = val;
    248    }
    249  }
    250  /**
    251   * @override
    252   */
    253  enter(breakdown, report, edge) {
    254    const newResults = breakdown.by === "allocationStack" ? new Map() : {};
    255    let newOther;
    256 
    257    if (!this._results) {
    258      // This is the first time we have entered a sub-report.
    259      this._results = newResults;
    260      newOther = this._otherCensus;
    261    } else {
    262      const topResults = this._resultsStack[this._resultsStack.length - 1];
    263      this._set(topResults, edge, newResults);
    264 
    265      const topOther =
    266        this._otherCensusStack[this._otherCensusStack.length - 1];
    267      newOther = this._get(topOther, edge);
    268    }
    269 
    270    this._resultsStack.push(newResults);
    271    this._otherCensusStack.push(newOther);
    272 
    273    const visited = this._edgesVisited[this._edgesVisited.length - 1];
    274    visited.add(edge);
    275    this._edgesVisited.push(new Set());
    276  }
    277  /**
    278   * @override
    279   */
    280  exit(breakdown) {
    281    // Find all the edges in the other census report that were not traversed and
    282    // add them to the results directly.
    283    const other = this._otherCensusStack[this._otherCensusStack.length - 1];
    284    if (other) {
    285      const visited = this._edgesVisited[this._edgesVisited.length - 1];
    286      const unvisited = getReportEdges(breakdown, other)
    287        .map(e => e.edge)
    288        .filter(e => !visited.has(e));
    289      const results = this._resultsStack[this._resultsStack.length - 1];
    290      for (const edg of unvisited) {
    291        this._set(results, edg, this._get(other, edg));
    292      }
    293    }
    294 
    295    this._otherCensusStack.pop();
    296    this._resultsStack.pop();
    297    this._edgesVisited.pop();
    298  }
    299  /**
    300   * @override
    301   */
    302  count(breakdown, report) {
    303    const other = this._otherCensusStack[this._otherCensusStack.length - 1];
    304    const results = this._resultsStack[this._resultsStack.length - 1];
    305 
    306    if (breakdown.count) {
    307      this._totalCount += report.count;
    308    }
    309    if (breakdown.bytes) {
    310      this._totalBytes += report.bytes;
    311    }
    312 
    313    if (other) {
    314      if (breakdown.count) {
    315        results.count = other.count - report.count;
    316      }
    317      if (breakdown.bytes) {
    318        results.bytes = other.bytes - report.bytes;
    319      }
    320    } else {
    321      if (breakdown.count) {
    322        results.count = -report.count;
    323      }
    324      if (breakdown.bytes) {
    325        results.bytes = -report.bytes;
    326      }
    327    }
    328  }
    329 
    330  /**
    331   * Get the resulting report of the difference between the traversed census
    332   * report and the other census report.
    333   *
    334   * @returns {object}
    335   *          The delta census report.
    336   */
    337  results() {
    338    if (!this._results) {
    339      throw new Error("Attempt to get results before computing diff!");
    340    }
    341 
    342    if (this._resultsStack.length) {
    343      throw new Error("Attempt to get results while still computing diff!");
    344    }
    345 
    346    this._results[basisTotalBytes] = this._totalBytes;
    347    this._results[basisTotalCount] = this._totalCount;
    348 
    349    return this._results;
    350  }
    351 }
    352 
    353 /**
    354 * Take the difference between two censuses. The resulting delta report
    355 * contains the number/size of things that are in the `endCensus` that are not
    356 * in the `startCensus`.
    357 *
    358 * @param {object} breakdown
    359 *        The breakdown used to generate both census reports.
    360 *
    361 * @param {object} startCensus
    362 *        The first census report.
    363 *
    364 * @param {object} endCensus
    365 *        The second census report.
    366 *
    367 * @returns {object}
    368 *          A delta report mirroring the structure of the two census reports (as
    369 *          specified by the given breakdown). Has two additional properties:
    370 *            - {Number} basisTotalBytes: the total number of bytes in the start
    371 *                                        census.
    372 *            - {Number} basisTotalCount: the total count in the start census.
    373 */
    374 function diff(breakdown, startCensus, endCensus) {
    375  const visitor = new DiffVisitor(endCensus);
    376  walk(breakdown, startCensus, visitor);
    377  return visitor.results();
    378 }
    379 exports.diff = diff;
    380 
    381 /**
    382 * Creates a hash map mapping node IDs to its parent node.
    383 *
    384 * @param {CensusTreeNode} node
    385 * @param {{[key: number]: TreeNode}} aggregator
    386 *
    387 * @return {{[key: number]: TreeNode}}
    388 */
    389 const createParentMap = function (node, getId = n => n.id, aggregator = {}) {
    390  if (node.children) {
    391    for (let i = 0, length = node.children.length; i < length; i++) {
    392      const child = node.children[i];
    393      aggregator[getId(child)] = node;
    394      createParentMap(child, getId, aggregator);
    395    }
    396  }
    397  return aggregator;
    398 };
    399 exports.createParentMap = createParentMap;
    400 
    401 const BUCKET = Object.freeze({ by: "bucket" });
    402 
    403 /**
    404 * Convert a breakdown whose leaves are { by: "count" } to an identical
    405 * breakdown, except with { by: "bucket" } leaves.
    406 *
    407 * @param {object} breakdown
    408 * @returns {object}
    409 */
    410 exports.countToBucketBreakdown = function (breakdown) {
    411  if (typeof breakdown !== "object" || !breakdown) {
    412    return breakdown;
    413  }
    414 
    415  if (breakdown.by === "count") {
    416    return BUCKET;
    417  }
    418 
    419  const keys = Object.keys(breakdown);
    420  const vals = keys.reduce((vs, k) => {
    421    vs.push(exports.countToBucketBreakdown(breakdown[k]));
    422    return vs;
    423  }, []);
    424 
    425  const result = {};
    426  for (let i = 0, length = keys.length; i < length; i++) {
    427    result[keys[i]] = vals[i];
    428  }
    429 
    430  return Object.freeze(result);
    431 };
    432 
    433 /**
    434 * A Visitor for finding report leaves by their DFS index.
    435 */
    436 class GetLeavesVisitor extends Visitor {
    437  constructor(targetIndices) {
    438    super();
    439 
    440    this._index = -1;
    441    this._targetIndices = targetIndices;
    442    this._leaves = [];
    443  }
    444  /**
    445   * @override
    446   */
    447  enter(breakdown, report) {
    448    this._index++;
    449    if (this._targetIndices.has(this._index)) {
    450      this._leaves.push(report);
    451    }
    452  }
    453  /**
    454   * Get the accumulated report leaves after traversal.
    455   */
    456  leaves() {
    457    if (this._index === -1) {
    458      throw new Error("Attempt to call `leaves` before traversing report!");
    459    }
    460    return this._leaves;
    461  }
    462 }
    463 
    464 /**
    465 * Given a set of indices of leaves in a pre-order depth-first traversal of the
    466 * given census report, return the leaves.
    467 *
    468 * @param {Set<number>} indices
    469 * @param {object} breakdown
    470 * @param {object} report
    471 *
    472 * @returns {Array<object>}
    473 */
    474 exports.getReportLeaves = function (indices, breakdown, report) {
    475  const visitor = new GetLeavesVisitor(indices);
    476  walk(breakdown, report, visitor);
    477  return visitor.leaves();
    478 };
    479 
    480 /**
    481 * Get a list of the individual node IDs that belong to the census report leaves
    482 * of the given indices.
    483 *
    484 * @param {Set<number>} indices
    485 * @param {object} breakdown
    486 * @param {HeapSnapshot} snapshot
    487 *
    488 * @returns {Array<NodeId>}
    489 */
    490 exports.getCensusIndividuals = function (indices, countBreakdown, snapshot) {
    491  const bucketBreakdown = exports.countToBucketBreakdown(countBreakdown);
    492  const bucketReport = snapshot.takeCensus({ breakdown: bucketBreakdown });
    493  const buckets = exports.getReportLeaves(
    494    indices,
    495    bucketBreakdown,
    496    bucketReport
    497  );
    498  return flatten(buckets);
    499 };