Bug 1200853 - Utility function for mapping census data into a tree view. r=fitzgen
authorJordan Santell <jsantell@mozilla.com>
Wed, 02 Sep 2015 14:46:26 -0700
changeset 290204 db8602cd98b1447b41be33cf9cf743c8ff7d1388
parent 290203 3cb68e7454d79f7b26702675f92420b29dc00bc0
child 290205 78b356dd104c7027ac2cc662560435b96a2a6f79
push id5104
push uservivekb.balakrishnan@gmail.com
push dateThu, 03 Sep 2015 21:52:07 +0000
reviewersfitzgen
bugs1200853
milestone43.0a1
Bug 1200853 - Utility function for mapping census data into a tree view. r=fitzgen
browser/devtools/memory/modules/census.js
browser/devtools/memory/moz.build
browser/devtools/memory/test/unit/head.js
browser/devtools/memory/test/unit/test_census-01.js
browser/devtools/memory/test/unit/test_census-02.js
browser/devtools/memory/test/unit/test_census-03.js
browser/devtools/memory/test/unit/xpcshell.ini
browser/devtools/moz.build
new file mode 100644
--- /dev/null
+++ b/browser/devtools/memory/modules/census.js
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Utilities for interfacing with census reports from dbg.memory.takeCensus().
+ */
+
+const COARSE_TYPES = new Set(["objects", "scripts", "strings", "other"]);
+
+/**
+ * Takes a report from a census (`dbg.memory.takeCensus()`) and the breakdown
+ * used to generate the census and returns a structure used to render
+ * a tree to display the data.
+ *
+ * Returns a recursive "CensusTreeNode" object, looking like:
+ *
+ * CensusTreeNode = {
+ *   // `children` if it exists, is sorted by `bytes`, if they are leaf nodes.
+ *   children: ?[<CensusTreeNode...>],
+ *   name: <?String>
+ *   count: <?Number>
+ *   bytes: <?Number>
+ * }
+ *
+ * @param {Object} breakdown
+ * @param {Object} report
+ * @param {?String} name
+ * @return {Object}
+ */
+function CensusTreeNode (breakdown, report, name) {
+  this.name = name;
+  this.bytes = void 0;
+  this.count = void 0;
+  this.children = void 0;
+
+  CensusTreeNodeBreakdowns[breakdown.by](this, breakdown, report);
+
+  if (this.children) {
+    this.children.sort(sortByBytes);
+  }
+}
+
+CensusTreeNode.prototype = null;
+
+/**
+ * A series of functions to handle different breakdowns used by CensusTreeNode
+ */
+const CensusTreeNodeBreakdowns = Object.create(null);
+
+CensusTreeNodeBreakdowns.count = function (node, breakdown, report) {
+  if (breakdown.bytes === true) {
+    node.bytes = report.bytes;
+  }
+  if (breakdown.count === true) {
+    node.count = report.count;
+  }
+};
+
+CensusTreeNodeBreakdowns.internalType = function (node, breakdown, report) {
+  node.children = [];
+  for (let key of Object.keys(report)) {
+    node.children.push(new CensusTreeNode(breakdown.then, report[key], key));
+  }
+}
+
+CensusTreeNodeBreakdowns.objectClass = function (node, breakdown, report) {
+  node.children = [];
+  for (let key of Object.keys(report)) {
+    let bd = key === "other" ? breakdown.other : breakdown.then;
+    node.children.push(new CensusTreeNode(bd, report[key], key));
+  }
+}
+
+CensusTreeNodeBreakdowns.coarseType = function (node, breakdown, report) {
+  node.children = [];
+  for (let type of Object.keys(breakdown).filter(type => COARSE_TYPES.has(type))) {
+    node.children.push(new CensusTreeNode(breakdown[type], report[type], type));
+  }
+}
+
+function sortByBytes (a, b) {
+  return (b.bytes || 0) - (a.bytes || 0);
+}
+
+exports.CensusTreeNode = CensusTreeNode;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/memory/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES.devtools.memory += [
+    'modules/census.js',
+]
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/browser/devtools/memory/test/unit/head.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+const CC = Components.Constructor;
+
+const { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+const { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+const { CensusTreeNode } = require("devtools/memory/census");
+const Services = require("Services");
+
+// Always log packets when running tests. runxpcshelltests.py will throw
+// the output away anyway, unless you give it the --verbose flag.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"]
+  .createInstance(Ci.nsIPrincipal);
+
+function dumpn(msg) {
+  dump("HEAPSNAPSHOT-TEST: " + msg + "\n");
+}
+
+function addTestingFunctionsToGlobal(global) {
+  global.eval(
+    `
+    const testingFunctions = Components.utils.getJSTestingFunctions();
+    for (let k in testingFunctions) {
+      this[k] = testingFunctions[k];
+    }
+    `
+  );
+  if (!global.print) {
+    global.print = do_print;
+  }
+  if (!global.newGlobal) {
+    global.newGlobal = newGlobal;
+  }
+  if (!global.Debugger) {
+    addDebuggerToGlobal(global);
+  }
+}
+
+addTestingFunctionsToGlobal(this);
+
+/**
+ * Create a new global, with all the JS shell testing functions. Similar to the
+ * newGlobal function exposed to JS shells, and useful for porting JS shell
+ * tests to xpcshell tests.
+ */
+function newGlobal() {
+  const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
+  addTestingFunctionsToGlobal(global);
+  return global;
+}
+
+function assertThrowsValue(f, val, msg) {
+  var fullmsg;
+  try {
+    f();
+  } catch (exc) {
+    if ((exc === val) === (val === val) && (val !== 0 || 1 / exc === 1 / val))
+      return;
+    fullmsg = "Assertion failed: expected exception " + val + ", got " + exc;
+  }
+  if (fullmsg === undefined)
+    fullmsg = "Assertion failed: expected exception " + val + ", no exception thrown";
+  if (msg !== undefined)
+    fullmsg += " - " + msg;
+  throw new Error(fullmsg);
+}
+
+/**
+ * Returns the full path of the file with the specified name in a
+ * platform-independent and URL-like form.
+ */
+function getFilePath(aName, aAllowMissing=false, aUsePlatformPathSeparator=false)
+{
+  let file = do_get_file(aName, aAllowMissing);
+  let path = Services.io.newFileURI(file).spec;
+  let filePrePath = "file://";
+  if ("nsILocalFileWin" in Ci &&
+      file instanceof Ci.nsILocalFileWin) {
+    filePrePath += "/";
+  }
+
+  path = path.slice(filePrePath.length);
+
+  if (aUsePlatformPathSeparator && path.match(/^\w:/)) {
+    path = path.replace(/\//g, "\\");
+  }
+
+  return path;
+}
+
+/**
+ * Save a heap snapshot to the file with the given name in the current
+ * directory, read it back as a HeapSnapshot instance, and then take a census of
+ * the heap snapshot's serialized heap graph with the provided census options.
+ *
+ * @param {Object|undefined} censusOptions
+ *        Options that should be passed through to the takeCensus method. See
+ *        js/src/doc/Debugger/Debugger.Memory.md for details.
+ *
+ * @param {Debugger|null} dbg
+ *        If a Debugger object is given, only serialize the subgraph covered by
+ *        the Debugger's debuggees. If null, serialize the whole heap graph.
+ *
+ * @param {String} fileName
+ *        The file name to save the heap snapshot's core dump file to, within
+ *        the current directory.
+ *
+ * @returns Census
+ */
+function saveHeapSnapshotAndTakeCensus(dbg=null, censusOptions=undefined,
+                                       // Add the Math.random() so that parallel
+                                       // tests are less likely to mess with
+                                       // each other.
+                                       fileName="core-dump-" + (Math.random()) + ".tmp") {
+  const filePath = getFilePath(fileName, true, true);
+  ok(filePath, "Should get a file path to save the core dump to.");
+
+  const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
+  ChromeUtils.saveHeapSnapshot(filePath, snapshotOptions);
+  ok(true, "Should have saved a heap snapshot to " + filePath);
+
+  const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+  ok(snapshot, "Should have read a heap snapshot back from " + filePath);
+  ok(snapshot instanceof HeapSnapshot, "snapshot should be an instance of HeapSnapshot");
+
+  equal(typeof snapshot.takeCensus, "function", "snapshot should have a takeCensus method");
+  return snapshot.takeCensus(censusOptions);
+}
+
+function compareCensusViewData (breakdown, report, expected, assertion) {
+  let data = new CensusTreeNode(breakdown, report);
+  console.log(data);
+  console.log(expected);
+  equal(JSON.stringify(data), JSON.stringify(expected), assertion);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/memory/test/unit/test_census-01.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests CensusTreeNode with `internalType` breakdown.
+ */
+function run_test() {
+  compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, `${JSON.stringify(BREAKDOWN)} has correct results.`);
+}
+
+const BREAKDOWN = {
+  by: "internalType",
+  then: { by: "count", count: true, bytes: true }
+};
+
+const REPORT = {
+  "JSObject": {
+    "bytes": 100,
+    "count": 10,
+  },
+  "js::Shape": {
+    "bytes": 500,
+    "count": 50,
+  },
+  "JSString": {
+    "bytes": 0,
+    "count": 0,
+  },
+};
+
+const EXPECTED = {
+  children: [
+    { name: "js::Shape", bytes: 500, count: 50, },
+    { name: "JSObject", bytes: 100, count: 10, },
+    { name: "JSString", bytes: 0, count: 0, },
+  ],
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/memory/test/unit/test_census-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests CensusTreeNode with `coarseType` breakdown.
+ */
+
+function run_test() {
+  compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, `${JSON.stringify(BREAKDOWN)} has correct results.`);
+}
+
+const countBreakdown = { by: "count", count: true, bytes: true };
+
+const BREAKDOWN = {
+  by: "coarseType",
+  objects: { by: "objectClass", then: countBreakdown },
+  strings: countBreakdown,
+  other: { by: "internalType", then: countBreakdown },
+};
+
+const REPORT = {
+  "objects": {
+    "Function": { bytes: 10, count: 1 },
+    "Array": { bytes: 20, count: 2 },
+  },
+  "strings": { bytes: 10, count: 1 },
+  "other": {
+    "js::Shape": { bytes: 30, count: 3 },
+    "js::Shape2": { bytes: 40, count: 4 }
+  },
+};
+
+const EXPECTED = {
+  children: [
+    { name: "strings", bytes: 10, count: 1, },
+    { name: "objects", children: [
+      { name: "Array", bytes: 20, count: 2, },
+      { name: "Function", bytes: 10, count: 1, },
+    ]},
+    { name: "other", children: [
+      { name: "js::Shape2", bytes: 40, count: 4, },
+      { name: "js::Shape", bytes: 30, count: 3, },
+    ]},
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/memory/test/unit/test_census-03.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests CensusTreeNode with `objectClass` breakdown.
+ */
+
+function run_test() {
+  compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, `${JSON.stringify(BREAKDOWN)} has correct results.`);
+}
+
+const countBreakdown = { by: "count", count: true, bytes: true };
+
+const BREAKDOWN = {
+  by: "objectClass",
+  then: countBreakdown,
+  other: { by: "internalType", then: countBreakdown }
+};
+
+const REPORT = {
+  "Function": { bytes: 10, count: 10 },
+  "Array": { bytes: 100, count: 1 },
+  "other": {
+    "JIT::CODE::NOW!!!": { bytes: 20, count: 2 },
+    "JIT::CODE::LATER!!!": { bytes: 40, count: 4 }
+  }
+};
+
+const EXPECTED = {
+  children: [
+    { name: "Array", bytes: 100, count: 1 },
+    { name: "Function", bytes: 10, count: 10 },
+    { name: "other", children: [
+      { name: "JIT::CODE::LATER!!!", bytes: 40, count: 4 },
+      { name: "JIT::CODE::NOW!!!", bytes: 20, count: 2 },
+    ]}
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/memory/test/unit/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android' || toolkit == 'gonk'
+
+[test_census-01.js]
+[test_census-02.js]
+[test_census-03.js]
--- a/browser/devtools/moz.build
+++ b/browser/devtools/moz.build
@@ -11,16 +11,17 @@ DIRS += [
     'commandline',
     'debugger',
     'eyedropper',
     'fontinspector',
     'framework',
     'inspector',
     'layoutview',
     'markupview',
+    'memory',
     'netmonitor',
     'performance',
     'projecteditor',
     'promisedebugger',
     'responsivedesign',
     'scratchpad',
     'shadereditor',
     'shared',