summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutmemory
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/aboutmemory')
-rw-r--r--toolkit/components/aboutmemory/content/aboutCompartments.xhtml16
-rw-r--r--toolkit/components/aboutmemory/content/aboutMemory.css154
-rw-r--r--toolkit/components/aboutmemory/content/aboutMemory.js2042
-rw-r--r--toolkit/components/aboutmemory/content/aboutMemory.xhtml15
-rw-r--r--toolkit/components/aboutmemory/jar.mn8
-rw-r--r--toolkit/components/aboutmemory/moz.build12
-rw-r--r--toolkit/components/aboutmemory/tests/.eslintrc.js7
-rw-r--r--toolkit/components/aboutmemory/tests/chrome.ini28
-rw-r--r--toolkit/components/aboutmemory/tests/crash-dump-diff1.json11
-rw-r--r--toolkit/components/aboutmemory/tests/crash-dump-diff2.json11
-rw-r--r--toolkit/components/aboutmemory/tests/crash-dump-good.json14
-rw-r--r--toolkit/components/aboutmemory/tests/memory-reports-bad.json3
-rw-r--r--toolkit/components/aboutmemory/tests/memory-reports-diff1.json45
-rw-r--r--toolkit/components/aboutmemory/tests/memory-reports-diff2.json44
-rw-r--r--toolkit/components/aboutmemory/tests/memory-reports-good.json29
-rw-r--r--toolkit/components/aboutmemory/tests/remote.xul12
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory.xul602
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory2.xul423
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory3.xul515
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory4.xul179
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory5.xul167
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory6.xul88
-rw-r--r--toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xul98
-rw-r--r--toolkit/components/aboutmemory/tests/test_memoryReporters.xul424
-rw-r--r--toolkit/components/aboutmemory/tests/test_memoryReporters2.xul108
-rw-r--r--toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul54
26 files changed, 5109 insertions, 0 deletions
diff --git a/toolkit/components/aboutmemory/content/aboutCompartments.xhtml b/toolkit/components/aboutmemory/content/aboutCompartments.xhtml
new file mode 100644
index 000000000..83432295a
--- /dev/null
+++ b/toolkit/components/aboutmemory/content/aboutCompartments.xhtml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>about:compartments</title>
+ <meta name="viewport" content="width=device-width"/>
+ </head>
+
+ <body>about:compartments no longer exists. The lists of compartments and
+ ghost windows can now be found in the "Other Measurements" section of
+ about:memory.</body>
+</html>
diff --git a/toolkit/components/aboutmemory/content/aboutMemory.css b/toolkit/components/aboutmemory/content/aboutMemory.css
new file mode 100644
index 000000000..b63bbac13
--- /dev/null
+++ b/toolkit/components/aboutmemory/content/aboutMemory.css
@@ -0,0 +1,154 @@
+/* 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/. */
+
+/*
+ * The version used for mobile is located at
+ * mobile/android/themes/core/aboutMemory.css.
+ * Desktop-specific stuff is at the bottom of this file.
+ */
+
+html {
+ background: -moz-Dialog;
+ font: message-box;
+}
+
+body {
+ padding: 0 2em;
+ margin: 0;
+ min-width: 45em;
+ margin: auto;
+}
+
+div.ancillary {
+ margin: 0.5em 0;
+ -moz-user-select: none;
+}
+
+div.section {
+ padding: 2em;
+ margin: 1em 0em;
+ border: 1px solid ThreeDShadow;
+ border-radius: 10px;
+ background: -moz-Field;
+}
+
+div.opsRow {
+ padding: 0.5em;
+ margin-right: 0.5em;
+ margin-top: 0.5em;
+ border: 1px solid ThreeDShadow;
+ border-radius: 10px;
+ background: -moz-Field;
+ display: inline-block;
+}
+
+div.opsRowLabel {
+ display: block;
+ margin-bottom: 0.2em;
+ font-weight: bold;
+}
+
+.opsRowLabel label {
+ margin-left: 1em;
+ font-weight: normal;
+}
+
+div.non-verbose pre.entries {
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+}
+
+h1 {
+ padding: 0;
+ margin: 0;
+ display: inline; /* allow subsequent text to the right of the heading */
+}
+
+h2 {
+ background: #ddd;
+ padding-left: .1em;
+}
+
+h3 {
+ display: inline; /* allow subsequent text to the right of the heading */
+}
+
+a.upDownArrow {
+ font-size: 130%;
+ text-decoration: none;
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.accuracyWarning {
+ color: #d22;
+}
+
+.badInputWarning {
+ color: #f00;
+}
+
+.treeline {
+ color: #888;
+}
+
+.mrValue {
+ font-weight: bold;
+ color: #400;
+}
+
+.mrPerc {
+}
+
+.mrSep {
+}
+
+.mrName {
+ color: #004;
+}
+
+.mrNote {
+ color: #604;
+}
+
+.hasKids {
+ cursor: pointer;
+}
+
+.hasKids:hover {
+ text-decoration: underline;
+}
+
+.noselect {
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.option {
+ font-size: 80%;
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.legend {
+ font-size: 80%;
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.debug {
+ font-size: 80%;
+}
+
+.hidden {
+ display: none;
+}
+
+.invalid {
+ color: #fff;
+ background-color: #f00;
+}
+
+/* Desktop-specific parts go here. */
+
+.hasKids:hover {
+ text-decoration: underline;
+}
+
diff --git a/toolkit/components/aboutmemory/content/aboutMemory.js b/toolkit/components/aboutmemory/content/aboutMemory.js
new file mode 100644
index 000000000..c62416dc5
--- /dev/null
+++ b/toolkit/components/aboutmemory/content/aboutMemory.js
@@ -0,0 +1,2042 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+// You can direct about:memory to immediately load memory reports from a file
+// by providing a file= query string. For example,
+//
+// about:memory?file=/home/username/reports.json.gz
+//
+// "file=" is not case-sensitive. We'll URI-unescape the contents of the
+// "file=" argument, and obviously the filename is case-sensitive iff you're on
+// a case-sensitive filesystem. If you specify more than one "file=" argument,
+// only the first one is used.
+
+"use strict";
+
+// ---------------------------------------------------------------------------
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var CC = Components.Constructor;
+
+const KIND_NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
+const KIND_HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+const KIND_OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+
+const UNITS_BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+const UNITS_COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
+const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
+const UNITS_PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "nsBinaryStream",
+ () => CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"));
+XPCOMUtils.defineLazyGetter(this, "nsFile",
+ () => CC("@mozilla.org/file/local;1",
+ "nsIFile", "initWithPath"));
+XPCOMUtils.defineLazyGetter(this, "nsGzipConverter",
+ () => CC("@mozilla.org/streamconv;1?from=gzip&to=uncompressed",
+ "nsIStreamConverter"));
+
+var gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(Ci.nsIMemoryReporterManager);
+
+const gPageName = 'about:memory';
+document.title = gPageName;
+
+const gUnnamedProcessStr = "Main Process";
+
+var gIsDiff = false;
+
+// ---------------------------------------------------------------------------
+
+// Forward slashes in URLs in paths are represented with backslashes to avoid
+// being mistaken for path separators. Paths/names where this hasn't been
+// undone are prefixed with "unsafe"; the rest are prefixed with "safe".
+function flipBackslashes(aUnsafeStr)
+{
+ // Save memory by only doing the replacement if it's necessary.
+ return (aUnsafeStr.indexOf('\\') === -1)
+ ? aUnsafeStr
+ : aUnsafeStr.replace(/\\/g, '/');
+}
+
+const gAssertionFailureMsgPrefix = "aboutMemory.js assertion failed: ";
+
+// This is used for things that should never fail, and indicate a defect in
+// this file if they do.
+function assert(aCond, aMsg)
+{
+ if (!aCond) {
+ reportAssertionFailure(aMsg)
+ throw new Error(gAssertionFailureMsgPrefix + aMsg);
+ }
+}
+
+// This is used for malformed input from memory reporters.
+function assertInput(aCond, aMsg)
+{
+ if (!aCond) {
+ throw new Error("Invalid memory report(s): " + aMsg);
+ }
+}
+
+function handleException(ex)
+{
+ let str = "" + ex;
+ if (str.startsWith(gAssertionFailureMsgPrefix)) {
+ // Argh, assertion failure within this file! Give up.
+ throw ex;
+ } else {
+ // File or memory reporter problem. Print a message.
+ updateMainAndFooter(str, HIDE_FOOTER, "badInputWarning");
+ }
+}
+
+function reportAssertionFailure(aMsg)
+{
+ let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
+ if (debug.isDebugBuild) {
+ debug.assertion(aMsg, "false", "aboutMemory.js", 0);
+ }
+}
+
+function debug(x)
+{
+ let section = appendElement(document.body, 'div', 'section');
+ appendElementWithText(section, "div", "debug", JSON.stringify(x));
+}
+
+// ---------------------------------------------------------------------------
+
+function onUnload()
+{
+}
+
+// ---------------------------------------------------------------------------
+
+// The <div> holding everything but the header and footer (if they're present).
+// It's what is updated each time the page changes.
+var gMain;
+
+// The <div> holding the footer.
+var gFooter;
+
+// The "verbose" checkbox.
+var gVerbose;
+
+// The "anonymize" checkbox.
+var gAnonymize;
+
+// Values for the |aFooterAction| argument to updateTitleMainAndFooter.
+var HIDE_FOOTER = 0;
+var SHOW_FOOTER = 1;
+
+function updateTitleMainAndFooter(aTitleNote, aMsg, aFooterAction, aClassName)
+{
+ document.title = gPageName;
+ if (aTitleNote) {
+ document.title += " (" + aTitleNote + ")";
+ }
+
+ // Clear gMain by replacing it with an empty node.
+ let tmp = gMain.cloneNode(false);
+ gMain.parentNode.replaceChild(tmp, gMain);
+ gMain = tmp;
+
+ gMain.classList.remove('hidden');
+ gMain.classList.remove('verbose');
+ gMain.classList.remove('non-verbose');
+ if (gVerbose) {
+ gMain.classList.add(gVerbose.checked ? 'verbose' : 'non-verbose');
+ }
+
+ let msgElement;
+ if (aMsg) {
+ let className = "section"
+ if (aClassName) {
+ className = className + " " + aClassName;
+ }
+ msgElement = appendElementWithText(gMain, 'div', className, aMsg);
+ }
+
+ switch (aFooterAction) {
+ case HIDE_FOOTER: gFooter.classList.add('hidden'); break;
+ case SHOW_FOOTER: gFooter.classList.remove('hidden'); break;
+ default: assert(false, "bad footer action in updateTitleMainAndFooter");
+ }
+ return msgElement;
+}
+
+function updateMainAndFooter(aMsg, aFooterAction, aClassName)
+{
+ return updateTitleMainAndFooter("", aMsg, aFooterAction, aClassName);
+}
+
+function appendTextNode(aP, aText)
+{
+ let e = document.createTextNode(aText);
+ aP.appendChild(e);
+ return e;
+}
+
+function appendElement(aP, aTagName, aClassName)
+{
+ let e = document.createElement(aTagName);
+ if (aClassName) {
+ e.className = aClassName;
+ }
+ aP.appendChild(e);
+ return e;
+}
+
+function appendElementWithText(aP, aTagName, aClassName, aText)
+{
+ let e = appendElement(aP, aTagName, aClassName);
+ // Setting textContent clobbers existing children, but there are none. More
+ // importantly, it avoids creating a JS-land object for the node, saving
+ // memory.
+ e.textContent = aText;
+ return e;
+}
+
+// ---------------------------------------------------------------------------
+
+const explicitTreeDescription =
+"This tree covers explicit memory allocations by the application. It includes \
+\n\n\
+* allocations made at the operating system level (via calls to functions such as \
+VirtualAlloc, vm_allocate, and mmap), \
+\n\n\
+* allocations made at the heap allocation level (via functions such as malloc, \
+calloc, realloc, memalign, operator new, and operator new[]) that have not been \
+explicitly decommitted (i.e. evicted from memory and swap), and \
+\n\n\
+* where possible, the overhead of the heap allocator itself.\
+\n\n\
+It excludes memory that is mapped implicitly such as code and data segments, \
+and thread stacks. \
+\n\n\
+'explicit' is not guaranteed to cover every explicit allocation, but it does cover \
+most (including the entire heap), and therefore it is the single best number to \
+focus on when trying to reduce memory usage.";
+
+// ---------------------------------------------------------------------------
+
+function appendButton(aP, aTitle, aOnClick, aText, aId)
+{
+ let b = appendElementWithText(aP, "button", "", aText);
+ b.title = aTitle;
+ b.onclick = aOnClick;
+ if (aId) {
+ b.id = aId;
+ }
+ return b;
+}
+
+function appendHiddenFileInput(aP, aId, aChangeListener)
+{
+ let input = appendElementWithText(aP, "input", "hidden", "");
+ input.type = "file";
+ input.id = aId; // used in testing
+ input.addEventListener("change", aChangeListener);
+ return input;
+}
+
+function onLoad()
+{
+ // Generate the header.
+
+ let header = appendElement(document.body, "div", "ancillary");
+
+ // A hidden file input element that can be invoked when necessary.
+ let fileInput1 = appendHiddenFileInput(header, "fileInput1", function() {
+ let file = this.files[0];
+ let filename = file.mozFullPath;
+ updateAboutMemoryFromFile(filename);
+ });
+
+ // Ditto.
+ let fileInput2 =
+ appendHiddenFileInput(header, "fileInput2", function(e) {
+ let file = this.files[0];
+ // First time around, we stash a copy of the filename and reinvoke. Second
+ // time around we do the diff and display.
+ if (!this.filename1) {
+ this.filename1 = file.mozFullPath;
+
+ // e.skipClick is only true when testing -- it allows fileInput2's
+ // onchange handler to be re-called without having to go via the file
+ // picker.
+ if (!e.skipClick) {
+ this.click();
+ }
+ } else {
+ let filename1 = this.filename1;
+ delete this.filename1;
+ updateAboutMemoryFromTwoFiles(filename1, file.mozFullPath);
+ }
+ });
+
+ const CuDesc = "Measure current memory reports and show.";
+ const LdDesc = "Load memory reports from file and show.";
+ const DfDesc = "Load memory report data from two files and show the " +
+ "difference.";
+
+ const SvDesc = "Save memory reports to file.";
+
+ const GCDesc = "Do a global garbage collection.";
+ const CCDesc = "Do a cycle collection.";
+ const MMDesc = "Send three \"heap-minimize\" notifications in a " +
+ "row. Each notification triggers a global garbage " +
+ "collection followed by a cycle collection, and causes the " +
+ "process to reduce memory usage in other ways, e.g. by " +
+ "flushing various caches.";
+
+ const GCAndCCLogDesc = "Save garbage collection log and concise cycle " +
+ "collection log.\n" +
+ "WARNING: These logs may be large (>1GB).";
+ const GCAndCCAllLogDesc = "Save garbage collection log and verbose cycle " +
+ "collection log.\n" +
+ "WARNING: These logs may be large (>1GB).";
+
+ const DMDEnabledDesc = "Analyze memory reports coverage and save the " +
+ "output to the temp directory.\n";
+ const DMDDisabledDesc = "DMD is not running. Please re-start with $DMD and " +
+ "the other relevant environment variables set " +
+ "appropriately.";
+
+ let ops = appendElement(header, "div", "");
+
+ let row1 = appendElement(ops, "div", "opsRow");
+
+ let labelDiv1 =
+ appendElementWithText(row1, "div", "opsRowLabel", "Show memory reports");
+ let label1 = appendElementWithText(labelDiv1, "label", "");
+ gVerbose = appendElement(label1, "input", "");
+ gVerbose.type = "checkbox";
+ gVerbose.id = "verbose"; // used for testing
+ appendTextNode(label1, "verbose");
+
+ const kEllipsis = "\u2026";
+
+ // The "measureButton" id is used for testing.
+ appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton");
+ appendButton(row1, LdDesc, () => fileInput1.click(), "Load" + kEllipsis);
+ appendButton(row1, DfDesc, () => fileInput2.click(),
+ "Load and diff" + kEllipsis);
+
+ let row2 = appendElement(ops, "div", "opsRow");
+
+ let labelDiv2 =
+ appendElementWithText(row2, "div", "opsRowLabel", "Save memory reports");
+ appendButton(row2, SvDesc, saveReportsToFile, "Measure and save" + kEllipsis);
+
+ // XXX: this isn't a great place for this checkbox, but I can't think of
+ // anywhere better.
+ let label2 = appendElementWithText(labelDiv2, "label", "");
+ gAnonymize = appendElement(label2, "input", "");
+ gAnonymize.type = "checkbox";
+ appendTextNode(label2, "anonymize");
+
+ let row3 = appendElement(ops, "div", "opsRow");
+
+ appendElementWithText(row3, "div", "opsRowLabel", "Free memory");
+ appendButton(row3, GCDesc, doGC, "GC");
+ appendButton(row3, CCDesc, doCC, "CC");
+ appendButton(row3, MMDesc, doMMU, "Minimize memory usage");
+
+ let row4 = appendElement(ops, "div", "opsRow");
+
+ appendElementWithText(row4, "div", "opsRowLabel", "Save GC & CC logs");
+ appendButton(row4, GCAndCCLogDesc,
+ saveGCLogAndConciseCCLog, "Save concise", 'saveLogsConcise');
+ appendButton(row4, GCAndCCAllLogDesc,
+ saveGCLogAndVerboseCCLog, "Save verbose", 'saveLogsVerbose');
+
+ // Three cases here:
+ // - DMD is disabled (i.e. not built): don't show the button.
+ // - DMD is enabled but is not running: show the button, but disable it.
+ // - DMD is enabled and is running: show the button and enable it.
+ if (gMgr.isDMDEnabled) {
+ let row5 = appendElement(ops, "div", "opsRow");
+
+ appendElementWithText(row5, "div", "opsRowLabel", "Save DMD output");
+ let enableButtons = gMgr.isDMDRunning;
+
+ let dmdButton =
+ appendButton(row5, enableButtons ? DMDEnabledDesc : DMDDisabledDesc,
+ doDMD, "Save");
+ dmdButton.disabled = !enableButtons;
+ }
+
+ // Generate the main div, where content ("section" divs) will go. It's
+ // hidden at first.
+
+ gMain = appendElement(document.body, 'div', '');
+ gMain.id = 'mainDiv';
+
+ // Generate the footer. It's hidden at first.
+
+ gFooter = appendElement(document.body, 'div', 'ancillary hidden');
+
+ let a = appendElementWithText(gFooter, "a", "option",
+ "Troubleshooting information");
+ a.href = "about:support";
+
+ let legendText1 = "Click on a non-leaf node in a tree to expand ('++') " +
+ "or collapse ('--') its children.";
+ let legendText2 = "Hover the pointer over the name of a memory report " +
+ "to see a description of what it measures.";
+
+ appendElementWithText(gFooter, "div", "legend", legendText1);
+ appendElementWithText(gFooter, "div", "legend hiddenOnMobile", legendText2);
+
+ // See if we're loading from a file. (Because about:memory is a non-standard
+ // URL, location.search is undefined, so we have to use location.href
+ // instead.)
+ let search = location.href.split('?')[1];
+ if (search) {
+ let searchSplit = search.split('&');
+ for (let i = 0; i < searchSplit.length; i++) {
+ if (searchSplit[i].toLowerCase().startsWith('file=')) {
+ let filename = searchSplit[i].substring('file='.length);
+ updateAboutMemoryFromFile(decodeURIComponent(filename));
+ return;
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+function doGC()
+{
+ Services.obs.notifyObservers(null, "child-gc-request", null);
+ Cu.forceGC();
+ updateMainAndFooter("Garbage collection completed", HIDE_FOOTER);
+}
+
+function doCC()
+{
+ Services.obs.notifyObservers(null, "child-cc-request", null);
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .cycleCollect();
+ updateMainAndFooter("Cycle collection completed", HIDE_FOOTER);
+}
+
+function doMMU()
+{
+ Services.obs.notifyObservers(null, "child-mmu-request", null);
+ gMgr.minimizeMemoryUsage(
+ () => updateMainAndFooter("Memory minimization completed", HIDE_FOOTER));
+}
+
+function doMeasure()
+{
+ updateAboutMemoryFromReporters();
+}
+
+function saveGCLogAndConciseCCLog()
+{
+ dumpGCLogAndCCLog(false);
+}
+
+function saveGCLogAndVerboseCCLog()
+{
+ dumpGCLogAndCCLog(true);
+}
+
+function doDMD()
+{
+ updateMainAndFooter("Saving memory reports and DMD output...", HIDE_FOOTER);
+ try {
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
+ .getService(Ci.nsIMemoryInfoDumper);
+
+ dumper.dumpMemoryInfoToTempDir(/* identifier = */ "",
+ gAnonymize.checked,
+ /* minimize = */ false);
+ updateMainAndFooter("Saved memory reports and DMD reports analysis " +
+ "to the temp directory",
+ HIDE_FOOTER);
+ } catch (ex) {
+ updateMainAndFooter(ex.toString(), HIDE_FOOTER);
+ }
+}
+
+function dumpGCLogAndCCLog(aVerbose)
+{
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
+ .getService(Ci.nsIMemoryInfoDumper);
+
+ let inProgress = updateMainAndFooter("Saving logs...", HIDE_FOOTER);
+ let section = appendElement(gMain, 'div', "section");
+
+ function displayInfo(gcLog, ccLog, isParent) {
+ appendElementWithText(section, 'div', "",
+ "Saved GC log to " + gcLog.path);
+
+ let ccLogType = aVerbose ? "verbose" : "concise";
+ appendElementWithText(section, 'div', "",
+ "Saved " + ccLogType + " CC log to " + ccLog.path);
+ }
+
+ dumper.dumpGCAndCCLogsToFile("", aVerbose, /* dumpChildProcesses = */ true,
+ { onDump: displayInfo,
+ onFinish: function() {
+ inProgress.remove();
+ }
+ });
+}
+
+/**
+ * Top-level function that does the work of generating the page from the memory
+ * reporters.
+ */
+function updateAboutMemoryFromReporters()
+{
+ updateMainAndFooter("Measuring...", HIDE_FOOTER);
+
+ try {
+ let processLiveMemoryReports =
+ function(aHandleReport, aDisplayReports) {
+ let handleReport = function(aProcess, aUnsafePath, aKind, aUnits,
+ aAmount, aDescription) {
+ aHandleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
+ aDescription, /* presence = */ undefined);
+ }
+
+ let displayReportsAndFooter = function() {
+ updateTitleMainAndFooter("live measurement", "", SHOW_FOOTER);
+ aDisplayReports();
+ }
+
+ gMgr.getReports(handleReport, null, displayReportsAndFooter, null,
+ gAnonymize.checked);
+ }
+
+ // Process the reports from the live memory reporters.
+ appendAboutMemoryMain(processLiveMemoryReports,
+ gMgr.hasMozMallocUsableSize);
+
+ } catch (ex) {
+ handleException(ex);
+ }
+}
+
+// Increment this if the JSON format changes.
+//
+var gCurrentFileFormatVersion = 1;
+
+
+/**
+ * Parse a string as JSON and extract the |memory_report| property if it has
+ * one, which indicates the string is from a crash dump.
+ *
+ * @param aStr
+ * The string.
+ * @return The extracted object.
+ */
+function parseAndUnwrapIfCrashDump(aStr) {
+ let obj = JSON.parse(aStr);
+ if (obj.memory_report !== undefined) {
+ // It looks like a crash dump. The memory reports should be in the
+ // |memory_report| property.
+ obj = obj.memory_report;
+ }
+ return obj;
+}
+
+/**
+ * Populate about:memory using the data in the given JSON object.
+ *
+ * @param aObj
+ * An object that (hopefully!) conforms to the JSON schema used by
+ * nsIMemoryInfoDumper.
+ */
+function updateAboutMemoryFromJSONObject(aObj)
+{
+ try {
+ assertInput(aObj.version === gCurrentFileFormatVersion,
+ "data version number missing or doesn't match");
+ assertInput(aObj.hasMozMallocUsableSize !== undefined,
+ "missing 'hasMozMallocUsableSize' property");
+ assertInput(aObj.reports && aObj.reports instanceof Array,
+ "missing or non-array 'reports' property");
+
+ let processMemoryReportsFromFile =
+ function(aHandleReport, aDisplayReports) {
+ for (let i = 0; i < aObj.reports.length; i++) {
+ let r = aObj.reports[i];
+
+ // A hack: for a brief time (late in the FF26 and early in the FF27
+ // cycle) we were dumping memory report files that contained reports
+ // whose path began with "redundant/". Such reports were ignored by
+ // about:memory. These reports are no longer produced, but some older
+ // builds are still floating around and producing files that contain
+ // them, so we need to still handle them (i.e. ignore them). This hack
+ // can be removed once FF26 and associated products (e.g. B2G 1.2) are
+ // no longer in common use.
+ if (!r.path.startsWith("redundant/")) {
+ aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
+ r.description, r._presence);
+ }
+ }
+ aDisplayReports();
+ }
+ appendAboutMemoryMain(processMemoryReportsFromFile,
+ aObj.hasMozMallocUsableSize);
+ } catch (ex) {
+ handleException(ex);
+ }
+}
+
+/**
+ * Populate about:memory using the data in the given JSON string.
+ *
+ * @param aStr
+ * A string containing JSON data conforming to the schema used by
+ * nsIMemoryReporterManager::dumpReports.
+ */
+function updateAboutMemoryFromJSONString(aStr)
+{
+ try {
+ let obj = parseAndUnwrapIfCrashDump(aStr);
+ updateAboutMemoryFromJSONObject(obj);
+ } catch (ex) {
+ handleException(ex);
+ }
+}
+
+/**
+ * Loads the contents of a file into a string and passes that to a callback.
+ *
+ * @param aFilename
+ * The name of the file being read from.
+ * @param aTitleNote
+ * A description to put in the page title upon completion.
+ * @param aFn
+ * The function to call and pass the read string to upon completion.
+ */
+function loadMemoryReportsFromFile(aFilename, aTitleNote, aFn)
+{
+ updateMainAndFooter("Loading...", HIDE_FOOTER);
+
+ try {
+ let reader = new FileReader();
+ reader.onerror = () => { throw new Error("FileReader.onerror"); };
+ reader.onabort = () => { throw new Error("FileReader.onabort"); };
+ reader.onload = (aEvent) => {
+ // Clear "Loading..." from above.
+ updateTitleMainAndFooter(aTitleNote, "", SHOW_FOOTER);
+ aFn(aEvent.target.result);
+ };
+
+ // If it doesn't have a .gz suffix, read it as a (legacy) ungzipped file.
+ if (!aFilename.endsWith(".gz")) {
+ reader.readAsText(File.createFromFileName(aFilename));
+ return;
+ }
+
+ // Read compressed gzip file.
+ let converter = new nsGzipConverter();
+ converter.asyncConvertData("gzip", "uncompressed", {
+ data: [],
+ onStartRequest: function(aR, aC) {},
+ onDataAvailable: function(aR, aC, aStream, aO, aCount) {
+ let bi = new nsBinaryStream(aStream);
+ this.data.push(bi.readBytes(aCount));
+ },
+ onStopRequest: function(aR, aC, aStatusCode) {
+ try {
+ if (!Components.isSuccessCode(aStatusCode)) {
+ throw new Components.Exception("Error while reading gzip file", aStatusCode);
+ }
+ reader.readAsText(new Blob(this.data));
+ } catch (ex) {
+ handleException(ex);
+ }
+ }
+ }, null);
+
+ let file = new nsFile(aFilename);
+ let fileChan = NetUtil.newChannel({
+ uri: Services.io.newFileURI(file),
+ loadUsingSystemPrincipal: true
+ });
+ fileChan.asyncOpen2(converter);
+
+ } catch (ex) {
+ handleException(ex);
+ }
+}
+
+/**
+ * Like updateAboutMemoryFromReporters(), but gets its data from a file instead
+ * of the memory reporters.
+ *
+ * @param aFilename
+ * The name of the file being read from. The expected format of the
+ * file's contents is described in a comment in nsIMemoryInfoDumper.idl.
+ */
+function updateAboutMemoryFromFile(aFilename)
+{
+ loadMemoryReportsFromFile(aFilename, /* title note */ aFilename,
+ updateAboutMemoryFromJSONString);
+}
+
+/**
+ * Like updateAboutMemoryFromFile(), but gets its data from a two files and
+ * diffs them.
+ *
+ * @param aFilename1
+ * The name of the first file being read from.
+ * @param aFilename2
+ * The name of the first file being read from.
+ */
+function updateAboutMemoryFromTwoFiles(aFilename1, aFilename2)
+{
+ let titleNote = "diff of " + aFilename1 + " and " + aFilename2;
+ loadMemoryReportsFromFile(aFilename1, titleNote, function(aStr1) {
+ loadMemoryReportsFromFile(aFilename2, titleNote, function(aStr2) {
+ try {
+ let obj1 = parseAndUnwrapIfCrashDump(aStr1);
+ let obj2 = parseAndUnwrapIfCrashDump(aStr2);
+ gIsDiff = true;
+ updateAboutMemoryFromJSONObject(diffJSONObjects(obj1, obj2));
+ gIsDiff = false;
+ } catch (ex) {
+ handleException(ex);
+ }
+ });
+ });
+}
+
+// ---------------------------------------------------------------------------
+
+// Something unlikely to appear in a process name.
+var kProcessPathSep = "^:^:^";
+
+// Short for "diff report".
+function DReport(aKind, aUnits, aAmount, aDescription, aNMerged, aPresence)
+{
+ this._kind = aKind;
+ this._units = aUnits;
+ this._amount = aAmount;
+ this._description = aDescription;
+ this._nMerged = aNMerged;
+ if (aPresence !== undefined) {
+ this._presence = aPresence;
+ }
+}
+
+DReport.prototype = {
+ assertCompatible: function(aKind, aUnits)
+ {
+ assert(this._kind == aKind, "Mismatched kinds");
+ assert(this._units == aUnits, "Mismatched units");
+
+ // We don't check that the "description" properties match. This is because
+ // on Linux we can get cases where the paths are the same but the
+ // descriptions differ, like this:
+ //
+ // "path": "size/other-files/icon-theme.cache/[r--p]",
+ // "description": "/usr/share/icons/gnome/icon-theme.cache (read-only, not executable, private)"
+ //
+ // "path": "size/other-files/icon-theme.cache/[r--p]"
+ // "description": "/usr/share/icons/hicolor/icon-theme.cache (read-only, not executable, private)"
+ //
+ // In those cases, we just use the description from the first-encountered
+ // one, which is what about:memory also does.
+ // (Note: reports with those paths are no longer generated, but allowing
+ // the descriptions to differ seems reasonable.)
+ },
+
+ merge: function(aJr) {
+ this.assertCompatible(aJr.kind, aJr.units);
+ this._amount += aJr.amount;
+ this._nMerged++;
+ },
+
+ toJSON: function(aProcess, aPath, aAmount) {
+ return {
+ process: aProcess,
+ path: aPath,
+ kind: this._kind,
+ units: this._units,
+ amount: aAmount,
+ description: this._description,
+ _presence: this._presence
+ };
+ }
+};
+
+// Constants that indicate if a DReport was present only in one of the data
+// sets, or had to be added for balance.
+DReport.PRESENT_IN_FIRST_ONLY = 1;
+DReport.PRESENT_IN_SECOND_ONLY = 2;
+DReport.ADDED_FOR_BALANCE = 3;
+
+/**
+ * Make a report map, which has combined path+process strings for keys, and
+ * DReport objects for values.
+ *
+ * @param aJSONReports
+ * The |reports| field of a JSON object.
+ * @return The constructed report map.
+ */
+function makeDReportMap(aJSONReports)
+{
+ let dreportMap = {};
+ for (let i = 0; i < aJSONReports.length; i++) {
+ let jr = aJSONReports[i];
+
+ assert(jr.process !== undefined, "Missing process");
+ assert(jr.path !== undefined, "Missing path");
+ assert(jr.kind !== undefined, "Missing kind");
+ assert(jr.units !== undefined, "Missing units");
+ assert(jr.amount !== undefined, "Missing amount");
+ assert(jr.description !== undefined, "Missing description");
+
+ // Strip out some non-deterministic stuff that prevents clean diffs.
+ // Ideally the memory reports themselves would contain information about
+ // which parts of the the process and path need to be stripped -- saving us
+ // from hardwiring knowledge of specific reporters here -- but we have no
+ // mechanism for that. (Any future redesign of how memory reporters work
+ // should include such a mechanism.)
+
+ // Strip PIDs:
+ // - pid 123
+ // - pid=123
+ let pidRegex = /pid([ =])\d+/g;
+ let pidSubst = "pid$1NNN";
+ let process = jr.process.replace(pidRegex, pidSubst);
+ let path = jr.path.replace(pidRegex, pidSubst);
+
+ // Strip addresses:
+ // - .../js-zone(0x12345678)/...
+ // - .../zone(0x12345678)/...
+ // - .../worker(<URL>, 0x12345678)/...
+ path = path.replace(/zone\(0x[0-9A-Fa-f]+\)\//, "zone(0xNNN)/");
+ path = path.replace(/\/worker\((.+), 0x[0-9A-Fa-f]+\)\//,
+ "/worker($1, 0xNNN)/");
+
+ // Strip top window IDs:
+ // - explicit/window-objects/top(<URL>, id=123)/...
+ path = path.replace(/^(explicit\/window-objects\/top\(.*, id=)\d+\)/,
+ "$1NNN)");
+
+ // Strip null principal UUIDs (but not other UUIDs, because they may be
+ // deterministic, such as those used by add-ons).
+ path = path.replace(
+ /moz-nullprincipal:{........-....-....-....-............}/g,
+ "moz-nullprincipal:{NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN}");
+
+ // Normalize omni.ja! paths.
+ path = path.replace(/jar:file:\\\\\\(.+)\\omni.ja!/,
+ "jar:file:\\\\\\...\\omni.ja!");
+
+ let processPath = process + kProcessPathSep + path;
+ let rOld = dreportMap[processPath];
+ if (rOld === undefined) {
+ dreportMap[processPath] =
+ new DReport(jr.kind, jr.units, jr.amount, jr.description, 1, undefined);
+ } else {
+ rOld.merge(jr);
+ }
+ }
+ return dreportMap;
+}
+
+// Return a new dreportMap which is the diff of two dreportMaps. Empties
+// aDReportMap2 along the way.
+function diffDReportMaps(aDReportMap1, aDReportMap2)
+{
+ let result = {};
+
+ for (let processPath in aDReportMap1) {
+ let r1 = aDReportMap1[processPath];
+ let r2 = aDReportMap2[processPath];
+ let r2_amount, r2_nMerged;
+ let presence;
+ if (r2 !== undefined) {
+ r1.assertCompatible(r2._kind, r2._units);
+ r2_amount = r2._amount;
+ r2_nMerged = r2._nMerged;
+ delete aDReportMap2[processPath];
+ presence = undefined; // represents that it's present in both
+ } else {
+ r2_amount = 0;
+ r2_nMerged = 0;
+ presence = DReport.PRESENT_IN_FIRST_ONLY;
+ }
+ result[processPath] =
+ new DReport(r1._kind, r1._units, r2_amount - r1._amount, r1._description,
+ Math.max(r1._nMerged, r2_nMerged), presence);
+ }
+
+ for (let processPath in aDReportMap2) {
+ let r2 = aDReportMap2[processPath];
+ result[processPath] = new DReport(r2._kind, r2._units, r2._amount,
+ r2._description, r2._nMerged,
+ DReport.PRESENT_IN_SECOND_ONLY);
+ }
+
+ return result;
+}
+
+function makeJSONReports(aDReportMap)
+{
+ let reports = [];
+ for (let processPath in aDReportMap) {
+ let r = aDReportMap[processPath];
+ if (r._amount !== 0) {
+ // If _nMerged > 1, we give the full (aggregated) amount in the first
+ // copy, and then use amount=0 in the remainder. When viewed in
+ // about:memory, this shows up as an entry with a "[2]"-style suffix
+ // and the correct amount.
+ let split = processPath.split(kProcessPathSep);
+ assert(split.length >= 2);
+ let process = split.shift();
+ let path = split.join();
+ reports.push(r.toJSON(process, path, r._amount));
+ for (let i = 1; i < r._nMerged; i++) {
+ reports.push(r.toJSON(process, path, 0));
+ }
+ }
+ }
+
+ return reports;
+}
+
+// Diff two JSON objects holding memory reports.
+function diffJSONObjects(aJson1, aJson2)
+{
+ function simpleProp(aProp)
+ {
+ assert(aJson1[aProp] !== undefined && aJson1[aProp] === aJson2[aProp],
+ aProp + " properties don't match");
+ return aJson1[aProp];
+ }
+
+ return {
+ version: simpleProp("version"),
+
+ hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"),
+
+ reports: makeJSONReports(diffDReportMaps(makeDReportMap(aJson1.reports),
+ makeDReportMap(aJson2.reports)))
+ };
+}
+
+// ---------------------------------------------------------------------------
+
+// |PColl| is short for "process collection".
+function PColl()
+{
+ this._trees = {};
+ this._degenerates = {};
+ this._heapTotal = 0;
+}
+
+/**
+ * Processes reports (whether from reporters or from a file) and append the
+ * main part of the page.
+ *
+ * @param aProcessReports
+ * Function that extracts the memory reports from the reporters or from
+ * file.
+ * @param aHasMozMallocUsableSize
+ * Boolean indicating if moz_malloc_usable_size works.
+ */
+function appendAboutMemoryMain(aProcessReports, aHasMozMallocUsableSize)
+{
+ let pcollsByProcess = {};
+
+ function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
+ aDescription, aPresence)
+ {
+ if (aUnsafePath.startsWith("explicit/")) {
+ assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP,
+ "bad explicit kind");
+ assertInput(aUnits === UNITS_BYTES, "bad explicit units");
+ }
+
+ assert(aPresence === undefined ||
+ aPresence == DReport.PRESENT_IN_FIRST_ONLY ||
+ aPresence == DReport.PRESENT_IN_SECOND_ONLY,
+ "bad presence");
+
+ let process = aProcess === "" ? gUnnamedProcessStr : aProcess;
+ let unsafeNames = aUnsafePath.split('/');
+ let unsafeName0 = unsafeNames[0];
+ let isDegenerate = unsafeNames.length === 1;
+
+ // Get the PColl table for the process, creating it if necessary.
+ let pcoll = pcollsByProcess[process];
+ if (!pcollsByProcess[process]) {
+ pcoll = pcollsByProcess[process] = new PColl();
+ }
+
+ // Get the root node, creating it if necessary.
+ let psubcoll = isDegenerate ? pcoll._degenerates : pcoll._trees;
+ let t = psubcoll[unsafeName0];
+ if (!t) {
+ t = psubcoll[unsafeName0] =
+ new TreeNode(unsafeName0, aUnits, isDegenerate);
+ }
+
+ if (!isDegenerate) {
+ // Add any missing nodes in the tree implied by aUnsafePath, and fill in
+ // the properties that we can with a top-down traversal.
+ for (let i = 1; i < unsafeNames.length; i++) {
+ let unsafeName = unsafeNames[i];
+ let u = t.findKid(unsafeName);
+ if (!u) {
+ u = new TreeNode(unsafeName, aUnits, isDegenerate);
+ if (!t._kids) {
+ t._kids = [];
+ }
+ t._kids.push(u);
+ }
+ t = u;
+ }
+
+ // Update the heap total if necessary.
+ if (unsafeName0 === "explicit" && aKind == KIND_HEAP) {
+ pcollsByProcess[process]._heapTotal += aAmount;
+ }
+ }
+
+ if (t._amount) {
+ // Duplicate! Sum the values and mark it as a dup.
+ t._amount += aAmount;
+ t._nMerged = t._nMerged ? t._nMerged + 1 : 2;
+ assert(t._presence === aPresence, "presence mismatch");
+ } else {
+ // New leaf node. Fill in extra node details from the report.
+ t._amount = aAmount;
+ t._description = aDescription;
+ if (aPresence !== undefined) {
+ t._presence = aPresence;
+ }
+ }
+ }
+
+ function displayReports()
+ {
+ // Sort the processes.
+ let processes = Object.keys(pcollsByProcess);
+ processes.sort(function(aProcessA, aProcessB) {
+ assert(aProcessA != aProcessB,
+ "Elements of Object.keys() should be unique, but " +
+ "saw duplicate '" + aProcessA + "' elem.");
+
+ // Always put the main process first.
+ if (aProcessA == gUnnamedProcessStr) {
+ return -1;
+ }
+ if (aProcessB == gUnnamedProcessStr) {
+ return 1;
+ }
+
+ // Then sort by resident size.
+ let nodeA = pcollsByProcess[aProcessA]._degenerates['resident'];
+ let nodeB = pcollsByProcess[aProcessB]._degenerates['resident'];
+ let residentA = nodeA ? nodeA._amount : -1;
+ let residentB = nodeB ? nodeB._amount : -1;
+
+ if (residentA > residentB) {
+ return -1;
+ }
+ if (residentA < residentB) {
+ return 1;
+ }
+
+ // Then sort by process name.
+ if (aProcessA < aProcessB) {
+ return -1;
+ }
+ if (aProcessA > aProcessB) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ // Generate output for each process.
+ for (let i = 0; i < processes.length; i++) {
+ let process = processes[i];
+ let section = appendElement(gMain, 'div', 'section');
+
+ appendProcessAboutMemoryElements(section, i, process,
+ pcollsByProcess[process]._trees,
+ pcollsByProcess[process]._degenerates,
+ pcollsByProcess[process]._heapTotal,
+ aHasMozMallocUsableSize);
+ }
+ }
+
+ aProcessReports(handleReport, displayReports);
+}
+
+// ---------------------------------------------------------------------------
+
+// There are two kinds of TreeNode.
+// - Leaf TreeNodes correspond to reports.
+// - Non-leaf TreeNodes are just scaffolding nodes for the tree; their values
+// are derived from their children.
+// Some trees are "degenerate", i.e. they contain a single node, i.e. they
+// correspond to a report whose path has no '/' separators.
+function TreeNode(aUnsafeName, aUnits, aIsDegenerate)
+{
+ this._units = aUnits;
+ this._unsafeName = aUnsafeName;
+ if (aIsDegenerate) {
+ this._isDegenerate = true;
+ }
+
+ // Leaf TreeNodes have these properties added immediately after construction:
+ // - _amount
+ // - _description
+ // - _nMerged (only defined if > 1)
+ // - _presence (only defined if value is PRESENT_IN_{FIRST,SECOND}_ONLY)
+ //
+ // Non-leaf TreeNodes have these properties added later:
+ // - _kids
+ // - _amount
+ // - _description
+ // - _hideKids (only defined if true)
+ // - _maxAbsDescendant (on-demand, only when gIsDiff is set)
+}
+
+TreeNode.prototype = {
+ findKid: function(aUnsafeName) {
+ if (this._kids) {
+ for (let i = 0; i < this._kids.length; i++) {
+ if (this._kids[i]._unsafeName === aUnsafeName) {
+ return this._kids[i];
+ }
+ }
+ }
+ return undefined;
+ },
+
+ // When gIsDiff is false, tree operations -- sorting and determining if a
+ // sub-tree is significant -- are straightforward. But when gIsDiff is true,
+ // the combination of positive and negative values within a tree complicates
+ // things. So for a non-leaf node, instead of just looking at _amount, we
+ // instead look at the maximum absolute value of the node and all of its
+ // descendants.
+ maxAbsDescendant: function() {
+ if (!this._kids) {
+ // No kids? Just return the absolute value of the amount.
+ return Math.abs(this._amount);
+ }
+
+ if ('_maxAbsDescendant' in this) {
+ // We've computed this before? Return the saved value.
+ return this._maxAbsDescendant;
+ }
+
+ // Compute the maximum absolute value of all descendants.
+ let max = Math.abs(this._amount);
+ for (let i = 0; i < this._kids.length; i++) {
+ max = Math.max(max, this._kids[i].maxAbsDescendant());
+ }
+ this._maxAbsDescendant = max;
+ return max;
+ },
+
+ toString: function() {
+ switch (this._units) {
+ case UNITS_BYTES: return formatBytes(this._amount);
+ case UNITS_COUNT:
+ case UNITS_COUNT_CUMULATIVE: return formatInt(this._amount);
+ case UNITS_PERCENTAGE: return formatPercentage(this._amount);
+ default:
+ throw "Invalid memory report(s): bad units in TreeNode.toString";
+ }
+ }
+};
+
+// Sort TreeNodes first by size, then by name. The latter is important for the
+// about:memory tests, which need a predictable ordering of reporters which
+// have the same amount.
+TreeNode.compareAmounts = function(aA, aB) {
+ let a, b;
+ if (gIsDiff) {
+ a = aA.maxAbsDescendant();
+ b = aB.maxAbsDescendant();
+ } else {
+ a = aA._amount;
+ b = aB._amount;
+ }
+ if (a > b) {
+ return -1;
+ }
+ if (a < b) {
+ return 1;
+ }
+ return TreeNode.compareUnsafeNames(aA, aB);
+};
+
+TreeNode.compareUnsafeNames = function(aA, aB) {
+ return aA._unsafeName.localeCompare(aB._unsafeName);
+};
+
+
+/**
+ * Fill in the remaining properties for the specified tree in a bottom-up
+ * fashion.
+ *
+ * @param aRoot
+ * The tree root.
+ */
+function fillInTree(aRoot)
+{
+ // Fill in the remaining properties bottom-up.
+ function fillInNonLeafNodes(aT)
+ {
+ if (!aT._kids) {
+ // Leaf node. Has already been filled in.
+
+ } else if (aT._kids.length === 1 && aT != aRoot) {
+ // Non-root, non-leaf node with one child. Merge the child with the node
+ // to avoid redundant entries.
+ let kid = aT._kids[0];
+ let kidBytes = fillInNonLeafNodes(kid);
+ aT._unsafeName += '/' + kid._unsafeName;
+ if (kid._kids) {
+ aT._kids = kid._kids;
+ } else {
+ delete aT._kids;
+ }
+ aT._amount = kidBytes;
+ aT._description = kid._description;
+ if (kid._nMerged !== undefined) {
+ aT._nMerged = kid._nMerged
+ }
+ assert(!aT._hideKids && !kid._hideKids, "_hideKids set when merging");
+
+ } else {
+ // Non-leaf node with multiple children. Derive its _amount and
+ // _description entirely from its children...
+ let kidsBytes = 0;
+ for (let i = 0; i < aT._kids.length; i++) {
+ kidsBytes += fillInNonLeafNodes(aT._kids[i]);
+ }
+
+ // ... except in one special case. When diffing two memory report sets,
+ // if one set has a node with children and the other has the same node
+ // but without children -- e.g. the first has "a/b/c" and "a/b/d", but
+ // the second only has "a/b" -- we need to add a fake node "a/b/(fake)"
+ // to the second to make the trees comparable. It's ugly, but it works.
+ if (aT._amount !== undefined &&
+ (aT._presence === DReport.PRESENT_IN_FIRST_ONLY ||
+ aT._presence === DReport.PRESENT_IN_SECOND_ONLY)) {
+ aT._amount += kidsBytes;
+ let fake = new TreeNode('(fake child)', aT._units);
+ fake._presence = DReport.ADDED_FOR_BALANCE;
+ fake._amount = aT._amount - kidsBytes;
+ aT._kids.push(fake);
+ delete aT._presence;
+ } else {
+ assert(aT._amount === undefined,
+ "_amount already set for non-leaf node")
+ aT._amount = kidsBytes;
+ }
+ aT._description = "The sum of all entries below this one.";
+ }
+ return aT._amount;
+ }
+
+ // cannotMerge is set because don't want to merge into a tree's root node.
+ fillInNonLeafNodes(aRoot);
+}
+
+/**
+ * Compute the "heap-unclassified" value and insert it into the "explicit"
+ * tree.
+ *
+ * @param aT
+ * The "explicit" tree.
+ * @param aHeapAllocatedNode
+ * The "heap-allocated" tree node.
+ * @param aHeapTotal
+ * The sum of all explicit HEAP reports for this process.
+ * @return A boolean indicating if "heap-allocated" is known for the process.
+ */
+function addHeapUnclassifiedNode(aT, aHeapAllocatedNode, aHeapTotal)
+{
+ if (aHeapAllocatedNode === undefined)
+ return false;
+
+ if (aT.findKid("heap-unclassified")) {
+ // heap-unclassified was already calculated, there's nothing left to do.
+ // This can happen when memory reports are exported from areweslimyet.com.
+ return true;
+ }
+
+ assert(aHeapAllocatedNode._isDegenerate, "heap-allocated is not degenerate");
+ let heapAllocatedBytes = aHeapAllocatedNode._amount;
+ let heapUnclassifiedT = new TreeNode("heap-unclassified", UNITS_BYTES);
+ heapUnclassifiedT._amount = heapAllocatedBytes - aHeapTotal;
+ heapUnclassifiedT._description =
+ "Memory not classified by a more specific report. This includes " +
+ "slop bytes due to internal fragmentation in the heap allocator " +
+ "(caused when the allocator rounds up request sizes).";
+ aT._kids.push(heapUnclassifiedT);
+ aT._amount += heapUnclassifiedT._amount;
+ return true;
+}
+
+/**
+ * Sort all kid nodes from largest to smallest, and insert aggregate nodes
+ * where appropriate.
+ *
+ * @param aTotalBytes
+ * The size of the tree's root node.
+ * @param aT
+ * The tree.
+ */
+function sortTreeAndInsertAggregateNodes(aTotalBytes, aT)
+{
+ const kSignificanceThresholdPerc = 1;
+
+ function isInsignificant(aT)
+ {
+ if (gVerbose.checked)
+ return false;
+
+ let perc = gIsDiff
+ ? 100 * aT.maxAbsDescendant() / Math.abs(aTotalBytes)
+ : 100 * aT._amount / aTotalBytes;
+ return perc < kSignificanceThresholdPerc;
+ }
+
+ if (!aT._kids) {
+ return;
+ }
+
+ aT._kids.sort(TreeNode.compareAmounts);
+
+ // If the first child is insignificant, they all are, and there's no point
+ // creating an aggregate node that lacks siblings. Just set the parent's
+ // _hideKids property and process all children.
+ if (isInsignificant(aT._kids[0])) {
+ aT._hideKids = true;
+ for (let i = 0; i < aT._kids.length; i++) {
+ sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
+ }
+ return;
+ }
+
+ // Look at all children except the last one.
+ let i;
+ for (i = 0; i < aT._kids.length - 1; i++) {
+ if (isInsignificant(aT._kids[i])) {
+ // This child is below the significance threshold. If there are other
+ // (smaller) children remaining, move them under an aggregate node.
+ let i0 = i;
+ let nAgg = aT._kids.length - i0;
+ // Create an aggregate node. Inherit units from the parent; everything
+ // in the tree should have the same units anyway (we test this later).
+ let aggT = new TreeNode("(" + nAgg + " tiny)", aT._units);
+ aggT._kids = [];
+ let aggBytes = 0;
+ for ( ; i < aT._kids.length; i++) {
+ aggBytes += aT._kids[i]._amount;
+ aggT._kids.push(aT._kids[i]);
+ }
+ aggT._hideKids = true;
+ aggT._amount = aggBytes;
+ aggT._description =
+ nAgg + " sub-trees that are below the " + kSignificanceThresholdPerc +
+ "% significance threshold.";
+ aT._kids.splice(i0, nAgg, aggT);
+ aT._kids.sort(TreeNode.compareAmounts);
+
+ // Process the moved children.
+ for (i = 0; i < aggT._kids.length; i++) {
+ sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]);
+ }
+ return;
+ }
+
+ sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
+ }
+
+ // The first n-1 children were significant. Don't consider if the last child
+ // is significant; there's no point creating an aggregate node that only has
+ // one child. Just process it.
+ sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
+}
+
+// Global variable indicating if we've seen any invalid values for this
+// process; it holds the unsafePaths of any such reports. It is reset for
+// each new process.
+var gUnsafePathsWithInvalidValuesForThisProcess = [];
+
+function appendWarningElements(aP, aHasKnownHeapAllocated,
+ aHasMozMallocUsableSize)
+{
+ if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) {
+ appendElementWithText(aP, "p", "",
+ "WARNING: the 'heap-allocated' memory reporter and the " +
+ "moz_malloc_usable_size() function do not work for this platform " +
+ "and/or configuration. This means that 'heap-unclassified' is not " +
+ "shown and the 'explicit' tree shows much less memory than it should.\n\n");
+
+ } else if (!aHasKnownHeapAllocated) {
+ appendElementWithText(aP, "p", "",
+ "WARNING: the 'heap-allocated' memory reporter does not work for this " +
+ "platform and/or configuration. This means that 'heap-unclassified' " +
+ "is not shown and the 'explicit' tree shows less memory than it should.\n\n");
+
+ } else if (!aHasMozMallocUsableSize) {
+ appendElementWithText(aP, "p", "",
+ "WARNING: the moz_malloc_usable_size() function does not work for " +
+ "this platform and/or configuration. This means that much of the " +
+ "heap-allocated memory is not measured by individual memory reporters " +
+ "and so will fall under 'heap-unclassified'.\n\n");
+ }
+
+ if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) {
+ let div = appendElement(aP, "div");
+ appendElementWithText(div, "p", "",
+ "WARNING: the following values are negative or unreasonably large.\n");
+
+ let ul = appendElement(div, "ul");
+ for (let i = 0;
+ i < gUnsafePathsWithInvalidValuesForThisProcess.length;
+ i++)
+ {
+ appendTextNode(ul, " ");
+ appendElementWithText(ul, "li", "",
+ flipBackslashes(gUnsafePathsWithInvalidValuesForThisProcess[i]) + "\n");
+ }
+
+ appendElementWithText(div, "p", "",
+ "This indicates a defect in one or more memory reporters. The " +
+ "invalid values are highlighted.\n\n");
+ gUnsafePathsWithInvalidValuesForThisProcess = []; // reset for the next process
+ }
+}
+
+/**
+ * Appends the about:memory elements for a single process.
+ *
+ * @param aP
+ * The parent DOM node.
+ * @param aN
+ * The number of the process, starting at 0.
+ * @param aProcess
+ * The name of the process.
+ * @param aTrees
+ * The table of non-degenerate trees for this process.
+ * @param aDegenerates
+ * The table of degenerate trees for this process.
+ * @param aHasMozMallocUsableSize
+ * Boolean indicating if moz_malloc_usable_size works.
+ * @return The generated text.
+ */
+function appendProcessAboutMemoryElements(aP, aN, aProcess, aTrees,
+ aDegenerates, aHeapTotal,
+ aHasMozMallocUsableSize)
+{
+ const kUpwardsArrow = "\u2191",
+ kDownwardsArrow = "\u2193";
+
+ let appendLink = function(aHere, aThere, aArrow) {
+ let link = appendElementWithText(aP, "a", "upDownArrow", aArrow);
+ link.href = "#" + aThere + aN;
+ link.id = aHere + aN;
+ link.title = "Go to the " + aThere + " of " + aProcess;
+ link.style = "text-decoration: none";
+
+ // This jumps to the anchor without the page location getting the anchor
+ // name tacked onto its end, which is what happens with a vanilla link.
+ link.addEventListener("click", function(event) {
+ document.documentElement.scrollTop =
+ document.querySelector(event.target.href).offsetTop;
+ event.preventDefault();
+ }, false);
+
+ // This gives nice spacing when we copy and paste.
+ appendElementWithText(aP, "span", "", "\n");
+ }
+
+ appendElementWithText(aP, "h1", "", aProcess);
+ appendLink("start", "end", kDownwardsArrow);
+
+ // We'll fill this in later.
+ let warningsDiv = appendElement(aP, "div", "accuracyWarning");
+
+ // The explicit tree.
+ let hasExplicitTree;
+ let hasKnownHeapAllocated;
+ {
+ let treeName = "explicit";
+ let t = aTrees[treeName];
+ if (t) {
+ let pre = appendSectionHeader(aP, "Explicit Allocations");
+ hasExplicitTree = true;
+ fillInTree(t);
+ // Using the "heap-allocated" reporter here instead of
+ // nsMemoryReporterManager.heapAllocated goes against the usual pattern.
+ // But the "heap-allocated" node will go in the tree like the others, so
+ // we have to deal with it, and once we're dealing with it, it's easier
+ // to keep doing so rather than switching to the distinguished amount.
+ hasKnownHeapAllocated =
+ aDegenerates &&
+ addHeapUnclassifiedNode(t, aDegenerates["heap-allocated"], aHeapTotal);
+ sortTreeAndInsertAggregateNodes(t._amount, t);
+ t._description = explicitTreeDescription;
+ appendTreeElements(pre, t, aProcess, "");
+ delete aTrees[treeName];
+ }
+ appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
+ }
+
+ // Fill in and sort all the non-degenerate other trees.
+ let otherTrees = [];
+ for (let unsafeName in aTrees) {
+ let t = aTrees[unsafeName];
+ assert(!t._isDegenerate, "tree is degenerate");
+ fillInTree(t);
+ sortTreeAndInsertAggregateNodes(t._amount, t);
+ otherTrees.push(t);
+ }
+ otherTrees.sort(TreeNode.compareUnsafeNames);
+
+ // Get the length of the longest root value among the degenerate other trees,
+ // and sort them as well.
+ let otherDegenerates = [];
+ let maxStringLength = 0;
+ for (let unsafeName in aDegenerates) {
+ let t = aDegenerates[unsafeName];
+ assert(t._isDegenerate, "tree is not degenerate");
+ let length = t.toString().length;
+ if (length > maxStringLength) {
+ maxStringLength = length;
+ }
+ otherDegenerates.push(t);
+ }
+ otherDegenerates.sort(TreeNode.compareUnsafeNames);
+
+ // Now generate the elements, putting non-degenerate trees first.
+ let pre = appendSectionHeader(aP, "Other Measurements");
+ for (let i = 0; i < otherTrees.length; i++) {
+ let t = otherTrees[i];
+ appendTreeElements(pre, t, aProcess, "");
+ appendTextNode(pre, "\n"); // blank lines after non-degenerate trees
+ }
+ for (let i = 0; i < otherDegenerates.length; i++) {
+ let t = otherDegenerates[i];
+ let padText = pad("", maxStringLength - t.toString().length, ' ');
+ appendTreeElements(pre, t, aProcess, padText);
+ }
+ appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
+
+ // Add any warnings about inaccuracies in the "explicit" tree due to platform
+ // limitations. These must be computed after generating all the text. The
+ // newlines give nice spacing if we copy+paste into a text buffer.
+ if (hasExplicitTree) {
+ appendWarningElements(warningsDiv, hasKnownHeapAllocated,
+ aHasMozMallocUsableSize);
+ }
+
+ appendElementWithText(aP, "h3", "", "End of " + aProcess);
+ appendLink("end", "start", kUpwardsArrow);
+}
+
+/**
+ * Determines if a number has a negative sign when converted to a string.
+ * Works even for -0.
+ *
+ * @param aN
+ * The number.
+ * @return A boolean.
+ */
+function hasNegativeSign(aN)
+{
+ if (aN === 0) { // this succeeds for 0 and -0
+ return 1 / aN === -Infinity; // this succeeds for -0
+ }
+ return aN < 0;
+}
+
+/**
+ * Formats an int as a human-readable string.
+ *
+ * @param aN
+ * The integer to format.
+ * @param aExtra
+ * An extra string to tack onto the end.
+ * @return A human-readable string representing the int.
+ *
+ * Note: building an array of chars and converting that to a string with
+ * Array.join at the end is more memory efficient than using string
+ * concatenation. See bug 722972 for details.
+ */
+function formatInt(aN, aExtra)
+{
+ let neg = false;
+ if (hasNegativeSign(aN)) {
+ neg = true;
+ aN = -aN;
+ }
+ let s = [];
+ while (true) {
+ let k = aN % 1000;
+ aN = Math.floor(aN / 1000);
+ if (aN > 0) {
+ if (k < 10) {
+ s.unshift(",00", k);
+ } else if (k < 100) {
+ s.unshift(",0", k);
+ } else {
+ s.unshift(",", k);
+ }
+ } else {
+ s.unshift(k);
+ break;
+ }
+ }
+ if (neg) {
+ s.unshift("-");
+ }
+ if (aExtra) {
+ s.push(aExtra);
+ }
+ return s.join("");
+}
+
+/**
+ * Converts a byte count to an appropriate string representation.
+ *
+ * @param aBytes
+ * The byte count.
+ * @return The string representation.
+ */
+function formatBytes(aBytes)
+{
+ let unit = gVerbose.checked ? " B" : " MB";
+
+ let s;
+ if (gVerbose.checked) {
+ s = formatInt(aBytes, unit);
+ } else {
+ let mbytes = (aBytes / (1024 * 1024)).toFixed(2);
+ let a = String(mbytes).split(".");
+ // If the argument to formatInt() is -0, it will print the negative sign.
+ s = formatInt(Number(a[0])) + "." + a[1] + unit;
+ }
+ return s;
+}
+
+/**
+ * Converts a percentage to an appropriate string representation.
+ *
+ * @param aPerc100x
+ * The percentage, multiplied by 100 (see nsIMemoryReporter).
+ * @return The string representation
+ */
+function formatPercentage(aPerc100x)
+{
+ return (aPerc100x / 100).toFixed(2) + "%";
+}
+
+/**
+ * Right-justifies a string in a field of a given width, padding as necessary.
+ *
+ * @param aS
+ * The string.
+ * @param aN
+ * The field width.
+ * @param aC
+ * The char used to pad.
+ * @return The string representation.
+ */
+function pad(aS, aN, aC)
+{
+ let padding = "";
+ let n2 = aN - aS.length;
+ for (let i = 0; i < n2; i++) {
+ padding += aC;
+ }
+ return padding + aS;
+}
+
+// There's a subset of the Unicode "light" box-drawing chars that is widely
+// implemented in terminals, and this code sticks to that subset to maximize
+// the chance that copying and pasting about:memory output to a terminal will
+// work correctly.
+const kHorizontal = "\u2500",
+ kVertical = "\u2502",
+ kUpAndRight = "\u2514",
+ kUpAndRight_Right_Right = "\u2514\u2500\u2500",
+ kVerticalAndRight = "\u251c",
+ kVerticalAndRight_Right_Right = "\u251c\u2500\u2500",
+ kVertical_Space_Space = "\u2502 ";
+
+const kNoKidsSep = " \u2500\u2500 ",
+ kHideKidsSep = " ++ ",
+ kShowKidsSep = " -- ";
+
+function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged,
+ aPresence)
+{
+ let safeName = flipBackslashes(aUnsafeName);
+ if (!aIsInvalid && !aNMerged && !aPresence) {
+ safeName += "\n";
+ }
+ let nameSpan = appendElementWithText(aP, "span", "mrName", safeName);
+ nameSpan.title = aDescription;
+
+ if (aIsInvalid) {
+ let noteText = " [?!]";
+ if (!aNMerged) {
+ noteText += "\n";
+ }
+ let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
+ noteSpan.title =
+ "Warning: this value is invalid and indicates a bug in one or more " +
+ "memory reporters. ";
+ }
+
+ if (aNMerged) {
+ let noteText = " [" + aNMerged + "]";
+ if (!aPresence) {
+ noteText += "\n";
+ }
+ let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
+ noteSpan.title =
+ "This value is the sum of " + aNMerged +
+ " memory reports that all have the same path.";
+ }
+
+ if (aPresence) {
+ let c, title;
+ switch (aPresence) {
+ case DReport.PRESENT_IN_FIRST_ONLY:
+ c = '-';
+ title = "This value was only present in the first set of memory reports.";
+ break;
+ case DReport.PRESENT_IN_SECOND_ONLY:
+ c = '+';
+ title = "This value was only present in the second set of memory reports.";
+ break;
+ case DReport.ADDED_FOR_BALANCE:
+ c = '!';
+ title = "One of the sets of memory reports lacked children for this " +
+ "node's parent. This is a fake child node added to make the " +
+ "two memory sets comparable.";
+ break;
+ default: assert(false, "bad presence");
+ break;
+ }
+ let noteSpan = appendElementWithText(aP, "span", "mrNote",
+ " [" + c + "]\n");
+ noteSpan.title = title;
+ }
+}
+
+// This is used to record the (safe) IDs of which sub-trees have been manually
+// expanded (marked as true) and collapsed (marked as false). It's used to
+// replicate the collapsed/expanded state when the page is updated. It can end
+// up holding IDs of nodes that no longer exist, e.g. for compartments that
+// have been closed. This doesn't seem like a big deal, because the number is
+// limited by the number of entries the user has changed from their original
+// state.
+var gShowSubtreesBySafeTreeId = {};
+
+function assertClassListContains(e, className) {
+ assert(e, "undefined " + className);
+ assert(e.classList.contains(className), "classname isn't " + className);
+}
+
+function toggle(aEvent)
+{
+ // This relies on each line being a span that contains at least four spans:
+ // mrValue, mrPerc, mrSep, mrName, and then zero or more mrNotes. All
+ // whitespace must be within one of these spans for this function to find the
+ // right nodes. And the span containing the children of this line must
+ // immediately follow. Assertions check this.
+
+ // |aEvent.target| will be one of the spans. Get the outer span.
+ let outerSpan = aEvent.target.parentNode;
+ assertClassListContains(outerSpan, "hasKids");
+
+ // Toggle the '++'/'--' separator.
+ let isExpansion;
+ let sepSpan = outerSpan.childNodes[2];
+ assertClassListContains(sepSpan, "mrSep");
+ if (sepSpan.textContent === kHideKidsSep) {
+ isExpansion = true;
+ sepSpan.textContent = kShowKidsSep;
+ } else if (sepSpan.textContent === kShowKidsSep) {
+ isExpansion = false;
+ sepSpan.textContent = kHideKidsSep;
+ } else {
+ assert(false, "bad sepSpan textContent");
+ }
+
+ // Toggle visibility of the span containing this node's children.
+ let subTreeSpan = outerSpan.nextSibling;
+ assertClassListContains(subTreeSpan, "kids");
+ subTreeSpan.classList.toggle("hidden");
+
+ // Record/unrecord that this sub-tree was toggled.
+ let safeTreeId = outerSpan.id;
+ if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
+ delete gShowSubtreesBySafeTreeId[safeTreeId];
+ } else {
+ gShowSubtreesBySafeTreeId[safeTreeId] = isExpansion;
+ }
+}
+
+function expandPathToThisElement(aElement)
+{
+ if (aElement.classList.contains("kids")) {
+ // Unhide the kids.
+ aElement.classList.remove("hidden");
+ expandPathToThisElement(aElement.previousSibling); // hasKids
+
+ } else if (aElement.classList.contains("hasKids")) {
+ // Change the separator to '--'.
+ let sepSpan = aElement.childNodes[2];
+ assertClassListContains(sepSpan, "mrSep");
+ sepSpan.textContent = kShowKidsSep;
+ expandPathToThisElement(aElement.parentNode); // kids or pre.entries
+
+ } else {
+ assertClassListContains(aElement, "entries");
+ }
+}
+
+/**
+ * Appends the elements for the tree, including its heading.
+ *
+ * @param aP
+ * The parent DOM node.
+ * @param aRoot
+ * The tree root.
+ * @param aProcess
+ * The process the tree corresponds to.
+ * @param aPadText
+ * A string to pad the start of each entry.
+ */
+function appendTreeElements(aP, aRoot, aProcess, aPadText)
+{
+ /**
+ * Appends the elements for a particular tree, without a heading.
+ *
+ * @param aP
+ * The parent DOM node.
+ * @param aProcess
+ * The process the tree corresponds to.
+ * @param aUnsafeNames
+ * An array of the names forming the path to aT.
+ * @param aRoot
+ * The root of the tree this sub-tree belongs to.
+ * @param aT
+ * The tree.
+ * @param aTreelineText1
+ * The first part of the treeline for this entry and this entry's
+ * children.
+ * @param aTreelineText2a
+ * The second part of the treeline for this entry.
+ * @param aTreelineText2b
+ * The second part of the treeline for this entry's children.
+ * @param aParentStringLength
+ * The length of the formatted byte count of the top node in the tree.
+ */
+ function appendTreeElements2(aP, aProcess, aUnsafeNames, aRoot, aT,
+ aTreelineText1, aTreelineText2a,
+ aTreelineText2b, aParentStringLength)
+ {
+ function appendN(aS, aC, aN)
+ {
+ for (let i = 0; i < aN; i++) {
+ aS += aC;
+ }
+ return aS;
+ }
+
+ // The tree line. Indent more if this entry is narrower than its parent.
+ let valueText = aT.toString();
+ let extraTreelineLength =
+ Math.max(aParentStringLength - valueText.length, 0);
+ if (extraTreelineLength > 0) {
+ aTreelineText2a =
+ appendN(aTreelineText2a, kHorizontal, extraTreelineLength);
+ aTreelineText2b =
+ appendN(aTreelineText2b, " ", extraTreelineLength);
+ }
+ let treelineText = aTreelineText1 + aTreelineText2a;
+ appendElementWithText(aP, "span", "treeline", treelineText);
+
+ // Detect and record invalid values. But not if gIsDiff is true, because
+ // we expect negative values in that case.
+ assertInput(aRoot._units === aT._units,
+ "units within a tree are inconsistent");
+ let tIsInvalid = false;
+ if (!gIsDiff && !(0 <= aT._amount && aT._amount <= aRoot._amount)) {
+ tIsInvalid = true;
+ let unsafePath = aUnsafeNames.join("/");
+ gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath);
+ reportAssertionFailure("Invalid value (" + aT._amount + " / " +
+ aRoot._amount + ") for " +
+ flipBackslashes(unsafePath));
+ }
+
+ // For non-leaf nodes, the entire sub-tree is put within a span so it can
+ // be collapsed if the node is clicked on.
+ let d;
+ let sep;
+ let showSubtrees;
+ if (aT._kids) {
+ // Determine if we should show the sub-tree below this entry; this
+ // involves reinstating any previous toggling of the sub-tree.
+ let unsafePath = aUnsafeNames.join("/");
+ let safeTreeId = aProcess + ":" + flipBackslashes(unsafePath);
+ showSubtrees = !aT._hideKids;
+ if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
+ showSubtrees = gShowSubtreesBySafeTreeId[safeTreeId];
+ }
+ d = appendElement(aP, "span", "hasKids");
+ d.id = safeTreeId;
+ d.onclick = toggle;
+ sep = showSubtrees ? kShowKidsSep : kHideKidsSep;
+ } else {
+ assert(!aT._hideKids, "leaf node with _hideKids set")
+ sep = kNoKidsSep;
+ d = aP;
+ }
+
+ // The value.
+ appendElementWithText(d, "span", "mrValue" + (tIsInvalid ? " invalid" : ""),
+ valueText);
+
+ // The percentage (omitted for single entries).
+ let percText;
+ if (!aT._isDegenerate) {
+ // Treat 0 / 0 as 100%.
+ let num = aRoot._amount === 0 ? 100 : (100 * aT._amount / aRoot._amount);
+ let numText = num.toFixed(2);
+ percText = numText === "100.00"
+ ? " (100.0%)"
+ : (0 <= num && num < 10 ? " (0" : " (") + numText + "%)";
+ appendElementWithText(d, "span", "mrPerc", percText);
+ }
+
+ // The separator.
+ appendElementWithText(d, "span", "mrSep", sep);
+
+ // The entry's name.
+ appendMrNameSpan(d, aT._description, aT._unsafeName,
+ tIsInvalid, aT._nMerged, aT._presence);
+
+ // In non-verbose mode, invalid nodes can be hidden in collapsed sub-trees.
+ // But it's good to always see them, so force this.
+ if (!gVerbose.checked && tIsInvalid) {
+ expandPathToThisElement(d);
+ }
+
+ // Recurse over children.
+ if (aT._kids) {
+ // The 'kids' class is just used for sanity checking in toggle().
+ d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden");
+
+ let kidTreelineText1 = aTreelineText1 + aTreelineText2b;
+ for (let i = 0; i < aT._kids.length; i++) {
+ let kidTreelineText2a, kidTreelineText2b;
+ if (i < aT._kids.length - 1) {
+ kidTreelineText2a = kVerticalAndRight_Right_Right;
+ kidTreelineText2b = kVertical_Space_Space;
+ } else {
+ kidTreelineText2a = kUpAndRight_Right_Right;
+ kidTreelineText2b = " ";
+ }
+ aUnsafeNames.push(aT._kids[i]._unsafeName);
+ appendTreeElements2(d, aProcess, aUnsafeNames, aRoot, aT._kids[i],
+ kidTreelineText1, kidTreelineText2a,
+ kidTreelineText2b, valueText.length);
+ aUnsafeNames.pop();
+ }
+ }
+ }
+
+ let rootStringLength = aRoot.toString().length;
+ appendTreeElements2(aP, aProcess, [aRoot._unsafeName], aRoot, aRoot,
+ aPadText, "", "", rootStringLength);
+}
+
+// ---------------------------------------------------------------------------
+
+function appendSectionHeader(aP, aText)
+{
+ appendElementWithText(aP, "h2", "", aText + "\n");
+ return appendElement(aP, "pre", "entries");
+}
+
+// ---------------------------------------------------------------------------
+
+function saveReportsToFile()
+{
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.appendFilter("Zipped JSON files", "*.json.gz");
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.filterIndex = 0;
+ fp.addToRecentDocs = true;
+ fp.defaultString = "memory-report.json.gz";
+
+ let fpFinish = function(file) {
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
+ .getService(Ci.nsIMemoryInfoDumper);
+ let finishDumping = () => {
+ updateMainAndFooter("Saved memory reports to " + file.path, HIDE_FOOTER);
+ }
+ dumper.dumpMemoryReportsToNamedFile(file.path, finishDumping, null,
+ gAnonymize.checked);
+ }
+
+ let fpCallback = function(aResult) {
+ if (aResult == Ci.nsIFilePicker.returnOK ||
+ aResult == Ci.nsIFilePicker.returnReplace) {
+ fpFinish(fp.file);
+ }
+ };
+
+ try {
+ fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave);
+ } catch (ex) {
+ // This will fail on Android, since there is no Save as file picker there.
+ // Just save to the default downloads dir if it does.
+ Downloads.getSystemDownloadsDirectory().then(function(dirPath) {
+ let file = FileUtils.File(dirPath);
+ file.append(fp.defaultString);
+ fpFinish(file);
+ });
+
+ return;
+ }
+ fp.open(fpCallback);
+}
diff --git a/toolkit/components/aboutmemory/content/aboutMemory.xhtml b/toolkit/components/aboutmemory/content/aboutMemory.xhtml
new file mode 100644
index 000000000..e20b3b624
--- /dev/null
+++ b/toolkit/components/aboutmemory/content/aboutMemory.xhtml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta name="viewport" content="width=device-width"/>
+ <link rel="stylesheet" href="chrome://global/skin/aboutMemory.css" type="text/css"/>
+ <script type="text/javascript;version=1.8" src="chrome://global/content/aboutMemory.js"/>
+ </head>
+
+ <body onload="onLoad()" onunload="onUnload()"></body>
+</html>
diff --git a/toolkit/components/aboutmemory/jar.mn b/toolkit/components/aboutmemory/jar.mn
new file mode 100644
index 000000000..0a6b01ed7
--- /dev/null
+++ b/toolkit/components/aboutmemory/jar.mn
@@ -0,0 +1,8 @@
+# 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/.
+
+toolkit.jar:
+ content/global/aboutMemory.js (content/aboutMemory.js)
+ content/global/aboutMemory.xhtml (content/aboutMemory.xhtml)
+ content/global/aboutMemory.css (content/aboutMemory.css)
diff --git a/toolkit/components/aboutmemory/moz.build b/toolkit/components/aboutmemory/moz.build
new file mode 100644
index 000000000..dd3f71d8c
--- /dev/null
+++ b/toolkit/components/aboutmemory/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'about:memory')
diff --git a/toolkit/components/aboutmemory/tests/.eslintrc.js b/toolkit/components/aboutmemory/tests/.eslintrc.js
new file mode 100644
index 000000000..2c669d844
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/aboutmemory/tests/chrome.ini b/toolkit/components/aboutmemory/tests/chrome.ini
new file mode 100644
index 000000000..c25bc30a0
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/chrome.ini
@@ -0,0 +1,28 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ crash-dump-diff1.json
+ crash-dump-diff2.json
+ crash-dump-good.json
+ memory-reports-bad.json
+ memory-reports-diff1.json
+ memory-reports-diff2.json
+ memory-reports-good.json
+ remote.xul
+
+[test_aboutmemory.xul]
+subsuite = clipboard
+[test_aboutmemory2.xul]
+subsuite = clipboard
+[test_aboutmemory3.xul]
+subsuite = clipboard
+[test_aboutmemory4.xul]
+subsuite = clipboard
+[test_aboutmemory5.xul]
+subsuite = clipboard
+skip-if = asan # Bug 1116230
+[test_aboutmemory6.xul]
+[test_memoryReporters.xul]
+[test_memoryReporters2.xul]
+[test_sqliteMultiReporter.xul]
+[test_dumpGCAndCCLogsToFile.xul]
diff --git a/toolkit/components/aboutmemory/tests/crash-dump-diff1.json b/toolkit/components/aboutmemory/tests/crash-dump-diff1.json
new file mode 100644
index 000000000..d41bbcc61
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/crash-dump-diff1.json
@@ -0,0 +1,11 @@
+{
+ "foo": 1,
+ "blah": 2,
+ "memory_report": {
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "Main Process (pid NNN)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144000, "description": "Heap allocated."}
+ ]
+ }
+}
diff --git a/toolkit/components/aboutmemory/tests/crash-dump-diff2.json b/toolkit/components/aboutmemory/tests/crash-dump-diff2.json
new file mode 100644
index 000000000..8f9451f62
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/crash-dump-diff2.json
@@ -0,0 +1,11 @@
+{
+ "foo": 3,
+ "blah": 4,
+ "memory_report": {
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "Main Process (pid NNN)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144001, "description": "Heap allocated."}
+ ]
+ }
+}
diff --git a/toolkit/components/aboutmemory/tests/crash-dump-good.json b/toolkit/components/aboutmemory/tests/crash-dump-good.json
new file mode 100644
index 000000000..6bee54d59
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/crash-dump-good.json
@@ -0,0 +1,14 @@
+{
+ "foo": 1,
+ "blah": 2,
+ "memory_report": {
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "Main Process (pid NNN)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144000, "description": "Heap allocated."},
+ {"process": "Main Process (pid NNN)", "path": "other/b", "kind": 2, "units": 0, "amount": 104857, "description": "Other b."},
+ {"process": "Main Process (pid NNN)", "path": "other/a", "kind": 2, "units": 0, "amount": 209715, "description": "Other a."},
+ {"process": "Main Process (pid NNN)", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 52428800, "description": "A b."}
+ ]
+ }
+}
diff --git a/toolkit/components/aboutmemory/tests/memory-reports-bad.json b/toolkit/components/aboutmemory/tests/memory-reports-bad.json
new file mode 100644
index 000000000..61a2092b1
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/memory-reports-bad.json
@@ -0,0 +1,3 @@
+{
+ "version": 1
+}
diff --git a/toolkit/components/aboutmemory/tests/memory-reports-diff1.json b/toolkit/components/aboutmemory/tests/memory-reports-diff1.json
new file mode 100644
index 000000000..0bfe0b26b
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/memory-reports-diff1.json
@@ -0,0 +1,45 @@
+{
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "P", "path": "explicit/xpcom/category-manager", "kind": 1, "units": 0, "amount": 56848, "description": "Desc."},
+ {"process": "P", "path": "explicit/storage/prefixset/goog-phish-shavar", "kind": 1, "units": 0, "amount": 680000, "description": "Desc."},
+
+ {"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 4, "description": "Desc."},
+ {"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 5, "description": "Desc."},
+
+ {"process": "P", "path": "page-faults-soft", "kind": 2, "units": 2, "amount": 61013, "description": "Desc."},
+
+ {"process": "P", "path": "foobar", "kind": 2, "units": 0, "amount": 100, "description": "Desc."},
+ {"process": "P", "path": "zero1", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
+
+ {"process": "P", "path": "a/b", "kind": 2, "units": 0, "amount": 1000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/d", "kind": 2, "units": 0, "amount": 2000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/e", "kind": 2, "units": 0, "amount": 2000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/f", "kind": 2, "units": 0, "amount": 3000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/g", "kind": 2, "units": 0, "amount": 3000000, "description": "Desc."},
+ {"process": "P", "path": "a/h", "kind": 2, "units": 0, "amount": 1000, "description": "Desc."},
+
+ {"process": "P2 (pid 22)", "path": "p1 (pid 123)", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p2 (blah, pid=123)", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p3/zone(0x1234)/p3", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p4/js-zone(0x1234)/p4", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p5/worker(foo.com, 0x1234)/p5", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "explicit/window-objects/top(bar.com, id=123)/...", "kind": 0, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p6/z-moz-nullprincipal:{85e250f3-57ae-46c4-a11e-4176dd39d9c5}/p6", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p7/js-main-runtime-compartments/system/jar:file:\\\\\\temp_xyz\\firefox\\omni.ja!/p7", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+
+ {"process": "P3", "path": "p3", "kind": 2, "units": 0, "amount": 55, "description": "Desc."},
+
+ {"process": "P5", "path": "p5", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
+
+ {"process": "P7", "path": "p7", "kind": 2, "units": 0, "amount": 5, "description": "Desc."},
+
+ {"process": "P8", "path": "p8/a/b/c/d", "kind": 2, "units": 0, "amount": 3, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/b/c/e", "kind": 2, "units": 0, "amount": 4, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/b/f", "kind": 2, "units": 0, "amount": 5, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/g/h", "kind": 2, "units": 0, "amount": 6, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/g/i", "kind": 2, "units": 0, "amount": 7, "description": "Desc."}
+ ]
+}
+
diff --git a/toolkit/components/aboutmemory/tests/memory-reports-diff2.json b/toolkit/components/aboutmemory/tests/memory-reports-diff2.json
new file mode 100644
index 000000000..e2ef4caa7
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/memory-reports-diff2.json
@@ -0,0 +1,44 @@
+{
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "P", "path": "explicit/xpcom/category-manager", "kind": 1, "units": 0, "amount": 56849, "description": "Desc."},
+ {"process": "P", "path": "explicit/storage/prefixset/goog-phish-shavar", "kind": 1, "units": 0, "amount": 670000, "description": "Desc."},
+
+ {"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 3, "description": "Desc."},
+
+ {"process": "P", "path": "page-faults-soft", "kind": 2, "units": 2, "amount": 61013, "description": "Desc."},
+
+ {"process": "P", "path": "canvas-2d-pixel-bytes", "kind": 2, "units": 0, "amount": 1000, "description": "Desc."},
+ {"process": "P", "path": "canvas-2d-pixel-bytes", "kind": 2, "units": 0, "amount": 2000, "description": "Desc."},
+
+ {"process": "P", "path": "foobaz", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
+
+ {"process": "P", "path": "a/b", "kind": 2, "units": 0, "amount": 2000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/d", "kind": 2, "units": 0, "amount": 2998000, "description": "Desc."},
+ {"process": "P", "path": "a/c/e", "kind": 2, "units": 0, "amount": 1001000, "description": "Desc."},
+ {"process": "P", "path": "a/c/f", "kind": 2, "units": 0, "amount": 3001000, "description": "Desc."},
+ {"process": "P", "path": "a/c/g", "kind": 2, "units": 0, "amount": 3001000, "description": "Desc."},
+ {"process": "P", "path": "a/h", "kind": 2, "units": 0, "amount": 2000, "description": "Desc."},
+
+ {"process": "P2 (pid 22)", "path": "p1 (pid 456)", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p2 (blah, pid=456)", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p3/zone(0x5678)/p3", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p4/js-zone(0x5678)/p4", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p5/worker(foo.com, 0x5678)/p5", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "explicit/window-objects/top(bar.com, id=456)/...", "kind": 0, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p6/z-moz-nullprincipal:{161effaa-c1f7-4010-a08e-e7c9aea01aed}/p6", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p7/js-main-runtime-compartments/system/jar:file:\\\\\\temp_abc\\firefox\\omni.ja!/p7", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+
+ {"process": "P4", "path": "p4", "kind": 2, "units": 0, "amount": 66, "description": "Desc."},
+
+ {"process": "P6", "path": "p6", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
+
+ {"process": "P7", "path": "p7/b", "kind": 2, "units": 0, "amount": 3, "description": "Desc."},
+ {"process": "P7", "path": "p7/c", "kind": 2, "units": 0, "amount": 4, "description": "Desc."},
+
+ {"process": "P8", "path": "p8/a/b", "kind": 2, "units": 0, "amount": 1, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/g", "kind": 2, "units": 0, "amount": 2, "description": "Desc."}
+ ]
+}
+
diff --git a/toolkit/components/aboutmemory/tests/memory-reports-good.json b/toolkit/components/aboutmemory/tests/memory-reports-good.json
new file mode 100644
index 000000000..013b4b125
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/memory-reports-good.json
@@ -0,0 +1,29 @@
+{
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "Main Process (pid NNN)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144000, "description": "Heap allocated."},
+ {"process": "Main Process (pid NNN)", "path": "other/b", "kind": 2, "units": 0, "amount": 104857, "description": "Other b."},
+ {"process": "Main Process (pid NNN)", "path": "other/a", "kind": 2, "units": 0, "amount": 209715, "description": "Other a."},
+ {"process": "Main Process (pid NNN)", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 52428800, "description": "A b."},
+
+ {"process": "Main Process (pid NNN)", "path": "size/a", "kind": 1, "units": 0, "amount": 1024, "description": "non-sentence"},
+ {"process": "Main Process (pid NNN)", "path": "rss/a", "kind": 1, "units": 0, "amount": 1024, "description": "non-sentence"},
+ {"process": "Main Process (pid NNN)", "path": "pss/a", "kind": 1, "units": 0, "amount": 1024, "description": "non-sentence"},
+ {"process": "Main Process (pid NNN)", "path": "swap/a", "kind": 1, "units": 0, "amount": 1024, "description": "non-sentence"},
+ {"process": "Main Process (pid NNN)", "path": "compartments/system/a", "kind": 1, "units": 0, "amount": 1024, "description": ""},
+ {"process": "Main Process (pid NNN)", "path": "ghost-windows/a", "kind": 1, "units": 0, "amount": 1024, "description": ""},
+
+ {"process": "Main Process (pid NNN)", "path": "redundant/should-be-ignored", "kind": 1, "units": 0, "amount": 1024, "description": ""},
+
+ {"process": "Heap-unclassified process", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144000, "description": "Heap allocated."},
+ {"process": "Heap-unclassified process", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 52428800, "description": "A b."},
+ {"process": "Heap-unclassified process", "path": "explicit/heap-unclassified", "kind": 1, "units": 0, "amount": 209715200, "description": "Heap unclassified"},
+
+ {"process": "Explicit-only process", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 100000, "description": "A b."},
+
+ {"process": "Other-only process", "path": "a/b", "kind": 1, "units": 0, "amount": 100000, "description": "A b."},
+ {"process": "Other-only process", "path": "a/c", "kind": 1, "units": 0, "amount": 100000, "description": "A c."},
+ {"process": "Other-only process", "path": "heap-allocated", "kind": 1, "units": 0, "amount": 500000, "description": "D."}
+ ]
+}
diff --git a/toolkit/components/aboutmemory/tests/remote.xul b/toolkit/components/aboutmemory/tests/remote.xul
new file mode 100644
index 000000000..7d6910130
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/remote.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Remote browser"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- test results are displayed in the html:body -->
+ <p>Remote browser</p>
+
+ <browser type="content" src="about:blank" id="remote" remote="true"/>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
new file mode 100644
index 000000000..bfeab4c7b
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
@@ -0,0 +1,602 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file uses fake memory reporters to test the presentation of memory
+ reports in about:memory. test_memoryReporters.xul uses the real
+ memory reporters to test whether the memory reporters are producing
+ sensible results. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ SimpleTest.expectAssertions(27);
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ const Cr = Components.results;
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ // Hide all the real reporters; we'll restore them at the end.
+ mgr.blockRegistrationAndHideExistingReporters();
+
+ // Setup various fake-but-deterministic reporters.
+ const KB = 1024;
+ const MB = KB * KB;
+ const NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
+ const HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+ const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+
+ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+ const COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
+ const COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
+ const PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
+
+ let fakeReporters = [
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aU, aA) {
+ aCbObj.callback("", aP, aK, aU, aA, "Desc.", aClosure);
+ }
+ f("heap-allocated", OTHER, BYTES, 500 * MB);
+ f("heap-unallocated", OTHER, BYTES, 100 * MB);
+ f("explicit/a", HEAP, BYTES, 222 * MB);
+ f("explicit/b/a", HEAP, BYTES, 85 * MB);
+ f("explicit/b/b", HEAP, BYTES, 75 * MB);
+ f("explicit/b/c/a", HEAP, BYTES, 70 * MB);
+ f("explicit/b/c/b", HEAP, BYTES, 2 * MB); // omitted
+ f("explicit/g/a", HEAP, BYTES, 6 * MB);
+ f("explicit/g/b", HEAP, BYTES, 5 * MB);
+ f("explicit/g/other", HEAP, BYTES, 4 * MB);
+ // A degenerate tree with the same name as a non-degenerate tree should
+ // work ok.
+ f("explicit", OTHER, BYTES, 888 * MB);
+ f("other1/a/b", OTHER, BYTES, 111 * MB);
+ f("other1/c/d", OTHER, BYTES, 22 * MB);
+ f("other1/c/e", OTHER, BYTES, 33 * MB);
+ f("other4", OTHER, COUNT_CUMULATIVE, 777);
+ f("other4", OTHER, COUNT_CUMULATIVE, 111);
+ f("other3/a/b/c/d/e", OTHER, PERCENTAGE, 2000);
+ f("other3/a/b/c/d/f", OTHER, PERCENTAGE, 10);
+ f("other3/a/b/c/d/g", OTHER, PERCENTAGE, 5);
+ f("other3/a/b/c/d/g", OTHER, PERCENTAGE, 5);
+ // Check that a rounded-up-to-100.00% value is shown as "100.0%" (i.e. one
+ // decimal point).
+ f("other6/big", OTHER, COUNT, 99999);
+ f("other6/small", OTHER, COUNT, 1);
+ // Check that a 0 / 0 is handled correctly.
+ f("other7/zero", OTHER, BYTES, 0);
+ // These compartments ones shouldn't be displayed.
+ f("compartments/user/foo", OTHER, COUNT, 1);
+ f("compartments/system/foo", OTHER, COUNT, 1);
+ }
+ },
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aU, aA) {
+ aCbObj.callback("", aP, aK, aU, aA, "Desc.", aClosure);
+ }
+ f("explicit/c/d", NONHEAP, BYTES, 13 * MB);
+ f("explicit/c/d", NONHEAP, BYTES, 10 * MB); // dup
+ f("explicit/c/other", NONHEAP, BYTES, 77 * MB);
+ f("explicit/cc", NONHEAP, BYTES, 13 * MB);
+ f("explicit/cc", NONHEAP, BYTES, 10 * MB); // dup
+ f("explicit/d", NONHEAP, BYTES, 499 * KB); // omitted
+ f("explicit/e", NONHEAP, BYTES, 100 * KB); // omitted
+ f("explicit/f/g/h/i", HEAP, BYTES, 10 * MB);
+ f("explicit/f/g/h/j", HEAP, BYTES, 10 * MB);
+ }
+ },
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aU, aA) {
+ aCbObj.callback("", aP, aK, aU, aA, "Desc.", aClosure);
+ }
+ f("other3", OTHER, COUNT, 777);
+ f("other2", OTHER, BYTES, 222 * MB);
+ f("perc2", OTHER, PERCENTAGE, 10000);
+ f("perc1", OTHER, PERCENTAGE, 4567);
+ f("compartments/user/https:\\\\very-long-url.com\\very-long\\oh-so-long\\really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789", OTHER, COUNT, 1);
+ }
+ },
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP) {
+ aCbObj.callback("", aP, OTHER, COUNT, 1, "Desc.", aClosure);
+ }
+ f("compartments/user/bar");
+ f("compartments/system/bar");
+ }
+ }
+ ];
+ for (let i = 0; i < fakeReporters.length; i++) {
+ mgr.registerStrongReporterEvenIfBlocked(fakeReporters[i]);
+ }
+
+ // The main process always comes first when we display about:memory. The
+ // remaining processes are sorted by their |resident| values (starting with
+ // the largest). Processes without a |resident| memory reporter are saved
+ // for the end.
+ let fakeReporters2 = [
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP1, aP2, aK, aU, aA) {
+ aCbObj.callback(aP1, aP2, aK, aU, aA, "Desc.", aClosure);
+ }
+ f("2nd", "heap-allocated", OTHER, BYTES,1000* MB);
+ f("2nd", "heap-unallocated",OTHER, BYTES,100 * MB);
+ f("2nd", "explicit/a/b/c", HEAP, BYTES,497 * MB);
+ f("2nd", "explicit/a/b/c", HEAP, BYTES, 1 * MB); // dup: merge
+ f("2nd", "explicit/a/b/c", HEAP, BYTES, 1 * MB); // dup: merge
+ f("2nd", "explicit/flip\\the\\backslashes",
+ HEAP, BYTES,200 * MB);
+ f("2nd", "explicit/compartment(compartment-url)",
+ HEAP, BYTES,200 * MB);
+ f("2nd", "other0", OTHER, BYTES,666 * MB);
+ f("2nd", "other1", OTHER, BYTES,111 * MB);
+
+ // Check that we can handle "heap-allocated" not being present.
+ f("3rd", "explicit/a/b", HEAP, BYTES,333 * MB);
+ f("3rd", "explicit/a/c", HEAP, BYTES,444 * MB);
+ f("3rd", "other1", OTHER, BYTES, 1 * MB);
+ f("3rd", "resident", OTHER, BYTES,100 * MB);
+
+ // Invalid values (negative, too-big) should be identified.
+ f("4th", "heap-allocated", OTHER, BYTES,100 * MB);
+ f("4th", "resident", OTHER, BYTES,200 * MB);
+ f("4th", "explicit/js/compartment(http:\\\\too-big.com\\)/stuff",
+ HEAP, BYTES,150 * MB);
+ f("4th", "explicit/ok", HEAP, BYTES, 5 * MB);
+ f("4th", "explicit/neg1", NONHEAP, BYTES, -2 * MB);
+ // -111 becomes "-0.00MB" in non-verbose mode, and getting the negative
+ // sign in there correctly is non-trivial.
+ f("4th", "other1", OTHER, BYTES,-111);
+ f("4th", "other2", OTHER, BYTES,-222 * MB);
+ f("4th", "other3", OTHER, COUNT, -333);
+ f("4th", "other4", OTHER, COUNT_CUMULATIVE, -444);
+ f("4th", "other5", OTHER, PERCENTAGE, -555);
+ f("4th", "other6", OTHER, PERCENTAGE, 66666);
+
+ // If a negative value is within a collapsed sub-tree in non-verbose mode,
+ // we should get the warning at the top and the relevant sub-trees should
+ // be expanded, even in non-verbose mode.
+ f("5th", "heap-allocated", OTHER, BYTES,100 * MB);
+ f("5th", "explicit/big", HEAP, BYTES, 99 * MB);
+ f("5th", "explicit/a/pos", HEAP, BYTES, 40 * KB);
+ f("5th", "explicit/a/neg1", NONHEAP, BYTES,-20 * KB);
+ f("5th", "explicit/a/neg2", NONHEAP, BYTES,-10 * KB);
+ f("5th", "explicit/b/c/d/e", NONHEAP, BYTES, 20 * KB);
+ f("5th", "explicit/b/c/d/f", NONHEAP, BYTES,-60 * KB);
+ f("5th", "explicit/b/c/g/h", NONHEAP, BYTES, 10 * KB);
+ f("5th", "explicit/b/c/i/j", NONHEAP, BYTES, 5 * KB);
+ }
+ }
+ ];
+ for (let i = 0; i < fakeReporters2.length; i++) {
+ mgr.registerStrongReporterEvenIfBlocked(fakeReporters2[i]);
+ }
+ fakeReporters = fakeReporters.concat(fakeReporters2);
+ ]]>
+ </script>
+
+ <iframe id="amFrame" height="300" src="about:memory"></iframe>
+ <!-- vary the capitalization to make sure that works -->
+ <iframe id="amvFrame" height="300" src="About:Memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ let amExpectedText =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+623.58 MB (100.0%) -- explicit\n\
+├──232.00 MB (37.20%) -- b\n\
+│ ├───85.00 MB (13.63%) ── a\n\
+│ ├───75.00 MB (12.03%) ── b\n\
+│ └───72.00 MB (11.55%) -- c\n\
+│ ├──70.00 MB (11.23%) ── a\n\
+│ └───2.00 MB (00.32%) ── b\n\
+├──222.00 MB (35.60%) ── a\n\
+├──100.00 MB (16.04%) -- c\n\
+│ ├───77.00 MB (12.35%) ── other\n\
+│ └───23.00 MB (03.69%) ── d [2]\n\
+├───23.00 MB (03.69%) ── cc [2]\n\
+├───20.00 MB (03.21%) -- f/g/h\n\
+│ ├──10.00 MB (01.60%) ── i\n\
+│ └──10.00 MB (01.60%) ── j\n\
+├───15.00 MB (02.41%) ++ g\n\
+├───11.00 MB (01.76%) ── heap-unclassified\n\
+└────0.58 MB (00.09%) ++ (2 tiny)\n\
+\n\
+Other Measurements\n\
+\n\
+5 (100.0%) -- compartments\n\
+├──3 (60.00%) -- user\n\
+│ ├──1 (20.00%) ── bar\n\
+│ ├──1 (20.00%) ── foo\n\
+│ └──1 (20.00%) ── https://very-long-url.com/very-long/oh-so-long/really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789\n\
+└──2 (40.00%) -- system\n\
+ ├──1 (20.00%) ── bar\n\
+ └──1 (20.00%) ── foo\n\
+\n\
+166.00 MB (100.0%) -- other1\n\
+├──111.00 MB (66.87%) ── a/b\n\
+└───55.00 MB (33.13%) -- c\n\
+ ├──33.00 MB (19.88%) ── e\n\
+ └──22.00 MB (13.25%) ── d\n\
+\n\
+20.20% (100.0%) -- other3\n\
+└──20.20% (100.0%) -- a/b/c/d\n\
+ ├──20.00% (99.01%) ── e\n\
+ └───0.20% (00.99%) ++ (2 tiny)\n\
+\n\
+100,000 (100.0%) -- other6\n\
+├───99,999 (100.0%) ── big\n\
+└────────1 (00.00%) ── small\n\
+\n\
+0.00 MB (100.0%) -- other7\n\
+└──0.00 MB (100.0%) ── zero\n\
+\n\
+888.00 MB ── explicit\n\
+500.00 MB ── heap-allocated\n\
+100.00 MB ── heap-unallocated\n\
+222.00 MB ── other2\n\
+ 777 ── other3\n\
+ 888 ── other4 [2]\n\
+ 45.67% ── perc1\n\
+ 100.00% ── perc2\n\
+\n\
+End of Main Process\n\
+4th\n\
+\n\
+WARNING: the following values are negative or unreasonably large.\n\
+\n\
+ explicit/js/compartment(http://too-big.com/)/stuff\n\
+ explicit/(2 tiny)\n\
+ explicit/(2 tiny)/neg1\n\
+ explicit/(2 tiny)/heap-unclassified\n\
+ other1\n\
+ other2\n\
+ other3\n\
+ other4\n\
+ other5 \n\
+\n\
+This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
+Explicit Allocations\n\
+\n\
+98.00 MB (100.0%) -- explicit\n\
+├──150.00 MB (153.06%) ── js/compartment(http://too-big.com/)/stuff [?!]\n\
+├───5.00 MB (05.10%) ── ok\n\
+└──-57.00 MB (-58.16%) -- (2 tiny) [?!]\n\
+ ├───-2.00 MB (-2.04%) ── neg1 [?!]\n\
+ └──-55.00 MB (-56.12%) ── heap-unclassified [?!]\n\
+\n\
+Other Measurements\n\
+\n\
+ 100.00 MB ── heap-allocated\n\
+ -0.00 MB ── other1 [?!]\n\
+-222.00 MB ── other2 [?!]\n\
+ -333 ── other3 [?!]\n\
+ -444 ── other4 [?!]\n\
+ -5.55% ── other5 [?!]\n\
+ 666.66% ── other6\n\
+ 200.00 MB ── resident\n\
+\n\
+End of 4th\n\
+3rd\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+777.00 MB (100.0%) -- explicit\n\
+└──777.00 MB (100.0%) -- a\n\
+ ├──444.00 MB (57.14%) ── c\n\
+ └──333.00 MB (42.86%) ── b\n\
+\n\
+Other Measurements\n\
+\n\
+ 1.00 MB ── other1\n\
+100.00 MB ── resident\n\
+\n\
+End of 3rd\n\
+2nd\n\
+Explicit Allocations\n\
+\n\
+1,000.00 MB (100.0%) -- explicit\n\
+├────499.00 MB (49.90%) ── a/b/c [3]\n\
+├────200.00 MB (20.00%) ── compartment(compartment-url)\n\
+├────200.00 MB (20.00%) ── flip/the/backslashes\n\
+└────101.00 MB (10.10%) ── heap-unclassified\n\
+\n\
+Other Measurements\n\
+\n\
+1,000.00 MB ── heap-allocated\n\
+ 100.00 MB ── heap-unallocated\n\
+ 666.00 MB ── other0\n\
+ 111.00 MB ── other1\n\
+\n\
+End of 2nd\n\
+5th\n\
+\n\
+WARNING: the following values are negative or unreasonably large.\n\
+\n\
+ explicit/(3 tiny)/a/neg2\n\
+ explicit/(3 tiny)/a/neg1\n\
+ explicit/(3 tiny)/b/c\n\
+ explicit/(3 tiny)/b/c/d\n\
+ explicit/(3 tiny)/b/c/d/f \n\
+\n\
+This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
+Explicit Allocations\n\
+\n\
+99.95 MB (100.0%) -- explicit\n\
+├──99.00 MB (99.05%) ── big\n\
+└───0.95 MB (00.95%) -- (3 tiny)\n\
+ ├──0.96 MB (00.96%) ── heap-unclassified\n\
+ ├──0.01 MB (00.01%) -- a\n\
+ │ ├──0.04 MB (00.04%) ── pos\n\
+ │ ├──-0.01 MB (-0.01%) ── neg2 [?!]\n\
+ │ └──-0.02 MB (-0.02%) ── neg1 [?!]\n\
+ └──-0.02 MB (-0.02%) -- b/c [?!]\n\
+ ├───0.01 MB (00.01%) ── g/h\n\
+ ├───0.00 MB (00.00%) ── i/j\n\
+ └──-0.04 MB (-0.04%) -- d [?!]\n\
+ ├───0.02 MB (00.02%) ── e\n\
+ └──-0.06 MB (-0.06%) ── f [?!]\n\
+\n\
+Other Measurements\n\
+\n\
+100.00 MB ── heap-allocated\n\
+\n\
+End of 5th\n\
+";
+
+ let amvExpectedText =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+653,876,224 B (100.0%) -- explicit\n\
+├──243,269,632 B (37.20%) -- b\n\
+│ ├───89,128,960 B (13.63%) ── a\n\
+│ ├───78,643,200 B (12.03%) ── b\n\
+│ └───75,497,472 B (11.55%) -- c\n\
+│ ├──73,400,320 B (11.23%) ── a\n\
+│ └───2,097,152 B (00.32%) ── b\n\
+├──232,783,872 B (35.60%) ── a\n\
+├──104,857,600 B (16.04%) -- c\n\
+│ ├───80,740,352 B (12.35%) ── other\n\
+│ └───24,117,248 B (03.69%) ── d [2]\n\
+├───24,117,248 B (03.69%) ── cc [2]\n\
+├───20,971,520 B (03.21%) -- f/g/h\n\
+│ ├──10,485,760 B (01.60%) ── i\n\
+│ └──10,485,760 B (01.60%) ── j\n\
+├───15,728,640 B (02.41%) -- g\n\
+│ ├───6,291,456 B (00.96%) ── a\n\
+│ ├───5,242,880 B (00.80%) ── b\n\
+│ └───4,194,304 B (00.64%) ── other\n\
+├───11,534,336 B (01.76%) ── heap-unclassified\n\
+├──────510,976 B (00.08%) ── d\n\
+└──────102,400 B (00.02%) ── e\n\
+\n\
+Other Measurements\n\
+\n\
+5 (100.0%) -- compartments\n\
+├──3 (60.00%) -- user\n\
+│ ├──1 (20.00%) ── bar\n\
+│ ├──1 (20.00%) ── foo\n\
+│ └──1 (20.00%) ── https://very-long-url.com/very-long/oh-so-long/really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789\n\
+└──2 (40.00%) -- system\n\
+ ├──1 (20.00%) ── bar\n\
+ └──1 (20.00%) ── foo\n\
+\n\
+174,063,616 B (100.0%) -- other1\n\
+├──116,391,936 B (66.87%) ── a/b\n\
+└───57,671,680 B (33.13%) -- c\n\
+ ├──34,603,008 B (19.88%) ── e\n\
+ └──23,068,672 B (13.25%) ── d\n\
+\n\
+20.20% (100.0%) -- other3\n\
+└──20.20% (100.0%) -- a/b/c/d\n\
+ ├──20.00% (99.01%) ── e\n\
+ ├───0.10% (00.50%) ── f\n\
+ └───0.10% (00.50%) ── g [2]\n\
+\n\
+100,000 (100.0%) -- other6\n\
+├───99,999 (100.0%) ── big\n\
+└────────1 (00.00%) ── small\n\
+\n\
+0 B (100.0%) -- other7\n\
+└──0 B (100.0%) ── zero\n\
+\n\
+931,135,488 B ── explicit\n\
+524,288,000 B ── heap-allocated\n\
+104,857,600 B ── heap-unallocated\n\
+232,783,872 B ── other2\n\
+ 777 ── other3\n\
+ 888 ── other4 [2]\n\
+ 45.67% ── perc1\n\
+ 100.00% ── perc2\n\
+\n\
+End of Main Process\n\
+4th\n\
+\n\
+WARNING: the following values are negative or unreasonably large.\n\
+\n\
+ explicit/js/compartment(http://too-big.com/)/stuff\n\
+ explicit/neg1\n\
+ explicit/heap-unclassified\n\
+ other1\n\
+ other2\n\
+ other3\n\
+ other4\n\
+ other5 \n\
+\n\
+This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
+Explicit Allocations\n\
+\n\
+102,760,448 B (100.0%) -- explicit\n\
+├──157,286,400 B (153.06%) ── js/compartment(http://too-big.com/)/stuff [?!]\n\
+├────5,242,880 B (05.10%) ── ok\n\
+├───-2,097,152 B (-2.04%) ── neg1 [?!]\n\
+└──-57,671,680 B (-56.12%) ── heap-unclassified [?!]\n\
+\n\
+Other Measurements\n\
+\n\
+ 104,857,600 B ── heap-allocated\n\
+ -111 B ── other1 [?!]\n\
+-232,783,872 B ── other2 [?!]\n\
+ -333 ── other3 [?!]\n\
+ -444 ── other4 [?!]\n\
+ -5.55% ── other5 [?!]\n\
+ 666.66% ── other6\n\
+ 209,715,200 B ── resident\n\
+\n\
+End of 4th\n\
+3rd\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+814,743,552 B (100.0%) -- explicit\n\
+└──814,743,552 B (100.0%) -- a\n\
+ ├──465,567,744 B (57.14%) ── c\n\
+ └──349,175,808 B (42.86%) ── b\n\
+\n\
+Other Measurements\n\
+\n\
+ 1,048,576 B ── other1\n\
+104,857,600 B ── resident\n\
+\n\
+End of 3rd\n\
+2nd\n\
+Explicit Allocations\n\
+\n\
+1,048,576,000 B (100.0%) -- explicit\n\
+├────523,239,424 B (49.90%) ── a/b/c [3]\n\
+├────209,715,200 B (20.00%) ── compartment(compartment-url)\n\
+├────209,715,200 B (20.00%) ── flip/the/backslashes\n\
+└────105,906,176 B (10.10%) ── heap-unclassified\n\
+\n\
+Other Measurements\n\
+\n\
+1,048,576,000 B ── heap-allocated\n\
+ 104,857,600 B ── heap-unallocated\n\
+ 698,351,616 B ── other0\n\
+ 116,391,936 B ── other1\n\
+\n\
+End of 2nd\n\
+5th\n\
+\n\
+WARNING: the following values are negative or unreasonably large.\n\
+\n\
+ explicit/a/neg2\n\
+ explicit/a/neg1\n\
+ explicit/b/c\n\
+ explicit/b/c/d\n\
+ explicit/b/c/d/f \n\
+\n\
+This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
+Explicit Allocations\n\
+\n\
+104,801,280 B (100.0%) -- explicit\n\
+├──103,809,024 B (99.05%) ── big\n\
+├────1,007,616 B (00.96%) ── heap-unclassified\n\
+├───────10,240 B (00.01%) -- a\n\
+│ ├──40,960 B (00.04%) ── pos\n\
+│ ├──-10,240 B (-0.01%) ── neg2 [?!]\n\
+│ └──-20,480 B (-0.02%) ── neg1 [?!]\n\
+└──────-25,600 B (-0.02%) -- b/c [?!]\n\
+ ├───10,240 B (00.01%) ── g/h\n\
+ ├────5,120 B (00.00%) ── i/j\n\
+ └──-40,960 B (-0.04%) -- d [?!]\n\
+ ├───20,480 B (00.02%) ── e\n\
+ └──-61,440 B (-0.06%) ── f [?!]\n\
+\n\
+Other Measurements\n\
+\n\
+104,857,600 B ── heap-allocated\n\
+\n\
+End of 5th\n\
+";
+
+ function finish()
+ {
+ mgr.unblockRegistrationAndRestoreOriginalReporters();
+ SimpleTest.finish();
+ }
+
+ // Cut+paste the entire page and check that the cut text matches what we
+ // expect. This tests the output in general and also that the cutting and
+ // pasting works as expected.
+ function test(aFrameId, aVerbose, aExpected, aNext) {
+ SimpleTest.executeSoon(function() {
+ ok(document.title === "about:memory", "document.title is correct");
+ let mostRecentActual;
+ let frame = document.getElementById(aFrameId);
+ frame.focus();
+
+ // Set the verbose checkbox value and click the go button.
+ let doc = frame.contentWindow.document;
+ let measureButton = doc.getElementById("measureButton");
+ let verbose = doc.getElementById("verbose");
+ verbose.checked = aVerbose;
+ measureButton.click();
+
+ SimpleTest.waitForClipboard(
+ function(aActual) {
+ mostRecentActual = aActual;
+ let rslt = aActual.trim() === aExpected.trim();
+ if (!rslt) {
+ // Try copying again.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ }
+
+ return rslt;
+ },
+ function() {
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ },
+ aNext,
+ function() {
+ ok(false, "pasted text doesn't match for " + aFrameId);
+ dump("******EXPECTED******\n");
+ dump("<<<" + aExpected + ">>>\n");
+ dump("*******ACTUAL*******\n");
+ dump("<<<" + mostRecentActual + ">>>\n");
+ dump("********************\n");
+ finish();
+ }
+ );
+ });
+ }
+
+ SimpleTest.waitForFocus(function() {
+ test(
+ "amFrame",
+ /* verbose = */ false,
+ amExpectedText,
+ function() {
+ test(
+ "amvFrame",
+ /* verbose = */ true,
+ amvExpectedText,
+ function() {
+ finish()
+ }
+ )
+ }
+ );
+ });
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul
new file mode 100644
index 000000000..8cf197e6d
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul
@@ -0,0 +1,423 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the collapsing and expanding of sub-trees in
+ about:memory. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ // Hide all the real reporters; we'll restore them at the end.
+ mgr.blockRegistrationAndHideExistingReporters();
+
+ // Setup various fake-but-deterministic reporters.
+ const KB = 1024;
+ const MB = KB * KB;
+ const HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+ const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+
+ let hiPath = "explicit/h/i";
+ let hi2Path = "explicit/h/i2";
+ let jkPath = "explicit/j/k";
+ let jk2Path = "explicit/j/k2";
+
+ let fakeReporters = [
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aA) {
+ aCbObj.callback("", aP, aK, BYTES, aA, "Desc.", aClosure);
+ }
+ f("heap-allocated", OTHER, 250 * MB);
+ f("explicit/a/b", HEAP, 50 * MB);
+ f("explicit/a/c/d", HEAP, 25 * MB);
+ f("explicit/a/c/e", HEAP, 15 * MB);
+ f("explicit/a/f", HEAP, 30 * MB);
+ f("explicit/g", HEAP, 100 * MB);
+ f(hiPath, HEAP, 10 * MB);
+ f(hi2Path, HEAP, 9 * MB);
+ f(jkPath, HEAP, 0.5 * MB);
+ f(jk2Path, HEAP, 0.3 * MB);
+ f("explicit/a/l/m", HEAP, 0.1 * MB);
+ f("explicit/a/l/n", HEAP, 0.1 * MB);
+ }
+ }
+ ];
+
+ for (let i = 0; i < fakeReporters.length; i++) {
+ mgr.registerStrongReporterEvenIfBlocked(fakeReporters[i]);
+ }
+
+ ]]>
+ </script>
+
+ <iframe id="amFrame" height="500" src="about:memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function finish()
+ {
+ mgr.unblockRegistrationAndRestoreOriginalReporters();
+ SimpleTest.finish();
+ }
+
+ // Click on the identified element, then cut+paste the entire page and
+ // check that the cut text matches what we expect.
+ function test(aId, aSwap, aExpected, aNext) {
+ let win = document.getElementById("amFrame").contentWindow;
+ if (aId) {
+ let node = win.document.getElementById(aId);
+
+ // Yuk: clicking a button is easy; but for tree entries we need to
+ // click on a child of the span identified via |id|.
+ if (node.nodeName === "button") {
+ if (aSwap) {
+ // We swap hipath/hi2Path and jkPath/jk2Path just before updating, to
+ // test what happens when significant nodes become insignificant and
+ // vice versa.
+ hiPath = "explicit/j/k";
+ hi2Path = "explicit/j/k2";
+ jkPath = "explicit/h/i";
+ jk2Path = "explicit/h/i2";
+ }
+ node.click();
+ } else {
+ node.childNodes[0].click();
+ }
+ }
+
+ SimpleTest.executeSoon(function() {
+ let mostRecentActual;
+ document.getElementById("amFrame").focus();
+ SimpleTest.waitForClipboard(
+ function(aActual) {
+ mostRecentActual = aActual;
+ let rslt = aActual.trim() === aExpected.trim();
+ if (!rslt) {
+ // Try copying again.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ }
+
+ return rslt;
+ },
+ function() {
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ },
+ aNext,
+ function() {
+ ok(false, "pasted text doesn't match");
+ dump("******EXPECTED******\n");
+ dump(aExpected);
+ dump("*******ACTUAL*******\n");
+ dump(mostRecentActual);
+ dump("********************\n");
+ finish();
+ }
+ );
+ });
+ }
+
+ // Returns a function that chains together one test() call per id.
+ function chain(aIds) {
+ let x = aIds.shift();
+ if (x) {
+ return function() { test(x.id, x.swap, x.expected, chain(aIds)); }
+ } else {
+ return function() { finish(); };
+ }
+ }
+
+ let startExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) -- c\n\
+│ │ ├──25.00 MB (10.00%) ── d\n\
+│ │ └──15.00 MB (06.00%) ── e\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) ++ l\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- h\n\
+│ ├──10.00 MB (04.00%) ── i\n\
+│ └───9.00 MB (03.60%) ── i2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let acCollapsedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) ++ c\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) ++ l\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- h\n\
+│ ├──10.00 MB (04.00%) ── i\n\
+│ └───9.00 MB (03.60%) ── i2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let alExpandedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) ++ c\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) -- l\n\
+│ ├──0.10 MB (00.04%) ── m\n\
+│ └──0.10 MB (00.04%) ── n\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- h\n\
+│ ├──10.00 MB (04.00%) ── i\n\
+│ └───9.00 MB (03.60%) ── i2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let aCollapsedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) ++ a\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- h\n\
+│ ├──10.00 MB (04.00%) ── i\n\
+│ └───9.00 MB (03.60%) ── i2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let hCollapsedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) ++ a\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) ++ h\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let jExpandedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) ++ a\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) ++ h\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) -- j\n\
+ ├──0.50 MB (00.20%) ── k\n\
+ └──0.30 MB (00.12%) ── k2\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ // The important thing here is that two values have been swapped.
+ // explicit/h/i should remain collapsed, and explicit/j/k should remain
+ // expanded. See bug 724863.
+ let updatedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) ++ a\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- j\n\
+│ ├──10.00 MB (04.00%) ── k\n\
+│ └───9.00 MB (03.60%) ── k2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ h\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let aExpandedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) ++ c\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) -- l\n\
+│ ├──0.10 MB (00.04%) ── m\n\
+│ └──0.10 MB (00.04%) ── n\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- j\n\
+│ ├──10.00 MB (04.00%) ── k\n\
+│ └───9.00 MB (03.60%) ── k2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ h\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let acExpandedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) -- c\n\
+│ │ ├──25.00 MB (10.00%) ── d\n\
+│ │ └──15.00 MB (06.00%) ── e\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) -- l\n\
+│ ├──0.10 MB (00.04%) ── m\n\
+│ └──0.10 MB (00.04%) ── n\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- j\n\
+│ ├──10.00 MB (04.00%) ── k\n\
+│ └───9.00 MB (03.60%) ── k2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ h\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let alCollapsedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) -- c\n\
+│ │ ├──25.00 MB (10.00%) ── d\n\
+│ │ └──15.00 MB (06.00%) ── e\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) ++ l\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- j\n\
+│ ├──10.00 MB (04.00%) ── k\n\
+│ └───9.00 MB (03.60%) ── k2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ h\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ // Test the following cases:
+ // - explicit/a/c is significant, we collapse it, it's unchanged upon
+ // update, we re-expand it
+ // - explicit/a/l is insignificant, we expand it, it's unchanged upon
+ // update, we re-collapse it
+ // - explicit/a is significant, we collapse it (which hides its
+ // sub-trees), it's unchanged upon update, we re-expand it
+ // - explicit/h is significant, we collapse it, it becomes insignificant
+ // upon update (and should remain collapsed)
+ // - explicit/j is insignificant, we expand it, it becomes significant
+ // upon update (and should remain expanded)
+ //
+ let idsToClick = [
+ { id: "measureButton", swap: 0, expected: startExpected },
+ { id: "Main Process:explicit/a/c", swap: 0, expected: acCollapsedExpected },
+ { id: "Main Process:explicit/a/l", swap: 0, expected: alExpandedExpected },
+ { id: "Main Process:explicit/a", swap: 0, expected: aCollapsedExpected },
+ { id: "Main Process:explicit/h", swap: 0, expected: hCollapsedExpected },
+ { id: "Main Process:explicit/j", swap: 0, expected: jExpandedExpected },
+ { id: "measureButton", swap: 1, expected: updatedExpected },
+ { id: "Main Process:explicit/a", swap: 0, expected: aExpandedExpected },
+ { id: "Main Process:explicit/a/c", swap: 0, expected: acExpandedExpected },
+ { id: "Main Process:explicit/a/l", swap: 0, expected: alCollapsedExpected }
+ ];
+
+ SimpleTest.waitForFocus(chain(idsToClick));
+
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
new file mode 100644
index 000000000..c712070cc
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
@@ -0,0 +1,515 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the saving and loading of memory reports to/from file in
+ about:memory. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ // Hide all the real reporters; we'll restore them at the end.
+ mgr.blockRegistrationAndHideExistingReporters();
+
+ // Setup a minimal number of fake reporters.
+ const KB = 1024;
+ const MB = KB * KB;
+ const HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+ const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+
+ let fakeReporters = [
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aA, aD) {
+ aCbObj.callback("", aP, aK, BYTES, aA, aD, aClosure);
+ }
+ f("heap-allocated", OTHER, 250 * MB, "Heap allocated.");
+ f("explicit/a/b", HEAP, 50 * MB, "A b.");
+ f("other/a", OTHER, 0.2 * MB, "Other a.");
+ f("other/b", OTHER, 0.1 * MB, "Other b.");
+ }
+ }
+ ];
+
+ for (let i = 0; i < fakeReporters.length; i++) {
+ mgr.registerStrongReporterEvenIfBlocked(fakeReporters[i]);
+ }
+
+ ]]>
+ </script>
+
+ <iframe id="amFrame" height="400" src="about:memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function finish()
+ {
+ mgr.unblockRegistrationAndRestoreOriginalReporters();
+ SimpleTest.finish();
+ }
+
+ // Load the given file into the frame, then copy+paste the entire frame and
+ // check that the cut text matches what we expect.
+ function test(aFilename, aFilename2, aExpected, aDumpFirst, aVerbose, aNext) {
+ let frame = document.getElementById("amFrame");
+ frame.focus();
+
+ let doc = frame.contentWindow.document;
+ let verbosity = doc.getElementById("verbose");
+ verbosity.checked = aVerbose;
+
+ function getFilePath(aFilename) {
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ file.append("chrome");
+ file.append("toolkit");
+ file.append("components");
+ file.append("aboutmemory");
+ file.append("tests");
+ file.append(aFilename);
+ return file.path;
+ }
+
+ let filePath = getFilePath(aFilename);
+
+ let e = document.createEvent('Event');
+ e.initEvent('change', true, true);
+
+ function check() {
+ // Initialize the clipboard contents.
+ SpecialPowers.clipboardCopyString("initial clipboard value");
+
+ let numFailures = 0, maxFailures = 30;
+
+ // Because the file load is async, we don't know when it will finish and
+ // the output will show up. So we poll.
+ function copyPasteAndCheck() {
+ // Copy and paste frame contents, and filter out non-deterministic
+ // differences.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ actual = actual.replace(/\(pid \d+\)/g, "(pid NNN)");
+
+ if (actual.trim() === aExpected.trim()) {
+ SimpleTest.ok(true, "Clipboard has the expected contents");
+ aNext();
+ } else {
+ numFailures++;
+ if (numFailures === maxFailures) {
+ ok(false, "pasted text doesn't match");
+ dump("******EXPECTED******\n");
+ dump(aExpected);
+ dump("*******ACTUAL*******\n");
+ dump(actual);
+ dump("********************\n");
+ finish();
+ } else {
+ setTimeout(copyPasteAndCheck, 100);
+ }
+ }
+ }
+ copyPasteAndCheck();
+ }
+
+ if (!aFilename2) {
+ function loadAndCheck() {
+ let fileInput1 =
+ frame.contentWindow.document.getElementById("fileInput1");
+ fileInput1.value = filePath; // this works because it's a chrome test
+
+ fileInput1.dispatchEvent(e);
+ check();
+ }
+
+ if (aDumpFirst) {
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
+ getService(Ci.nsIMemoryInfoDumper);
+ dumper.dumpMemoryReportsToNamedFile(filePath, loadAndCheck, null,
+ /* anonymize = */ false);
+ } else {
+ loadAndCheck();
+ }
+
+ } else {
+ let fileInput2 =
+ frame.contentWindow.document.getElementById("fileInput2");
+ fileInput2.value = filePath; // this works because it's a chrome test
+
+ // Hack alert: fileInput2's onchange handler calls fileInput2.click().
+ // But we don't want that to happen, because we want to bypass the file
+ // picker for the test. So we set |e.skipClick|, which causes
+ // fileInput2.click() to be skipped, and dispatch the second change event
+ // directly ourselves.
+
+ e.skipClick = true;
+ fileInput2.dispatchEvent(e);
+
+ let filePath2 = getFilePath(aFilename2);
+ fileInput2.value = filePath2; // this works because it's a chrome test
+
+ let e2 = document.createEvent('Event');
+ e2.initEvent('change', true, true);
+ fileInput2.dispatchEvent(e);
+
+ check();
+ }
+ }
+
+ // Returns a function that chains together multiple test() calls.
+ function chain(aPieces) {
+ let x = aPieces.shift();
+ if (x) {
+ return function() { test(x.filename, x.filename2, x.expected, x.dumpFirst, x.verbose, chain(aPieces)); }
+ } else {
+ return function() { finish(); };
+ }
+ }
+
+ let expectedGood =
+"\
+Explicit-only process\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+100,000 B (100.0%) -- explicit\n\
+└──100,000 B (100.0%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+End of Explicit-only process\n\
+Heap-unclassified process\n\
+Explicit Allocations\n\
+\n\
+262,144,000 B (100.0%) -- explicit\n\
+├──209,715,200 B (80.00%) ── heap-unclassified\n\
+└───52,428,800 B (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+262,144,000 B ── heap-allocated\n\
+\n\
+End of Heap-unclassified process\n\
+Main Process (pid NNN)\n\
+Explicit Allocations\n\
+\n\
+262,144,000 B (100.0%) -- explicit\n\
+├──209,715,200 B (80.00%) ── heap-unclassified\n\
+└───52,428,800 B (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+1,024 B (100.0%) -- compartments\n\
+└──1,024 B (100.0%) ── system/a\n\
+\n\
+1,024 B (100.0%) -- ghost-windows\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+314,572 B (100.0%) -- other\n\
+├──209,715 B (66.67%) ── a\n\
+└──104,857 B (33.33%) ── b\n\
+\n\
+1,024 B (100.0%) -- pss\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+1,024 B (100.0%) -- rss\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+1,024 B (100.0%) -- size\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+1,024 B (100.0%) -- swap\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+262,144,000 B ── heap-allocated\n\
+\n\
+End of Main Process (pid NNN)\n\
+Other-only process\n\
+Other Measurements\n\
+\n\
+200,000 B (100.0%) -- a\n\
+├──100,000 B (50.00%) ── b\n\
+└──100,000 B (50.00%) ── c\n\
+\n\
+500,000 B ── heap-allocated\n\
+\n\
+End of Other-only process\n\
+";
+
+ let expectedGood2 =
+"\
+Main Process (pid NNN)\n\
+Explicit Allocations\n\
+\n\
+262,144,000 B (100.0%) -- explicit\n\
+├──209,715,200 B (80.00%) ── heap-unclassified\n\
+└───52,428,800 B (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+314,572 B (100.0%) -- other\n\
+├──209,715 B (66.67%) ── a\n\
+└──104,857 B (33.33%) ── b\n\
+\n\
+262,144,000 B ── heap-allocated\n\
+\n\
+End of Main Process (pid NNN)\n\
+";
+
+ // This is the output for a malformed data file.
+ let expectedBad =
+"\
+Error: Invalid memory report(s): missing 'hasMozMallocUsableSize' property\
+";
+
+ // This is the output for a non-verbose diff.
+ let expectedDiffNonVerbose =
+"\
+P\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+-0.01 MB (100.0%) -- explicit\n\
+├──-0.01 MB (99.95%) ── storage/prefixset/goog-phish-shavar\n\
+└──-0.00 MB (00.05%) ++ (2 tiny)\n\
+\n\
+Other Measurements\n\
+\n\
+0.96 MB (100.0%) -- a\n\
+├──0.95 MB (99.80%) ── b\n\
+├──0.00 MB (00.10%) -- c\n\
+│ ├──-0.95 MB (-99.70%) ── e\n\
+│ ├──0.95 MB (99.60%) ── d\n\
+│ └──0.00 MB (00.20%) ++ (2 tiny)\n\
+└──0.00 MB (00.10%) ── h\n\
+\n\
+ 0.00 MB ── canvas-2d-pixel-bytes [2] [+]\n\
+-0.00 MB ── foobar [-]\n\
+\n\
+End of P\n\
+P2 (pid NNN)\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+0.00 MB (100.0%) -- explicit\n\
+└──0.00 MB (100.0%) ── window-objects/top(bar.com, id=NNN)/...\n\
+\n\
+Other Measurements\n\
+\n\
+0.00 MB (100.0%) -- p3\n\
+└──0.00 MB (100.0%) ── zone(0xNNN)/p3\n\
+\n\
+0.00 MB (100.0%) -- p4\n\
+└──0.00 MB (100.0%) ── js-zone(0xNNN)/p4\n\
+\n\
+0.00 MB (100.0%) -- p5\n\
+└──0.00 MB (100.0%) ── worker(foo.com, 0xNNN)/p5\n\
+\n\
+0.00 MB (100.0%) -- p6\n\
+└──0.00 MB (100.0%) ── z-moz-nullprincipal:{NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN}/p6\n\
+\n\
+0.00 MB (100.0%) -- p7\n\
+└──0.00 MB (100.0%) ── js-main-runtime-compartments/system/jar:file:///.../omni.ja!/p7\n\
+\n\
+0.00 MB ── p1 (pid NNN)\n\
+0.00 MB ── p2 (blah, pid=NNN)\n\
+\n\
+End of P2 (pid NNN)\n\
+P3\n\
+Other Measurements\n\
+\n\
+-0.00 MB ── p3 [-]\n\
+\n\
+End of P3\n\
+P4\n\
+Other Measurements\n\
+\n\
+0.00 MB ── p4 [+]\n\
+\n\
+End of P4\n\
+P7\n\
+Other Measurements\n\
+\n\
+0.00 MB (100.0%) -- p7\n\
+├──0.00 MB (57.14%) ── c [+]\n\
+└──0.00 MB (42.86%) ── b [+]\n\
+\n\
+-0.00 MB ── p7 [-]\n\
+\n\
+End of P7\n\
+P8\n\
+Other Measurements\n\
+\n\
+-0.00 MB (100.0%) -- p8\n\
+└──-0.00 MB (100.0%) -- a\n\
+ ├──-0.00 MB (50.00%) -- b\n\
+ │ ├──-0.00 MB (31.82%) -- c\n\
+ │ │ ├──-0.00 MB (18.18%) ── e [-]\n\
+ │ │ └──-0.00 MB (13.64%) ── d [-]\n\
+ │ ├──-0.00 MB (22.73%) ── f [-]\n\
+ │ └───0.00 MB (-4.55%) ── (fake child) [!]\n\
+ └──-0.00 MB (50.00%) -- g\n\
+ ├──-0.00 MB (31.82%) ── i [-]\n\
+ ├──-0.00 MB (27.27%) ── h [-]\n\
+ └───0.00 MB (-9.09%) ── (fake child) [!]\n\
+\n\
+End of P8\n\
+";
+
+ // This is the output for a verbose diff.
+ let expectedDiffVerbose =
+"\
+P\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+-10,005 B (100.0%) -- explicit\n\
+├──-10,000 B (99.95%) ── storage/prefixset/goog-phish-shavar\n\
+├───────-6 B (00.06%) ── spell-check [2]\n\
+└────────1 B (-0.01%) ── xpcom/category-manager\n\
+\n\
+Other Measurements\n\
+\n\
+1,002,000 B (100.0%) -- a\n\
+├──1,000,000 B (99.80%) ── b\n\
+├──────1,000 B (00.10%) -- c\n\
+│ ├──-999,000 B (-99.70%) ── e\n\
+│ ├──998,000 B (99.60%) ── d\n\
+│ ├──1,000 B (00.10%) ── f\n\
+│ └──1,000 B (00.10%) ── g\n\
+└──────1,000 B (00.10%) ── h\n\
+\n\
+3,000 B ── canvas-2d-pixel-bytes [2] [+]\n\
+ -100 B ── foobar [-]\n\
+\n\
+End of P\n\
+P2 (pid NNN)\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+11 B (100.0%) -- explicit\n\
+└──11 B (100.0%) ── window-objects/top(bar.com, id=NNN)/...\n\
+\n\
+Other Measurements\n\
+\n\
+11 B (100.0%) -- p3\n\
+└──11 B (100.0%) ── zone(0xNNN)/p3\n\
+\n\
+11 B (100.0%) -- p4\n\
+└──11 B (100.0%) ── js-zone(0xNNN)/p4\n\
+\n\
+11 B (100.0%) -- p5\n\
+└──11 B (100.0%) ── worker(foo.com, 0xNNN)/p5\n\
+\n\
+11 B (100.0%) -- p6\n\
+└──11 B (100.0%) ── z-moz-nullprincipal:{NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN}/p6\n\
+\n\
+11 B (100.0%) -- p7\n\
+└──11 B (100.0%) ── js-main-runtime-compartments/system/jar:file:///.../omni.ja!/p7\n\
+\n\
+11 B ── p1 (pid NNN)\n\
+11 B ── p2 (blah, pid=NNN)\n\
+\n\
+End of P2 (pid NNN)\n\
+P3\n\
+Other Measurements\n\
+\n\
+-55 B ── p3 [-]\n\
+\n\
+End of P3\n\
+P4\n\
+Other Measurements\n\
+\n\
+66 B ── p4 [+]\n\
+\n\
+End of P4\n\
+P7\n\
+Other Measurements\n\
+\n\
+7 B (100.0%) -- p7\n\
+├──4 B (57.14%) ── c [+]\n\
+└──3 B (42.86%) ── b [+]\n\
+\n\
+-5 B ── p7 [-]\n\
+\n\
+End of P7\n\
+P8\n\
+Other Measurements\n\
+\n\
+-22 B (100.0%) -- p8\n\
+└──-22 B (100.0%) -- a\n\
+ ├──-11 B (50.00%) -- b\n\
+ │ ├───-7 B (31.82%) -- c\n\
+ │ │ ├──-4 B (18.18%) ── e [-]\n\
+ │ │ └──-3 B (13.64%) ── d [-]\n\
+ │ ├───-5 B (22.73%) ── f [-]\n\
+ │ └────1 B (-4.55%) ── (fake child) [!]\n\
+ └──-11 B (50.00%) -- g\n\
+ ├───-7 B (31.82%) ── i [-]\n\
+ ├───-6 B (27.27%) ── h [-]\n\
+ └────2 B (-9.09%) ── (fake child) [!]\n\
+\n\
+End of P8\n\
+";
+
+ // This is the output for the crash reports diff.
+ let expectedDiff2 =
+"\
+Main Process (pid NNN)\n\
+Other Measurements\n\
+\n\
+1 B ── heap-allocated\n\
+\n\
+End of Main Process (pid NNN)\n\
+";
+
+ let frames = [
+ // This loads a pre-existing memory reports file that is valid.
+ { filename: "memory-reports-good.json", expected: expectedGood, dumpFirst: false, verbose: true },
+
+ // This loads a pre-existing crash dump file that is valid.
+ { filename: "crash-dump-good.json", expected: expectedGood2, dumpFirst: false, verbose: true },
+
+ // This dumps to a file and then reads it back in. (The result is the same
+ // as the previous test.)
+ { filename: "memory-reports-dumped.json.gz", expected: expectedGood2, dumpFirst: true, verbose: true },
+
+ // This loads a pre-existing file that is invalid.
+ { filename: "memory-reports-bad.json", expected: expectedBad, dumpFirst: false, verbose: true },
+
+ // This diffs two pre-existing memory reports files.
+ { filename: "memory-reports-diff1.json", filename2: "memory-reports-diff2.json", expected: expectedDiffNonVerbose, dumpFirst: false, verbose: false },
+
+ // Ditto.
+ { filename: "memory-reports-diff1.json", filename2: "memory-reports-diff2.json", expected: expectedDiffVerbose, dumpFirst: false, verbose: true },
+
+ // This diffs two pre-existing crash report files.
+ { filename: "crash-dump-diff1.json", filename2: "crash-dump-diff2.json", expected: expectedDiff2, dumpFirst: false, verbose: true }
+ ];
+
+ SimpleTest.waitForFocus(chain(frames));
+
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory4.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory4.xul
new file mode 100644
index 000000000..f2c752ac5
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory4.xul
@@ -0,0 +1,179 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the loading of memory reports from file when specified
+ in about:memory's URL (via the "file=" suffix). -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ function makePathname(aFilename) {
+ let file = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ file.append("chrome");
+ file.append("toolkit");
+ file.append("components");
+ file.append("aboutmemory");
+ file.append("tests");
+ file.append(aFilename);
+ return file.path;
+ }
+
+ // Load the given file into the frame, then copy+paste the entire frame and
+ // check that the cut text matches what we expect.
+ function test(aFilename, aExpected, aNext) {
+ let frame = document.createElementNS("http://www.w3.org/1999/xhtml", "iframe")
+ frame.height = 300;
+ frame.src = "about:memory?file=" + makePathname(aFilename);
+ document.documentElement.appendChild(frame);
+ frame.focus();
+
+ // Initialize the clipboard contents.
+ SpecialPowers.clipboardCopyString("initial clipboard value");
+
+ let numFailures = 0, maxFailures = 30;
+
+ // Because the file load is async, we don't know when it will finish and
+ // the output will show up. So we poll.
+ function copyPasteAndCheck() {
+ // Copy and paste frame contents, and filter out non-deterministic
+ // differences.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ actual = actual.replace(/\(pid \d+\)/, "(pid NNN)");
+
+ if (actual.trim() === aExpected.trim()) {
+ SimpleTest.ok(true, "Clipboard has the expected contents");
+ aNext();
+ } else {
+ numFailures++;
+ if (numFailures === maxFailures) {
+ ok(false, "pasted text doesn't match");
+ dump("******EXPECTED******\n");
+ dump(aExpected);
+ dump("*******ACTUAL*******\n");
+ dump(actual);
+ dump("********************\n");
+ SimpleTest.finish();
+ } else {
+ setTimeout(copyPasteAndCheck, 100);
+ }
+ }
+ }
+ copyPasteAndCheck();
+ }
+
+ // Returns a function that chains together multiple test() calls.
+ function chain(aFrameIds) {
+ let x = aFrameIds.shift();
+ if (x) {
+ return function() { test(x.filename, x.expected, chain(aFrameIds)); }
+ } else {
+ return function() { SimpleTest.finish(); };
+ }
+ }
+
+ // This is pretty simple output, but that's ok; this file is about testing
+ // the loading of data from file. If we got this far, we're doing fine.
+ let expectedGood =
+"\
+Explicit-only process\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+0.10 MB (100.0%) -- explicit\n\
+└──0.10 MB (100.0%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+End of Explicit-only process\n\
+Heap-unclassified process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──200.00 MB (80.00%) ── heap-unclassified\n\
+└───50.00 MB (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Heap-unclassified process\n\
+Main Process (pid NNN)\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──200.00 MB (80.00%) ── heap-unclassified\n\
+└───50.00 MB (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+0.00 MB (100.0%) -- compartments\n\
+└──0.00 MB (100.0%) ── system/a\n\
+\n\
+0.00 MB (100.0%) -- ghost-windows\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+0.30 MB (100.0%) -- other\n\
+├──0.20 MB (66.67%) ── a\n\
+└──0.10 MB (33.33%) ── b\n\
+\n\
+0.00 MB (100.0%) -- pss\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+0.00 MB (100.0%) -- rss\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+0.00 MB (100.0%) -- size\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+0.00 MB (100.0%) -- swap\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process (pid NNN)\n\
+Other-only process\n\
+Other Measurements\n\
+\n\
+0.19 MB (100.0%) -- a\n\
+├──0.10 MB (50.00%) ── b\n\
+└──0.10 MB (50.00%) ── c\n\
+\n\
+0.48 MB ── heap-allocated\n\
+\n\
+End of Other-only process\n\
+";
+
+ // This is the output for a malformed data file.
+ let expectedBad =
+"\
+Error: Invalid memory report(s): missing 'hasMozMallocUsableSize' property";
+
+ let frames = [
+ // This loads a pre-existing file that is valid.
+ { filename: "memory-reports-good.json", expected: expectedGood },
+
+ // This loads a pre-existing file that is valid.
+ { filename: "memory-reports-bad.json", expected: expectedBad }
+ ];
+
+ SimpleTest.waitForFocus(chain(frames));
+
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory5.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory5.xul
new file mode 100644
index 000000000..2fec803b9
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory5.xul
@@ -0,0 +1,167 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the saving and loading of memory reports to/from file in
+ about:memory in the presence of child processes. It is also notable
+ for being an about:memory test that uses the real reporters, rather
+ than fake deterministic ones, and so tends to show up problems in the
+ real reporters (like bogus negative values). -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <iframe id="amFrame" height="400" src="about:memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ let numRemotes = 3;
+ let numReady = 0;
+
+ // Create some remote processes, and set up message-passing so that
+ // we know when each child is fully initialized.
+ let remotes = [];
+
+ let prefs = [
+ ["dom.ipc.processCount", 3], // Allow up to 3 child processes
+ ["memory.report_concurrency", 2], // Cover more child handling cases
+ ["memory.system_memory_reporter", true] // Test SystemMemoryReporter
+ ];
+
+ SpecialPowers.pushPrefEnv({"set": prefs}, function() {
+ for (let i = 0; i < numRemotes; i++) {
+ let w = remotes[i] = window.open("remote.xul", "", "chrome");
+
+ w.addEventListener("load", function loadHandler() {
+ w.removeEventListener("load", loadHandler);
+ let remoteBrowser = w.document.getElementById("remote");
+ let mm = remoteBrowser.messageManager;
+ mm.addMessageListener("test:ready", function readyHandler() {
+ mm.removeMessageListener("test:ready", readyHandler);
+ numReady++;
+ if (numReady == numRemotes) {
+ // All the remote processes are ready.
+ SimpleTest.waitForFocus(onFocus);
+ }
+ });
+ mm.loadFrameScript("data:," + encodeURI("sendAsyncMessage('test:ready');"), true);
+ });
+ }
+ });
+
+ // Load the given file into the frame, then copy+paste the entire frame and
+ // check that the cut text matches what we expect.
+ function onFocus() {
+ let frame = document.getElementById("amFrame");
+ frame.focus();
+
+ function getFilePath(aFilename) {
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ file.append("chrome");
+ file.append("toolkit");
+ file.append("components");
+ file.append("aboutmemory");
+ file.append("tests");
+ file.append(aFilename);
+ return file.path;
+ }
+
+ let filePath = getFilePath("memory-reports-dumped.json.gz");
+
+ let e = document.createEvent('Event');
+ e.initEvent('change', true, true);
+
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
+ getService(Ci.nsIMemoryInfoDumper);
+ dumper.dumpMemoryReportsToNamedFile(filePath, loadAndCheck, null,
+ /* anonymize = */ false);
+
+ function loadAndCheck() {
+ // Load the file.
+ let fileInput1 =
+ frame.contentWindow.document.getElementById("fileInput1");
+ fileInput1.value = filePath; // this works because it's a chrome test
+ fileInput1.dispatchEvent(e);
+
+ // Initialize the clipboard contents.
+ SpecialPowers.clipboardCopyString("initial clipboard value");
+
+ let numFailures = 0, maxFailures = 30;
+
+ copyPasteAndCheck();
+
+ // Because the file load is async, we don't know when it will finish and
+ // the output will show up. So we poll.
+ function copyPasteAndCheck() {
+ // Copy and paste frame contents, and filter out non-deterministic
+ // differences.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+
+ // If we have more than 1000 chars, we've probably successfully
+ // copy+pasted.
+ if (actual.length > 1000) {
+
+ let good = true;
+
+ if (actual.match("End of System")) {
+ let m1 = actual.match("anonymous") &&
+ actual.match("shared-libraries");
+ ok(m1, "system-wide reporter")
+ good = good && !!m1;
+ }
+
+ // Note: Match "vsize" but not "vsize-max-contiguous".
+ let vsizes = actual.match(/vsize[^-]/g);
+ let endOfBrowsers = actual.match(/End of Browser/g);
+ if (endOfBrowsers == null) {
+ endOfBrowsers = actual.match(/End of Web Content/g);
+ }
+ let m2 = (vsizes.length == 4 && endOfBrowsers.length == 3);
+ ok(m2, "three child processes present in loaded data");
+ good = good && !!m2;
+
+ if (!good) {
+ dump("*******ACTUAL*******\n");
+ dump(actual);
+ dump("********************\n");
+ }
+
+ // Close the remote processes.
+ for (let i = 0; i < numRemotes; i++) {
+ remotes[i].close();
+ }
+
+ SimpleTest.finish();
+
+ } else {
+ numFailures++;
+ if (numFailures === maxFailures) {
+ ok(false, "not enough chars in pasted output");
+ SimpleTest.finish();
+ } else {
+ setTimeout(copyPasteAndCheck, 100);
+ }
+ }
+ }
+ }
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory6.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory6.xul
new file mode 100644
index 000000000..365f99091
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory6.xul
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the saving of GC and CC logs in both concise and
+ verbose formats. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <iframe id="amFrame" height="400" src="about:memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+
+ function onFocus() {
+ let frame = document.getElementById("amFrame");
+ frame.focus();
+
+ // Checks that a file exists on the local file system and removes it if it
+ // is present.
+ function checkForFileAndRemove(aFilename) {
+ let localFile = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
+ localFile.initWithPath(aFilename);
+
+ let exists = localFile.exists();
+ if (exists) {
+ localFile.remove(/* recursive = */ false);
+ }
+
+ return exists;
+ }
+
+ // Given a save log button, triggers the action and checks if both CC & GC
+ // logs were written to disk.
+ function saveLogs(aLogButton, aCCLogType)
+ {
+ // trigger the log saving
+ aLogButton.click();
+
+ // mainDiv
+ // |-> section
+ // | -> div gc log path
+ // | -> div cc log path
+ let mainDiv = frame.contentWindow.document.getElementById("mainDiv");
+ let logNodes = mainDiv.childNodes[0];
+
+ // we expect 2 logs listed
+ let numOfLogs = logNodes.childNodes.length;
+ ok(numOfLogs == 2, "two log entries generated")
+
+ // grab the path portion of the text
+ let gcLogPath = logNodes.childNodes[0].textContent
+ .replace("Saved GC log to ", "");
+ let ccLogPath = logNodes.childNodes[1].textContent
+ .replace("Saved " + aCCLogType + " CC log to ", "");
+
+ // check that the files actually exist
+ ok(checkForFileAndRemove(gcLogPath), "GC log file exists");
+ ok(checkForFileAndRemove(ccLogPath), "CC log file exists");
+ }
+
+ // get the log buttons to test
+ let saveConcise = frame.contentWindow.document
+ .getElementById("saveLogsConcise");
+ let saveVerbose = frame.contentWindow.document
+ .getElementById("saveLogsVerbose");
+
+ saveLogs(saveConcise, "concise");
+ saveLogs(saveVerbose, "verbose");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(onFocus);
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xul b/toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xul
new file mode 100644
index 000000000..a39869b7d
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xul
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="GC/CC logging with child processes"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+
+ SimpleTest.waitForExplicitFinish();
+
+ let numRemotes = 3;
+ let numReady = 0;
+
+ // Create some remote processes, and set up message-passing so that
+ // we know when each child is fully initialized.
+ let remotes = [];
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", numRemotes]]},
+ function() {
+ for (let i = 0; i < numRemotes; i++) {
+ let w = remotes[i] = window.open("remote.xul", "", "chrome");
+
+ w.addEventListener("load", function loadHandler() {
+ w.removeEventListener("load", loadHandler);
+ let remoteBrowser = w.document.getElementById("remote");
+ let mm = remoteBrowser.messageManager;
+ mm.addMessageListener("test:ready", function readyHandler() {
+ mm.removeMessageListener("test:ready", readyHandler);
+ numReady++;
+ if (numReady == numRemotes) {
+ // All the remote processes are ready. Run test.
+ runTest();
+ }
+ });
+ mm.loadFrameScript("data:," + encodeURI("sendAsyncMessage('test:ready');"), true);
+ });
+ }
+ });
+
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
+ getService(Ci.nsIMemoryInfoDumper);
+
+ function runTest()
+ {
+ let numParents = 0;
+ let numChildren = 0;
+ dumper.dumpGCAndCCLogsToFile(
+ /* identifier: */ "test." + Date.now(),
+ /* allTraces: */ false,
+ /* childProcesses: */ true,
+ {
+ onDump: function(gcLog, ccLog, isParent) {
+ if (isParent) {
+ numParents++;
+ } else {
+ numChildren++;
+ }
+ checkAndRemoveLog(gcLog);
+ checkAndRemoveLog(ccLog);
+ },
+ onFinish: function() {
+ is(numParents, 1,
+ "GC/CC logs for the parent process");
+ is(numChildren, numRemotes,
+ "GC/CC logs for each child process");
+ cleanUpAndFinish();
+ }
+ });
+ }
+
+ function cleanUpAndFinish() {
+ // Close the remote processes.
+ for (let i = 0; i < numRemotes; i++) {
+ remotes[i].close();
+ }
+ SimpleTest.finish();
+ }
+
+ function checkAndRemoveLog(logFile) {
+ let name = logFile.path;
+ ok(logFile.exists(), "log file "+name+" exists");
+ ok(logFile.isFile(), "log file "+name+" is a regular file");
+ ok(logFile.fileSize > 0, "log file "+name+" is not empty");
+ logFile.remove(/* recursive: */ false);
+ }
+
+ ]]></script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_memoryReporters.xul b/toolkit/components/aboutmemory/tests/test_memoryReporters.xul
new file mode 100644
index 000000000..9d56890b3
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_memoryReporters.xul
@@ -0,0 +1,424 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="Memory reporters"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- This file tests (in a rough fashion) whether the memory reporters are
+ producing sensible results. test_aboutmemory.xul tests the
+ presentation of memory reports in about:memory. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <!-- In bug 773533, <marquee> elements crashed the JS memory reporter -->
+ <marquee>Marquee</marquee>
+ </body>
+
+ <!-- some URIs that should be anonymized in anonymous mode -->
+ <iframe id="amFrame" height="200" src="http://example.org:80"></iframe>
+ <iframe id="amFrame" height="200" src="https://example.com:443"></iframe>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ const Cr = Components.results;
+
+ const NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
+ const HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+ const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+
+ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+ const COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
+ const COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
+ const PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
+
+ // Use backslashes instead of forward slashes due to memory reporting's hacky
+ // handling of URLs.
+ const XUL_NS =
+ "http:\\\\www.mozilla.org\\keymaster\\gatekeeper\\there.is.only.xul";
+
+ SimpleTest.waitForExplicitFinish();
+
+ let vsizeAmounts = [];
+ let residentAmounts = [];
+ let heapAllocatedAmounts = [];
+ let storageSqliteAmounts = [];
+
+ let jsGcHeapUsedGcThingsTotal = 0;
+ let jsGcHeapUsedGcThings = {};
+
+ let present = {}
+
+ // Generate a long, random string. We'll check that this string is
+ // reported in at least one of the memory reporters.
+ let bigString = "";
+ while (bigString.length < 10000) {
+ bigString += Math.random();
+ }
+ let bigStringPrefix = bigString.substring(0, 100);
+
+ // Generate many copies of two distinctive short strings, "!)(*&" and
+ // "@)(*&". We'll check that these strings are reported in at least
+ // one of the memory reporters.
+ let shortStrings = [];
+ for (let i = 0; i < 10000; i++) {
+ let str = (Math.random() > 0.5 ? "!" : "@") + ")(*&";
+ shortStrings.push(str);
+ }
+
+ let mySandbox = Components.utils.Sandbox(document.nodePrincipal,
+ { sandboxName: "this-is-a-sandbox-name" });
+
+ function handleReportNormal(aProcess, aPath, aKind, aUnits, aAmount,
+ aDescription)
+ {
+ // Record the values of some notable reporters.
+ if (aPath === "vsize") {
+ vsizeAmounts.push(aAmount);
+ } else if (aPath === "resident") {
+ residentAmounts.push(aAmount);
+ } else if (aPath.search(/^js-main-runtime-gc-heap-committed\/used\/gc-things\//) >= 0) {
+ jsGcHeapUsedGcThingsTotal += aAmount;
+ jsGcHeapUsedGcThings[aPath] = (jsGcHeapUsedGcThings[aPath] | 0) + 1;
+ } else if (aPath === "heap-allocated") {
+ heapAllocatedAmounts.push(aAmount);
+ } else if (aPath === "storage-sqlite") {
+ storageSqliteAmounts.push(aAmount);
+
+ // Check the presence of some other notable reporters.
+ } else if (aPath.search(/^explicit\/js-non-window\/.*compartment\(/) >= 0) {
+ present.jsNonWindowCompartments = true;
+ } else if (aPath.search(/^explicit\/window-objects\/top\(.*\/js-compartment\(/) >= 0) {
+ present.windowObjectsJsCompartments = true;
+ } else if (aPath.search(/^explicit\/storage\/sqlite\/places.sqlite/) >= 0) {
+ present.places = true;
+ } else if (aPath.search(/^explicit\/images/) >= 0) {
+ present.images = true;
+ } else if (aPath.search(/^explicit\/xpti-working-set$/) >= 0) {
+ present.xptiWorkingSet = true;
+ } else if (aPath.search(/^explicit\/atom-tables\/main$/) >= 0) {
+ present.atomTablesMain = true;
+ } else if (/\[System Principal\].*this-is-a-sandbox-name/.test(aPath)) {
+ // A system compartment with a location (such as a sandbox) should
+ // show that location.
+ present.sandboxLocation = true;
+ } else if (aPath.includes(bigStringPrefix)) {
+ present.bigString = true;
+ } else if (aPath.includes("!)(*&")) {
+ present.smallString1 = true;
+ } else if (aPath.includes("@)(*&")) {
+ present.smallString2 = true;
+ }
+
+ // Shouldn't get any anonymized paths.
+ if (aPath.includes('<anonymized')) {
+ present.anonymizedWhenUnnecessary = aPath;
+ }
+ }
+
+ function handleReportAnonymized(aProcess, aPath, aKind, aUnits, aAmount,
+ aDescription)
+ {
+ // Path might include an xmlns using http, which is safe to ignore.
+ let reducedPath = aPath.replace(XUL_NS, "");
+
+ // Shouldn't get http: or https: in any paths.
+ if (reducedPath.includes('http:')) {
+ present.httpWhenAnonymized = aPath;
+ }
+
+ // file: URLs should have their path anonymized.
+ if (reducedPath.search('file:..[^<]') !== -1) {
+ present.unanonymizedFilePathWhenAnonymized = aPath;
+ }
+ }
+
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ let amounts = [
+ "vsize",
+ "vsizeMaxContiguous",
+ "resident",
+ "residentFast",
+ "residentPeak",
+ "residentUnique",
+ "heapAllocated",
+ "heapOverheadFraction",
+ "JSMainRuntimeGCHeap",
+ "JSMainRuntimeTemporaryPeak",
+ "JSMainRuntimeCompartmentsSystem",
+ "JSMainRuntimeCompartmentsUser",
+ "imagesContentUsedUncompressed",
+ "storageSQLite",
+ "lowMemoryEventsVirtual",
+ "lowMemoryEventsPhysical",
+ "ghostWindows",
+ "pageFaultsHard",
+ ];
+ for (let i = 0; i < amounts.length; i++) {
+ try {
+ // If mgr[amounts[i]] throws an exception, just move on -- some amounts
+ // aren't available on all platforms. But if the attribute simply
+ // isn't present, that indicates the distinguished amounts have changed
+ // and this file hasn't been updated appropriately.
+ let dummy = mgr[amounts[i]];
+ ok(dummy !== undefined,
+ "accessed an unknown distinguished amount: " + amounts[i]);
+ } catch (ex) {
+ }
+ }
+
+ // Run sizeOfTab() to make sure it doesn't crash. We can't check the result
+ // values because they're non-deterministic.
+ let jsObjectsSize = {};
+ let jsStringsSize = {};
+ let jsOtherSize = {};
+ let domSize = {};
+ let styleSize = {};
+ let otherSize = {};
+ let totalSize = {};
+ let jsMilliseconds = {};
+ let nonJSMilliseconds = {};
+ mgr.sizeOfTab(window, jsObjectsSize, jsStringsSize, jsOtherSize,
+ domSize, styleSize, otherSize, totalSize,
+ jsMilliseconds, nonJSMilliseconds);
+
+ let asyncSteps = [
+ getReportsNormal,
+ getReportsAnonymized,
+ checkResults,
+ test_register_strong,
+ test_register_strong, // Make sure re-registering works
+ test_register_weak,
+ SimpleTest.finish
+ ];
+
+ function runNext() {
+ setTimeout(asyncSteps.shift(), 0);
+ }
+
+ function getReportsNormal()
+ {
+ mgr.getReports(handleReportNormal, null,
+ runNext, null,
+ /* anonymize = */ false);
+ }
+
+ function getReportsAnonymized()
+ {
+ mgr.getReports(handleReportAnonymized, null,
+ runNext, null,
+ /* anonymize = */ true);
+ }
+
+ function checkSizeReasonable(aName, aAmount)
+ {
+ // Check the size is reasonable -- i.e. not ridiculously large or small.
+ ok(100 * 1000 <= aAmount && aAmount <= 10 * 1000 * 1000 * 1000,
+ aName + "'s size is reasonable");
+ }
+
+ function checkSpecialReport(aName, aAmounts, aCanBeUnreasonable)
+ {
+ ok(aAmounts.length == 1, aName + " has " + aAmounts.length + " report");
+ let n = aAmounts[0];
+ if (!aCanBeUnreasonable) {
+ checkSizeReasonable(aName, n);
+ }
+ }
+
+ function checkResults()
+ {
+ try {
+ // Nb: mgr.heapAllocated will throw NS_ERROR_NOT_AVAILABLE if this is a
+ // --disable-jemalloc build. Allow for skipping this test on that
+ // exception, but *only* that exception.
+ let dummy = mgr.heapAllocated;
+ checkSpecialReport("heap-allocated", heapAllocatedAmounts);
+ } catch (ex) {
+ is(ex.result, Cr.NS_ERROR_NOT_AVAILABLE, "mgr.heapAllocated exception");
+ }
+ // vsize may be unreasonable if ASAN is enabled
+ checkSpecialReport("vsize", vsizeAmounts, /*canBeUnreasonable*/true);
+ checkSpecialReport("resident", residentAmounts);
+
+ for (var reporter in jsGcHeapUsedGcThings) {
+ ok(jsGcHeapUsedGcThings[reporter] == 1);
+ }
+ checkSizeReasonable("js-main-runtime-gc-heap-committed/used/gc-things",
+ jsGcHeapUsedGcThingsTotal);
+
+ ok(present.jsNonWindowCompartments, "js-non-window compartments are present");
+ ok(present.windowObjectsJsCompartments, "window-objects/.../js compartments are present");
+ ok(present.places, "places is present");
+ ok(present.images, "images is present");
+ ok(present.xptiWorkingSet, "xpti-working-set is present");
+ ok(present.atomTablesMain, "atom-tables/main is present");
+ ok(present.sandboxLocation, "sandbox locations are present");
+ ok(present.bigString, "large string is present");
+ ok(present.smallString1, "small string 1 is present");
+ ok(present.smallString2, "small string 2 is present");
+
+ ok(!present.anonymizedWhenUnnecessary,
+ "anonymized paths are not present when unnecessary. Failed case: " +
+ present.anonymizedWhenUnnecessary);
+ ok(!present.httpWhenAnonymized,
+ "http URLs are anonymized when necessary. Failed case: " +
+ present.httpWhenAnonymized);
+ ok(!present.unanonymizedFilePathWhenAnonymized,
+ "file URLs are anonymized when necessary. Failed case: " +
+ present.unanonymizedFilePathWhenAnonymized);
+
+ runNext();
+ }
+
+ // Reporter registration tests
+
+ // collectReports() calls to the test reporter.
+ let called = 0;
+
+ // The test memory reporter, testing the various report units.
+ // Also acts as a report collector, verifying the reported values match the
+ // expected ones after passing through XPConnect / nsMemoryReporterManager
+ // and back.
+ function MemoryReporterAndCallback() {
+ this.seen = 0;
+ }
+ MemoryReporterAndCallback.prototype = {
+ // The test reports.
+ // Each test key corresponds to the path of the report. |amount| is a
+ // function called when generating the report. |expected| is a function
+ // to be tested when receiving a report during collection. If |expected| is
+ // omitted the |amount| will be checked instead.
+ tests: {
+ "test-memory-reporter-bytes1": {
+ units: BYTES,
+ amount: () => 0
+ },
+ "test-memory-reporter-bytes2": {
+ units: BYTES,
+ amount: () => (1<<30) * 8 // awkward way to say 8G in JS
+ },
+ "test-memory-reporter-counter": {
+ units: COUNT,
+ amount: () => 2
+ },
+ "test-memory-reporter-ccounter": {
+ units: COUNT_CUMULATIVE,
+ amount: () => ++called,
+ expected: () => called
+ },
+ "test-memory-reporter-percentage": {
+ units: PERCENTAGE,
+ amount: () => 9999
+ }
+ },
+ // nsIMemoryReporter
+ collectReports: function(callback, data, anonymize) {
+ for (let path of Object.keys(this.tests)) {
+ try {
+ let test = this.tests[path];
+ callback.callback(
+ "", // Process. Should be "" initially.
+ path,
+ OTHER,
+ test.units,
+ test.amount(),
+ "Test " + path + ".",
+ data);
+ }
+ catch (ex) {
+ ok(false, ex);
+ }
+ }
+ },
+ // nsIMemoryReporterCallback
+ callback: function(process, path, kind, units, amount, data) {
+ if (path in this.tests) {
+ this.seen++;
+ let test = this.tests[path];
+ ok(units === test.units, "Test reporter units match");
+ ok(amount === (test.expected || test.amount)(),
+ "Test reporter values match: " + amount);
+ }
+ },
+ // Checks that the callback has seen the expected number of reports, and
+ // resets the callback counter.
+ // @param expected Optional. Expected number of reports the callback
+ // should have processed.
+ finish: function(expected) {
+ if (expected === undefined) {
+ expected = Object.keys(this.tests).length;
+ }
+ is(expected, this.seen,
+ "Test reporter called the correct number of times: " + expected);
+ this.seen = 0;
+ }
+ };
+
+ // General memory reporter + registerStrongReporter tests.
+ function test_register_strong() {
+ let reporterAndCallback = new MemoryReporterAndCallback();
+ // Registration works.
+ mgr.registerStrongReporter(reporterAndCallback);
+
+ // Check the generated reports.
+ mgr.getReports(reporterAndCallback, null,
+ () => {
+ reporterAndCallback.finish();
+ window.setTimeout(test_unregister_strong, 0, reporterAndCallback);
+ }, null,
+ /* anonymize = */ false);
+ }
+
+ function test_unregister_strong(aReporterAndCallback)
+ {
+ mgr.unregisterStrongReporter(aReporterAndCallback);
+
+ // The reporter was unregistered, hence there shouldn't be any reports from
+ // the test reporter.
+ mgr.getReports(aReporterAndCallback, null,
+ () => {
+ aReporterAndCallback.finish(0);
+ runNext();
+ }, null,
+ /* anonymize = */ false);
+ }
+
+ // Check that you cannot register JS components as weak reporters.
+ function test_register_weak() {
+ let reporterAndCallback = new MemoryReporterAndCallback();
+ try {
+ // Should fail! nsMemoryReporterManager will only hold a raw pointer to
+ // "weak" reporters. When registering a weak reporter, XPConnect will
+ // create a WrappedJS for JS components. This WrappedJS would be
+ // successfully registered with the manager, only to be destroyed
+ // immediately after, which would eventually lead to a crash when
+ // collecting the reports. Therefore nsMemoryReporterManager should
+ // reject WrappedJS reporters, which is what is tested here.
+ // See bug 950391 comment #0.
+ mgr.registerWeakReporter(reporterAndCallback);
+ ok(false, "Shouldn't be allowed to register a JS component (WrappedJS)");
+ }
+ catch (ex) {
+ ok(ex.message.indexOf("NS_ERROR_") >= 0,
+ "WrappedJS reporter got rejected: " + ex);
+ }
+
+ runNext();
+ }
+
+ // Kick-off the async tests.
+ runNext();
+
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_memoryReporters2.xul b/toolkit/components/aboutmemory/tests/test_memoryReporters2.xul
new file mode 100644
index 000000000..0e8ba2e81
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_memoryReporters2.xul
@@ -0,0 +1,108 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Memory reporters with child processes"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- This file tests (in a rough fashion) whether the memory reporters are
+ producing sensible results in the presence of child processes. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+
+ SimpleTest.waitForExplicitFinish();
+
+ let numRemotes = 3;
+ let numReady = 0;
+
+ // Create some remote processes, and set up message-passing so that
+ // we know when each child is fully initialized.
+ let remotes = [];
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", 3]]}, function() {
+ for (let i = 0; i < numRemotes; i++) {
+ let w = remotes[i] = window.open("remote.xul", "", "chrome");
+
+ w.addEventListener("load", function loadHandler() {
+ w.removeEventListener("load", loadHandler);
+ let remoteBrowser = w.document.getElementById("remote");
+ let mm = remoteBrowser.messageManager;
+ mm.addMessageListener("test:ready", function readyHandler() {
+ mm.removeMessageListener("test:ready", readyHandler);
+ numReady++;
+ if (numReady == numRemotes) {
+ // All the remote processes are ready. Do memory reporting.
+ doReports();
+ }
+ });
+ mm.loadFrameScript("data:," + encodeURI("sendAsyncMessage('test:ready');"), true);
+ });
+ }
+ });
+
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ function doReports()
+ {
+ let residents = {};
+
+ let handleReport = function(aProcess, aPath, aKind, aUnits, aAmount, aDesc) {
+ if (aPath === "resident") {
+ ok(100 * 1000 <= aAmount && aAmount <= 10 * 1000 * 1000 * 1000,
+ "resident is reasonable");
+ residents[aProcess] = aAmount;
+ }
+ }
+
+ let processReports = function() {
+ // First, test a failure case: calling getReports() before the previous
+ // getReports() has finished should silently abort. (And the arguments
+ // won't be used.)
+ mgr.getReports(
+ () => ok(false, "handleReport called for nested getReports() call"),
+ null, null, null, /* anonymize = */ false
+ );
+
+ // Close the remote processes.
+ for (let i = 0; i < numRemotes; i++) {
+ remotes[i].close();
+ }
+
+ // Check the results.
+
+ let processes = Object.keys(residents);
+ ok(processes.length == numRemotes + 1, "correct resident count");
+
+ let numEmptyProcesses = 0, numNonEmptyProcesses = 0;
+ for (let i = 0; i < processes.length; i++) {
+ if (processes[i] == "") {
+ numEmptyProcesses++;
+ } else {
+ ok(processes[i].startsWith("Browser (") || processes[i].startsWith("Web Content ("),
+ "correct non-empty process name prefix: " + processes[i]);
+ numNonEmptyProcesses++;
+ }
+ }
+ ok(numEmptyProcesses == 1, "correct empty process name count");
+ ok(numNonEmptyProcesses == numRemotes,
+ "correct non-empty process name count");
+
+ SimpleTest.finish();
+ }
+
+ mgr.getReports(handleReport, null, processReports, null,
+ /* anonymize = */ false);
+ }
+
+ ]]></script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul b/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul
new file mode 100644
index 000000000..3452bbbc7
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+
+ // Test for bug 708248, where the SQLite memory multi-reporter was
+ // crashing when a DB was closed.
+
+ // Nb: this test is all JS and chould be done with an xpcshell test,
+ // but all the other memory reporter tests are mochitests, so it's easier
+ // if this one is too.
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ const Cu = Components.utils;
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Make a fake DB file.
+ let file = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).
+ get("ProfD", Ci.nsIFile);
+ file.append("test_sqliteMultiReporter-fake-DB-tmp.sqlite");
+
+ // Open and close the DB.
+ let storage = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ let db = storage.openDatabase(file);
+ db.close();
+
+ // Invoke all the reporters. The SQLite multi-reporter is among
+ // them. It shouldn't crash.
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+ mgr.getReports(function(){}, null,
+ () => {
+ ok(true, "didn't crash");
+ SimpleTest.finish();
+ }, null,
+ /* anonymize = */ false);
+
+ ]]>
+ </script>
+</window>