CensusUtils.js 14 KB

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