diff --git a/browser/devtools/memory/modules/census.js b/browser/devtools/memory/modules/census.js new file mode 100644 index 00000000000..c9ec918782b --- /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: ?[], + * name: + * count: + * bytes: + * } + * + * @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; diff --git a/browser/devtools/memory/moz.build b/browser/devtools/memory/moz.build new file mode 100644 index 00000000000..7a6f9a2043c --- /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'] diff --git a/browser/devtools/memory/test/unit/head.js b/browser/devtools/memory/test/unit/head.js new file mode 100644 index 00000000000..87051caaaf9 --- /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); +} diff --git a/browser/devtools/memory/test/unit/test_census-01.js b/browser/devtools/memory/test/unit/test_census-01.js new file mode 100644 index 00000000000..56b52cf73f2 --- /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, }, + ], +}; diff --git a/browser/devtools/memory/test/unit/test_census-02.js b/browser/devtools/memory/test/unit/test_census-02.js new file mode 100644 index 00000000000..23b8bb0084a --- /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, }, + ]}, + ] +}; diff --git a/browser/devtools/memory/test/unit/test_census-03.js b/browser/devtools/memory/test/unit/test_census-03.js new file mode 100644 index 00000000000..c8bcde2fe87 --- /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 }, + ]} + ] +}; diff --git a/browser/devtools/memory/test/unit/xpcshell.ini b/browser/devtools/memory/test/unit/xpcshell.ini new file mode 100644 index 00000000000..8444a471e76 --- /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] diff --git a/browser/devtools/moz.build b/browser/devtools/moz.build index a89409a75a7..872e6672fc7 100644 --- a/browser/devtools/moz.build +++ b/browser/devtools/moz.build @@ -16,6 +16,7 @@ DIRS += [ 'inspector', 'layoutview', 'markupview', + 'memory', 'netmonitor', 'performance', 'projecteditor',