tor-browser

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

census-tree-node.js (23559B)


      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 // CensusTreeNode is an intermediate representation of a census report that
      7 // exists between after a report is generated by taking a census and before the
      8 // report is rendered in the DOM. It must be dead simple to render, with no
      9 // further data processing or massaging needed before rendering DOM nodes. Our
     10 // goal is to do the census report to CensusTreeNode transformation in the
     11 // HeapAnalysesWorker, and ensure that the **only** work that the main thread
     12 // has to do is strictly DOM rendering work.
     13 
     14 const {
     15  Visitor,
     16  walk,
     17  basisTotalBytes,
     18  basisTotalCount,
     19 } = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
     20 
     21 // Monotonically increasing integer for CensusTreeNode `id`s.
     22 let censusTreeNodeIdCounter = 0;
     23 
     24 /**
     25 * Return true if the given object is a SavedFrame stack object, false otherwise.
     26 *
     27 * @param {any} obj
     28 * @returns {boolean}
     29 */
     30 function isSavedFrame(obj) {
     31  return Object.prototype.toString.call(obj) === "[object SavedFrame]";
     32 }
     33 
     34 /**
     35 * A CensusTreeNodeCache maps from SavedFrames to CensusTreeNodes. It is used when
     36 * aggregating multiple SavedFrame allocation stack keys into a tree of many
     37 * CensusTreeNodes. Each stack may share older frames, and we want to preserve
     38 * this sharing when converting to CensusTreeNode, so before creating a new
     39 * CensusTreeNode, we look for an existing one in one of our CensusTreeNodeCaches.
     40 */
     41 class CensusTreeNodeCache {
     42  /**
     43   * Create a unique string for the given SavedFrame (ignoring the frame's parent
     44   * chain) that can be used as a hash to key this frame within a CensusTreeNodeCache.
     45   *
     46   * NB: We manually hash rather than using an ES6 Map because we are purposely
     47   * ignoring the parent chain and wish to consider frames with everything the
     48   * same except their parents as the same.
     49   *
     50   * @param {SavedFrame} frame
     51   *        The SavedFrame object we would like to lookup in or insert into a
     52   *        CensusTreeNodeCache.
     53   *
     54   * @returns {string}
     55   *          The unique string that can be used as a key in a CensusTreeNodeCache.
     56   */
     57  static hashFrame(frame) {
     58    // eslint-disable-next-line max-len
     59    return `FRAME,${frame.functionDisplayName},${frame.source},${frame.line},${frame.column},${frame.asyncCause}`;
     60  }
     61 
     62  /**
     63   * Create a unique string for the given CensusTreeNode **with regards to
     64   * siblings at the current depth of the tree, not within the whole tree.** It
     65   * can be used as a hash to key this node within a CensusTreeNodeCache.
     66   *
     67   * @param {CensusTreeNode} node
     68   *        The node we would like to lookup in or insert into a cache.
     69   *
     70   * @returns {string}
     71   *          The unique string that can be used as a key in a CensusTreeNodeCache.
     72   */
     73  static hashNode(node) {
     74    return isSavedFrame(node.name)
     75      ? this.hashFrame(node.name)
     76      : `NODE,${node.name}`;
     77  }
     78 
     79  /**
     80   * Insert the given CensusTreeNodeCacheValue whose node.name is a SavedFrame
     81   * object in the given cache.
     82   *
     83   * @param {CensusTreeNodeCache} cache
     84   * @param {CensusTreeNodeCacheValue} value
     85   */
     86  static insertFrame(cache, value) {
     87    cache[this.hashFrame(value.node.name)] = value;
     88  }
     89 
     90  /**
     91   * Insert the given value in the cache.
     92   *
     93   * @param {CensusTreeNodeCache} cache
     94   * @param {CensusTreeNodeCacheValue} value
     95   */
     96  static insertNode(cache, value) {
     97    if (isSavedFrame(value.node.name)) {
     98      this.insertFrame(cache, value);
     99    } else {
    100      cache[this.hashNode(value.node)] = value;
    101    }
    102  }
    103 
    104  /**
    105   * Lookup `frame` in `cache` and return its value if it exists.
    106   *
    107   * @param {CensusTreeNodeCache} cache
    108   * @param {SavedFrame} frame
    109   *
    110   * @returns {undefined|CensusTreeNodeCacheValue}
    111   */
    112  static lookupFrame(cache, frame) {
    113    return cache[this.hashFrame(frame)];
    114  }
    115 
    116  /**
    117   * Lookup `node` in `cache` and return its value if it exists.
    118   *
    119   * @param {CensusTreeNodeCache} cache
    120   * @param {CensusTreeNode} node
    121   *
    122   * @returns {undefined|CensusTreeNodeCacheValue}
    123   */
    124  static lookupNode(cache, node) {
    125    return isSavedFrame(node.name)
    126      ? this.lookupFrame(cache, node.name)
    127      : cache[this.hashNode(node)];
    128  }
    129 }
    130 
    131 /**
    132 * The value of a single entry stored in a CensusTreeNodeCache. It is a pair of
    133 * the CensusTreeNode for this cache value, and the subsequent
    134 * CensusTreeNodeCache for this node's children.
    135 */
    136 class CensusTreeNodeCacheValue {
    137  constructor() {
    138    // The CensusTreeNode for this cache value.
    139    this.node = undefined;
    140    // The CensusTreeNodeCache for this frame's children.
    141    this.children = undefined;
    142  }
    143 }
    144 
    145 /**
    146 * Add `child` to `parent`'s set of children and store the parent ID
    147 * on the child.
    148 *
    149 * @param {CensusTreeNode} parent
    150 * @param {CensusTreeNode} child
    151 */
    152 function addChild(parent, child) {
    153  if (!parent.children) {
    154    parent.children = [];
    155  }
    156  child.parent = parent.id;
    157  parent.children.push(child);
    158 }
    159 
    160 /**
    161 * Get an array of each frame in the provided stack.
    162 *
    163 * @param {SavedFrame} stack
    164 * @returns {Array<SavedFrame>}
    165 */
    166 function getArrayOfFrames(stack) {
    167  const frames = [];
    168  let frame = stack;
    169  while (frame) {
    170    frames.push(frame);
    171    frame = frame.parent;
    172  }
    173  frames.reverse();
    174  return frames;
    175 }
    176 
    177 /**
    178 * Given an `edge` to a sub-`report` whose structure is described by
    179 * `breakdown`, create a CensusTreeNode tree.
    180 *
    181 * @param {object} breakdown
    182 *        The breakdown specifying the structure of the given report.
    183 *
    184 * @param {object} report
    185 *        The census report.
    186 *
    187 * @param {null | string | SavedFrame} edge
    188 *        The edge leading to this report from the parent report.
    189 *
    190 * @param {CensusTreeNodeCache} cache
    191 *        The cache of CensusTreeNodes we have already made for the siblings of
    192 *        the node being created. The existing nodes are reused when possible.
    193 *
    194 * @param {object} outParams
    195 *        The return values are attached to this object after this function
    196 *        returns. Because we create a CensusTreeNode for each frame in a
    197 *        SavedFrame stack edge, there may multiple nodes per sub-report.
    198 *
    199 *          - top: The deepest node in the CensusTreeNode subtree created.
    200 *
    201 *          - bottom: The shallowest node in the CensusTreeNode subtree created.
    202 *                    This is null if the shallowest node in the subtree was
    203 *                    found in the `cache` and reused.
    204 *
    205 *        Note that top and bottom are not necessarily different. In the case
    206 *        where there is a 1:1 correspondence between an edge in the report and
    207 *        a CensusTreeNode, top and bottom refer to the same node.
    208 */
    209 function makeCensusTreeNodeSubTree(breakdown, report, edge, cache, outParams) {
    210  if (!isSavedFrame(edge)) {
    211    const node = new CensusTreeNode(edge);
    212    outParams.top = outParams.bottom = node;
    213    return;
    214  }
    215 
    216  const frames = getArrayOfFrames(edge);
    217  let currentCache = cache;
    218  let prevNode;
    219  for (let i = 0, length = frames.length; i < length; i++) {
    220    const frame = frames[i];
    221 
    222    // Get or create the CensusTreeNodeCacheValue for this frame. If we already
    223    // have a CensusTreeNodeCacheValue (and hence a CensusTreeNode) for this
    224    // frame, we don't need to add the node to the previous node's children as
    225    // we have already done that. If we don't have a CensusTreeNodeCacheValue
    226    // and CensusTreeNode for this frame, then create one and make sure to hook
    227    // it up as a child of the previous node.
    228    let isNewNode = false;
    229    let val = CensusTreeNodeCache.lookupFrame(currentCache, frame);
    230    if (!val) {
    231      isNewNode = true;
    232      val = new CensusTreeNodeCacheValue();
    233      val.node = new CensusTreeNode(frame);
    234 
    235      CensusTreeNodeCache.insertFrame(currentCache, val);
    236      if (prevNode) {
    237        addChild(prevNode, val.node);
    238      }
    239    }
    240 
    241    if (i === 0) {
    242      outParams.bottom = isNewNode ? val.node : null;
    243    }
    244    if (i === length - 1) {
    245      outParams.top = val.node;
    246    }
    247 
    248    prevNode = val.node;
    249 
    250    if (i !== length - 1 && !val.children) {
    251      // This is not the last frame and therefore this node will have
    252      // children, which we must cache.
    253      val.children = new CensusTreeNodeCache();
    254    }
    255 
    256    currentCache = val.children;
    257  }
    258 }
    259 
    260 /**
    261 * A Visitor that walks a census report and creates the corresponding
    262 * CensusTreeNode tree.
    263 */
    264 class CensusTreeNodeVisitor extends Visitor {
    265  constructor() {
    266    super();
    267    // The root of the resulting CensusTreeNode tree.
    268    this._root = null;
    269 
    270    // The stack of CensusTreeNodes that we are in the process of building while
    271    // walking the census report.
    272    this._nodeStack = [];
    273 
    274    // To avoid unnecessary allocations, we reuse the same out parameter object
    275    // passed to `makeCensusTreeNodeSubTree` every time we call it.
    276    this._outParams = {
    277      top: null,
    278      bottom: null,
    279    };
    280 
    281    // The stack of `CensusTreeNodeCache`s that we use to aggregate many
    282    // SavedFrame stacks into a single CensusTreeNode tree.
    283    this._cacheStack = [new CensusTreeNodeCache()];
    284 
    285    // The current index in the DFS of the census report tree.
    286    this._index = -1;
    287  }
    288 
    289  /**
    290   * Create the CensusTreeNode subtree for this sub-report and link it to the
    291   * parent CensusTreeNode.
    292   *
    293   * @override
    294   */
    295  enter(breakdown, report, edge) {
    296    this._index++;
    297 
    298    const cache = this._cacheStack[this._cacheStack.length - 1];
    299    makeCensusTreeNodeSubTree(breakdown, report, edge, cache, this._outParams);
    300    const { top, bottom } = this._outParams;
    301 
    302    if (!this._root) {
    303      this._root = bottom;
    304    } else if (bottom) {
    305      addChild(this._nodeStack[this._nodeStack.length - 1], bottom);
    306    }
    307 
    308    this._cacheStack.push(new CensusTreeNodeCache());
    309    this._nodeStack.push(top);
    310  }
    311 
    312  /**
    313   * We have finished adding children to the CensusTreeNode subtree for the
    314   * current sub-report. Make sure that the children are sorted for every node in
    315   * the subtree.
    316   *
    317   * @override
    318   */
    319  exit() {
    320    // Ensure all children are sorted and have their counts/bytes aggregated. We
    321    // only need to consider cache children here, because other children
    322    // correspond to other sub-reports and we already fixed them up in an earlier
    323    // invocation of `exit`.
    324 
    325    function dfs(node, childrenCache) {
    326      if (childrenCache) {
    327        const childValues = values(childrenCache);
    328        for (let i = 0, length = childValues.length; i < length; i++) {
    329          dfs(childValues[i].node, childValues[i].children);
    330        }
    331      }
    332 
    333      node.totalCount = node.count;
    334      node.totalBytes = node.bytes;
    335 
    336      if (node.children) {
    337        // Prune empty leaves.
    338        node.children = node.children.filter(isNonEmpty);
    339 
    340        node.children.sort(compareByTotal);
    341 
    342        for (let i = 0, length = node.children.length; i < length; i++) {
    343          node.totalCount += node.children[i].totalCount;
    344          node.totalBytes += node.children[i].totalBytes;
    345        }
    346      }
    347    }
    348 
    349    const top = this._nodeStack.pop();
    350    const cache = this._cacheStack.pop();
    351    dfs(top, cache);
    352  }
    353 
    354  /**
    355   * @override
    356   */
    357  count(breakdown, report) {
    358    const node = this._nodeStack[this._nodeStack.length - 1];
    359    node.reportLeafIndex = this._index;
    360 
    361    if (breakdown.count) {
    362      node.count = report.count;
    363    }
    364 
    365    if (breakdown.bytes) {
    366      node.bytes = report.bytes;
    367    }
    368  }
    369 
    370  /**
    371   * Get the root of the resulting CensusTreeNode tree.
    372   *
    373   * @returns {CensusTreeNode}
    374   */
    375  root() {
    376    if (!this._root) {
    377      throw new Error(
    378        "Attempt to get the root before walking the census report!"
    379      );
    380    }
    381 
    382    if (this._nodeStack.length) {
    383      throw new Error(
    384        "Attempt to get the root while walking the census report!"
    385      );
    386    }
    387 
    388    return this._root;
    389  }
    390 }
    391 
    392 function values(cache) {
    393  return Object.keys(cache).map(k => cache[k]);
    394 }
    395 
    396 function isNonEmpty(node) {
    397  return (
    398    (node.children !== undefined && node.children.length) ||
    399    node.bytes !== 0 ||
    400    node.count !== 0
    401  );
    402 }
    403 
    404 /**
    405 * Create a single, uninitialized CensusTreeNode.
    406 */
    407 class CensusTreeNode {
    408  /**
    409   *
    410   * @param {null | string | SavedFrame} name
    411   */
    412  constructor(name) {
    413    // Display name for this CensusTreeNode. Either null, a string, or a
    414    // SavedFrame.
    415    this.name = name;
    416 
    417    // The number of bytes occupied by matching things in the heap snapshot.
    418    this.bytes = 0;
    419 
    420    // The sum of `this.bytes` and `child.totalBytes` for each child in
    421    // `this.children`.
    422    this.totalBytes = 0;
    423 
    424    // The number of things in the heap snapshot that match this node in the
    425    // census tree.
    426    this.count = 0;
    427 
    428    // The sum of `this.count` and `child.totalCount` for each child in
    429    // `this.children`.
    430    this.totalCount = 0;
    431 
    432    // An array of this node's children, or undefined if it has no children.
    433    this.children = undefined;
    434 
    435    // The unique ID of this node.
    436    this.id = ++censusTreeNodeIdCounter;
    437 
    438    // If present, the unique ID of this node's parent. If this node does not have
    439    // a parent, then undefined.
    440    this.parent = undefined;
    441 
    442    // The `reportLeafIndex` property allows mapping a CensusTreeNode node back to
    443    // a leaf in the census report it was generated from. It is always one of the
    444    // following variants:
    445    //
    446    // * A `Number` index pointing a leaf report in a pre-order DFS traversal of
    447    //   this CensusTreeNode's census report.
    448    //
    449    // * A `Set` object containing such indices, when this is part of an inverted
    450    //   CensusTreeNode tree and multiple leaves in the report map onto this node.
    451    //
    452    // * Finally, `undefined` when no leaves in the census report correspond with
    453    //   this node.
    454    //
    455    // The first and third cases are the common cases. The second case is rather
    456    // uncommon, and to avoid doubling the number of allocations when creating
    457    // CensusTreeNode trees, and objects that get structured cloned when sending
    458    // such trees from the HeapAnalysesWorker to the main thread, we only allocate
    459    // a Set object once a node actually does have multiple leaves it corresponds
    460    // to.
    461    this.reportLeafIndex = undefined;
    462  }
    463 }
    464 
    465 /**
    466 * Compare the given nodes by their `totalBytes` properties, and breaking ties
    467 * with the `totalCount`, `bytes`, and `count` properties (in that order).
    468 *
    469 * @param {CensusTreeNode} node1
    470 * @param {CensusTreeNode} node2
    471 *
    472 * @returns {number}
    473 *          A number suitable for using with Array.prototype.sort.
    474 */
    475 function compareByTotal(node1, node2) {
    476  return (
    477    Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes) ||
    478    Math.abs(node2.totalCount) - Math.abs(node1.totalCount) ||
    479    Math.abs(node2.bytes) - Math.abs(node1.bytes) ||
    480    Math.abs(node2.count) - Math.abs(node1.count)
    481  );
    482 }
    483 
    484 /**
    485 * Compare the given nodes by their `bytes` properties, and breaking ties with
    486 * the `count`, `totalBytes`, and `totalCount` properties (in that order).
    487 *
    488 * @param {CensusTreeNode} node1
    489 * @param {CensusTreeNode} node2
    490 *
    491 * @returns {number}
    492 *          A number suitable for using with Array.prototype.sort.
    493 */
    494 function compareBySelf(node1, node2) {
    495  return (
    496    Math.abs(node2.bytes) - Math.abs(node1.bytes) ||
    497    Math.abs(node2.count) - Math.abs(node1.count) ||
    498    Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes) ||
    499    Math.abs(node2.totalCount) - Math.abs(node1.totalCount)
    500  );
    501 }
    502 
    503 /**
    504 * Given a parent cache value from a tree we are building and a child node from
    505 * a tree we are basing the new tree off of, if we already have a corresponding
    506 * node in the parent's children cache, merge this node's counts with
    507 * it. Otherwise, create the corresponding node, add it to the parent's children
    508 * cache, and create the parent->child edge.
    509 *
    510 * @param {CensusTreeNodeCacheValue} parentCachevalue
    511 * @param {CensusTreeNode} node
    512 *
    513 * @returns {CensusTreeNodeCacheValue}
    514 *          The new or extant child node's corresponding cache value.
    515 */
    516 function insertOrMergeNode(parentCacheValue, node) {
    517  if (!parentCacheValue.children) {
    518    parentCacheValue.children = new CensusTreeNodeCache();
    519  }
    520 
    521  let val = CensusTreeNodeCache.lookupNode(parentCacheValue.children, node);
    522 
    523  if (val) {
    524    // When inverting, it is possible that multiple leaves in the census report
    525    // get merged into a single CensusTreeNode node. When this occurs, switch
    526    // from a single index to a set of indices.
    527    if (
    528      val.node.reportLeafIndex !== undefined &&
    529      val.node.reportLeafIndex !== node.reportLeafIndex
    530    ) {
    531      if (typeof val.node.reportLeafIndex === "number") {
    532        const oldIndex = val.node.reportLeafIndex;
    533        val.node.reportLeafIndex = new Set();
    534        val.node.reportLeafIndex.add(oldIndex);
    535        val.node.reportLeafIndex.add(node.reportLeafIndex);
    536      } else {
    537        val.node.reportLeafIndex.add(node.reportLeafIndex);
    538      }
    539    }
    540 
    541    val.node.count += node.count;
    542    val.node.bytes += node.bytes;
    543  } else {
    544    val = new CensusTreeNodeCacheValue();
    545 
    546    val.node = new CensusTreeNode(node.name);
    547    val.node.reportLeafIndex = node.reportLeafIndex;
    548    val.node.count = node.count;
    549    val.node.totalCount = node.totalCount;
    550    val.node.bytes = node.bytes;
    551    val.node.totalBytes = node.totalBytes;
    552 
    553    addChild(parentCacheValue.node, val.node);
    554    CensusTreeNodeCache.insertNode(parentCacheValue.children, val);
    555  }
    556 
    557  return val;
    558 }
    559 
    560 /**
    561 * Given an un-inverted CensusTreeNode tree, return the corresponding inverted
    562 * CensusTreeNode tree. The input tree is not modified. The resulting inverted
    563 * tree is sorted by self bytes rather than by total bytes.
    564 *
    565 * @param {CensusTreeNode} tree
    566 *        The un-inverted tree.
    567 *
    568 * @returns {CensusTreeNode}
    569 *          The corresponding inverted tree.
    570 */
    571 function invert(tree) {
    572  const inverted = new CensusTreeNodeCacheValue();
    573  inverted.node = new CensusTreeNode(null);
    574 
    575  // Do a depth-first search of the un-inverted tree. As we reach each leaf,
    576  // take the path from the old root to the leaf, reverse that path, and add it
    577  // to the new, inverted tree's root.
    578 
    579  const path = [];
    580  (function addInvertedPaths(node) {
    581    path.push(node);
    582 
    583    if (node.children) {
    584      for (let i = 0, length = node.children.length; i < length; i++) {
    585        addInvertedPaths(node.children[i]);
    586      }
    587    } else {
    588      // We found a leaf node, add the reverse path to the inverted tree.
    589      let currentCacheValue = inverted;
    590      for (let i = path.length - 1; i >= 0; i--) {
    591        currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
    592      }
    593    }
    594 
    595    path.pop();
    596  })(tree);
    597 
    598  // Ensure that the root node always has the totals.
    599  inverted.node.totalBytes = tree.totalBytes;
    600  inverted.node.totalCount = tree.totalCount;
    601 
    602  return inverted.node;
    603 }
    604 
    605 /**
    606 * Given a CensusTreeNode tree and predicate function, create the tree
    607 * containing only the nodes in any path `(node_0, node_1, ..., node_n-1)` in
    608 * the given tree where `predicate(node_j)` is true for `0 <= j < n`, `node_0`
    609 * is the given tree's root, and `node_n-1` is a leaf in the given tree. The
    610 * given tree is left unmodified.
    611 *
    612 * @param {CensusTreeNode} tree
    613 * @param {Function} predicate
    614 *
    615 * @returns {CensusTreeNode}
    616 */
    617 function filter(tree, predicate) {
    618  const filtered = new CensusTreeNodeCacheValue();
    619  filtered.node = new CensusTreeNode(null);
    620 
    621  // Do a DFS over the given tree. If the predicate returns true for any node,
    622  // add that node and its whole subtree to the filtered tree.
    623 
    624  const path = [];
    625  let match = false;
    626 
    627  function addMatchingNodes(node) {
    628    path.push(node);
    629 
    630    const oldMatch = match;
    631    if (!match && predicate(node)) {
    632      match = true;
    633    }
    634 
    635    if (node.children) {
    636      for (let i = 0, length = node.children.length; i < length; i++) {
    637        addMatchingNodes(node.children[i]);
    638      }
    639    } else if (match) {
    640      // We found a matching leaf node, add it to the filtered tree.
    641      let currentCacheValue = filtered;
    642      for (let i = 0, length = path.length; i < length; i++) {
    643        currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
    644      }
    645    }
    646 
    647    match = oldMatch;
    648    path.pop();
    649  }
    650 
    651  if (tree.children) {
    652    for (let i = 0, length = tree.children.length; i < length; i++) {
    653      addMatchingNodes(tree.children[i]);
    654    }
    655  }
    656 
    657  filtered.node.count = tree.count;
    658  filtered.node.totalCount = tree.totalCount;
    659  filtered.node.bytes = tree.bytes;
    660  filtered.node.totalBytes = tree.totalBytes;
    661 
    662  return filtered.node;
    663 }
    664 
    665 /**
    666 * Given a filter string, return a predicate function that takes a node and
    667 * returns true iff the node matches the filter.
    668 *
    669 * @param {string} filterString
    670 * @returns {Function}
    671 */
    672 function makeFilterPredicate(filterString) {
    673  return function (node) {
    674    if (!node.name) {
    675      return false;
    676    }
    677 
    678    if (isSavedFrame(node.name)) {
    679      return (
    680        node.name.source.includes(filterString) ||
    681        (node.name.functionDisplayName || "").includes(filterString) ||
    682        (node.name.asyncCause || "").includes(filterString)
    683      );
    684    }
    685 
    686    return String(node.name).includes(filterString);
    687  };
    688 }
    689 
    690 /**
    691 * Takes a report from a census (`dbg.memory.takeCensus()`) and the breakdown
    692 * used to generate the census and returns a structure used to render
    693 * a tree to display the data.
    694 *
    695 * Returns a recursive "CensusTreeNode" object, looking like:
    696 *
    697 * CensusTreeNode = {
    698 *   // `children` if it exists, is sorted by `bytes`, if they are leaf nodes.
    699 *   children: ?[<CensusTreeNode...>],
    700 *   name: <?String>
    701 *   count: <?Number>
    702 *   bytes: <?Number>
    703 *   id: <?Number>
    704 *   parent: <?Number>
    705 * }
    706 *
    707 * @param {object} breakdown
    708 *        The breakdown used to generate the census report.
    709 *
    710 * @param {object} report
    711 *        The census report generated with the specified breakdown.
    712 *
    713 * @param {object} options
    714 *        Configuration options.
    715 *          - invert: Whether to invert the resulting tree or not. Defaults to
    716 *                    false, ie uninverted.
    717 *
    718 * @returns {CensusTreeNode}
    719 */
    720 exports.censusReportToCensusTreeNode = function (
    721  breakdown,
    722  report,
    723  options = {
    724    invert: false,
    725    filter: null,
    726  }
    727 ) {
    728  // Reset the counter so that turning the same census report into a
    729  // CensusTreeNode tree repeatedly is idempotent.
    730  censusTreeNodeIdCounter = 0;
    731 
    732  const visitor = new CensusTreeNodeVisitor();
    733  walk(breakdown, report, visitor);
    734  let result = visitor.root();
    735 
    736  if (options.invert) {
    737    result = invert(result);
    738  }
    739 
    740  if (typeof options.filter === "string") {
    741    result = filter(result, makeFilterPredicate(options.filter));
    742  }
    743 
    744  // If the report is a delta report that was generated by diffing two other
    745  // reports, make sure to use the basis totals rather than the totals of the
    746  // difference.
    747  if (typeof report[basisTotalBytes] === "number") {
    748    result.totalBytes = report[basisTotalBytes];
    749    result.totalCount = report[basisTotalCount];
    750  }
    751 
    752  // Inverting and filtering could have messed up the sort order, so do a
    753  // depth-first search of the tree and ensure that siblings are sorted.
    754  const comparator = options.invert ? compareBySelf : compareByTotal;
    755  (function ensureSorted(node) {
    756    if (node.children) {
    757      node.children.sort(comparator);
    758      for (let i = 0, length = node.children.length; i < length; i++) {
    759        ensureSorted(node.children[i]);
    760      }
    761    }
    762  })(result);
    763 
    764  return result;
    765 };