census-tree-node.js 23 KB

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