diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/aboutmemory | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/aboutmemory')
29 files changed, 5899 insertions, 0 deletions
diff --git a/toolkit/components/aboutmemory/content/aboutMemory.css b/toolkit/components/aboutmemory/content/aboutMemory.css new file mode 100644 index 0000000000..dff9d42da0 --- /dev/null +++ b/toolkit/components/aboutmemory/content/aboutMemory.css @@ -0,0 +1,189 @@ +/* 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 + * toolkit/themes/mobile/global/aboutMemory.css. + * Desktop-specific stuff is at the bottom of this file. + */ + +html { + background: -moz-Dialog; + color: -moz-DialogText; + font: message-box; +} + +body { + padding: 0 2em; + margin: 0; + min-width: 45em; + margin: auto; +} + +/* The comment at the top of aboutMemory.xhtml explains this font choice. */ +pre { + font-family: DejaVu Sans Mono, Liberation Mono, Fira Mono, monospace; +} + +div.ancillary { + margin: 0.5em 0; + user-select: none; +} + +div.section { + padding: 2em; + margin: 1em 0em; + border: 1px solid ThreeDShadow; + border-radius: 10px; + background: Field; + color: FieldText; +} + +div.outputContainer { + display: flex; +} + +div.sections { + flex: 1; + min-width: 0; +} + +div.sidebar { + flex: 0 0 max-content; + margin-left: 1em; +} + +div.sidebarContents { + position: sticky; + top: 0.5em; +} + +div.sidebarItem { + padding: 0.5em; + margin: 1em 0em; + border: 1px solid ThreeDShadow; + border-radius: 10px; + background: Field; + color: FieldText; + user-select: none; /* no need to include this when cutting+pasting */ +} + +input.filterInput { + width: calc(100% - 1em); +} + +ul.index { + list-style-position: inside; + margin: 0; + padding: 0; +} + +ul.index > li { + padding-left: 0.5em; +} + +div.opsRow { + padding: 0.5em; + margin-right: 0.5em; + margin-top: 0.5em; + border: 1px solid ThreeDShadow; + border-radius: 10px; + background: Field; + color: FieldText; + display: inline-block; +} + +div.opsRowLabel, div.sidebarLabel { + 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; + background: inherit; /* When sticky give it the solid background of the parent */ + display: inline; /* allow subsequent text to the right of the heading */ + position: sticky; /* Stay at the top of the page when scrolling */ + top: 0; + z-index: 1; /* When sticky it should appear above the tree lines */ +} + +h2 { + padding-left: .1em; +} + +h3 { + display: inline; /* allow subsequent text to the right of the heading */ +} + +a.upDownArrow { + font-size: 130%; + text-decoration: none; + user-select: none; /* no need to include this when cutting+pasting */ +} + +.accuracyWarning, .badInputWarning, .invalid { + /* + * Technically this should be used with the default background colour, + * instead we're using the default field background colour, + * I hope this will be okay. + */ + color: -moz-activehyperlinktext; +} + +.treeline { + color: FieldText; + opacity: 0.5; +} + +.mrValue { + font-weight: bold; +} + +.hasKids { + cursor: pointer; +} + +.hasKids:hover { + text-decoration: underline; +} + +.noselect { + user-select: none; /* no need to include this when cutting+pasting */ +} + +.option { + font-size: 80%; + user-select: none; /* no need to include this when cutting+pasting */ +} + +.legend { + font-size: 80%; + user-select: none; /* no need to include this when cutting+pasting */ +} + +.debug { + font-size: 80%; +} + +.hidden { + display: none; +} + +/* 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 0000000000..7cb5ccefb3 --- /dev/null +++ b/toolkit/components/aboutmemory/content/aboutMemory.js @@ -0,0 +1,2478 @@ +/* -*- 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"; + +// --------------------------------------------------------------------------- + +let 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; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +ChromeUtils.defineESModuleGetters(this, { + Downloads: "resource://gre/modules/Downloads.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", +}); + +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" + ) +); + +let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService( + Ci.nsIMemoryReporterManager +); + +const gPageName = "about:memory"; +document.title = gPageName; + +const gMainProcessPrefix = "Main Process"; + +const gFilterUpdateDelayMS = 300; + +let gIsDiff = false; + +let gCurrentReports = []; +let gCurrentHasMozMallocUsableSize = false; +let gCurrentIsDiff = false; + +let gFilter = ""; + +// --------------------------------------------------------------------------- + +// 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.includes("\\") + ? 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(aEx) { + let str = "" + aEx; + if (str.startsWith(gAssertionFailureMsgPrefix)) { + // Argh, assertion failure within this file! Give up. + throw aEx; + } else { + // File or memory reporter problem. Print a message. + updateMainAndFooter(str, NO_TIMESTAMP, 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(aVal) { + let section = appendElement(document.body, "div", "section"); + appendElementWithText(section, "div", "debug", JSON.stringify(aVal)); +} + +function stringMatchesFilter(aString, aFilter) { + assert( + typeof aFilter == "string" || aFilter instanceof RegExp, + "unexpected aFilter type" + ); + + return typeof aFilter == "string" + ? aString.includes(aFilter) + : aFilter.test(aString); +} + +// --------------------------------------------------------------------------- + +window.onunload = function() {}; + +// --------------------------------------------------------------------------- + +// The <div> holding everything but the header and footer (if they're present). +// It's what is updated each time the page changes. +let gMain; + +// The <div> holding the footer. +let gFooter; + +// The "verbose" checkbox. +let gVerbose; + +// The "anonymize" checkbox. +let gAnonymize; + +// Values for the |aFooterAction| argument to updateTitleMainAndFooter. +const HIDE_FOOTER = 0; +const SHOW_FOOTER = 1; + +// Values for the |aShowTimestamp| argument to updateTitleMainAndFooter. +const NO_TIMESTAMP = 0; +const SHOW_TIMESTAMP = 1; + +function updateTitleMainAndFooter( + aTitleNote, + aMsg, + aShowTimestamp, + 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; + } + if (aShowTimestamp == SHOW_TIMESTAMP) { + // JS has many options for pretty-printing timestamps. We use + // toISOString() because it has sub-second granularity, which is useful + // if you quickly and repeatedly click one of the buttons. + aMsg += ` (${new Date().toISOString()})`; + } + 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, aShowTimestamp, aFooterAction, aClassName) { + return updateTitleMainAndFooter( + "", + aMsg, + aShowTimestamp, + aFooterAction, + aClassName + ); +} + +function appendTextNode(aP, aText) { + let e = document.createTextNode(aText); + aP.appendChild(e); + return e; +} + +function appendElement(aP, aTagName, aClassName) { + let e = newElement(aTagName, 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; +} + +function newElement(aTagName, aClassName) { + let e = document.createElement(aTagName); + if (aClassName) { + e.className = aClassName; + } + return e; +} + +// --------------------------------------------------------------------------- + +const explicitTreeDescription = + "This tree covers explicit memory allocations by the application. It includes \ +\n\n\ +* all 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\ +* some allocations (those covered by memory reporters) made at the operating \ +system level (via calls to functions such as VirtualAlloc, vm_allocate, and \ +mmap), \ +\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; +} + +window.onload = function() { + // 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(aElem) { + 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; + + // aElem.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 (!aElem.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" + ); + labelDiv1.setAttribute("role", "heading"); + labelDiv1.setAttribute("aria-level", "1"); + let label1 = appendElementWithText(labelDiv1, "label", ""); + gVerbose = appendElement(label1, "input", ""); + gVerbose.type = "checkbox"; + gVerbose.id = "verbose"; // used for testing + appendTextNode(label1, "verbose"); + + // The "measureButton" id is used for testing. + appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton"); + appendButton(row1, LdDesc, () => fileInput1.click(), "Load…"); + appendButton(row1, DfDesc, () => fileInput2.click(), "Load and diff…"); + + let row2 = appendElement(ops, "div", "opsRow"); + + let labelDiv2 = appendElementWithText( + row2, + "div", + "opsRowLabel", + "Save memory reports" + ); + labelDiv2.setAttribute("role", "heading"); + labelDiv2.setAttribute("aria-level", "1"); + appendButton(row2, SvDesc, saveReportsToFile, "Measure and save…"); + + // 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"); + + let labelDiv3 = appendElementWithText( + row3, + "div", + "opsRowLabel", + "Free memory" + ); + labelDiv3.setAttribute("role", "heading"); + labelDiv3.setAttribute("aria-level", "1"); + appendButton(row3, GCDesc, doGC, "GC"); + appendButton(row3, CCDesc, doCC, "CC"); + appendButton(row3, MMDesc, doMMU, "Minimize memory usage"); + + let row4 = appendElement(ops, "div", "opsRow"); + + let labelDiv4 = appendElementWithText( + row4, + "div", + "opsRowLabel", + "Save GC & CC logs" + ); + labelDiv4.setAttribute("role", "heading"); + labelDiv4.setAttribute("aria-level", "1"); + 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"); + + let labelDiv5 = appendElementWithText( + row5, + "div", + "opsRowLabel", + "Save DMD output" + ); + labelDiv5.setAttribute("role", "heading"); + labelDiv5.setAttribute("aria-level", "1"); + 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"); + gFooter.setAttribute("role", "contentinfo"); + + if (Services.policies.isAllowed("aboutSupport")) { + 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 s of searchSplit) { + if (s.toLowerCase().startsWith("file=")) { + let filename = s.substring("file=".length); + updateAboutMemoryFromFile(decodeURIComponent(filename)); + return; + } + } + } +}; + +// --------------------------------------------------------------------------- + +function doGC() { + Services.obs.notifyObservers(null, "child-gc-request"); + Cu.forceGC(); + updateMainAndFooter( + "Garbage collection completed", + SHOW_TIMESTAMP, + HIDE_FOOTER + ); +} + +function doCC() { + Services.obs.notifyObservers(null, "child-cc-request"); + window.windowUtils.cycleCollect(); + updateMainAndFooter( + "Cycle collection completed", + SHOW_TIMESTAMP, + HIDE_FOOTER + ); +} + +function doMMU() { + Services.obs.notifyObservers(null, "child-mmu-request"); + gMgr.minimizeMemoryUsage(() => + updateMainAndFooter( + "Memory minimization completed", + SHOW_TIMESTAMP, + HIDE_FOOTER + ) + ); +} + +function doMeasure() { + updateAboutMemoryFromReporters(); +} + +function saveGCLogAndConciseCCLog() { + dumpGCLogAndCCLog(false); +} + +function saveGCLogAndVerboseCCLog() { + dumpGCLogAndCCLog(true); +} + +function doDMD() { + updateMainAndFooter( + "Saving memory reports and DMD output...", + NO_TIMESTAMP, + 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", + SHOW_TIMESTAMP, + HIDE_FOOTER + ); + } catch (ex) { + updateMainAndFooter(ex.toString(), NO_TIMESTAMP, HIDE_FOOTER); + } +} + +function dumpGCLogAndCCLog(aVerbose) { + let dumper = Cc["@mozilla.org/memory-info-dumper;1"].getService( + Ci.nsIMemoryInfoDumper + ); + + let inProgress = updateMainAndFooter( + "Saving logs...", + NO_TIMESTAMP, + HIDE_FOOTER + ); + let section = appendElement(gMain, "div", "section"); + + function displayInfo(aGCLog, aCCLog, aIsParent) { + appendElementWithText(section, "div", "", "Saved GC log to " + aGCLog.path); + + let ccLogType = aVerbose ? "verbose" : "concise"; + appendElementWithText( + section, + "div", + "", + "Saved " + ccLogType + " CC log to " + aCCLog.path + ); + } + + dumper.dumpGCAndCCLogsToFile("", aVerbose, /* dumpChildProcesses = */ true, { + onDump: displayInfo, + onFinish() { + inProgress.remove(); + }, + }); +} + +/** + * Top-level function that does the work of generating the page from the memory + * reporters. + */ +function updateAboutMemoryFromReporters() { + updateMainAndFooter("Measuring...", NO_TIMESTAMP, HIDE_FOOTER); + + try { + gCurrentReports = []; + gCurrentHasMozMallocUsableSize = gMgr.hasMozMallocUsableSize; + gCurrentIsDiff = false; + gFilter = ""; + + // Record the reports from the live memory reporters then process them. + let handleReport = function( + aProcess, + aUnsafePath, + aKind, + aUnits, + aAmount, + aDescription + ) { + gCurrentReports.push({ + process: aProcess, + path: aUnsafePath, + kind: aKind, + units: aUnits, + amount: aAmount, + description: aDescription, + }); + }; + + let displayReports = function() { + updateTitleMainAndFooter( + "live measurement", + "", + NO_TIMESTAMP, + SHOW_FOOTER + ); + updateAboutMemoryFromCurrentData(); + }; + + gMgr.getReports( + handleReport, + null, + displayReports, + null, + gAnonymize.checked + ); + } catch (ex) { + handleException(ex); + } +} + +// Increment this if the JSON format changes. +// +let 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 stored in gCurrentReports and + * gCurrentHasMozMallocUsableSize. + */ +function updateAboutMemoryFromCurrentData() { + function processCurrentMemoryReports(aHandleReport, aDisplayReports) { + for (let r of gCurrentReports) { + aHandleReport( + r.process, + r.path, + r.kind, + r.units, + r.amount, + r.description, + r._presence + ); + } + aDisplayReports(); + } + + gIsDiff = gCurrentIsDiff; + appendAboutMemoryMain( + processCurrentMemoryReports, + gFilter, + gCurrentHasMozMallocUsableSize + ); + gIsDiff = false; +} + +/** + * 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" + ); + + gCurrentReports = aObj.reports.concat(); + gCurrentHasMozMallocUsableSize = aObj.hasMozMallocUsableSize; + gCurrentIsDiff = gIsDiff; + gFilter = ""; + + updateAboutMemoryFromCurrentData(); + } 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...", NO_TIMESTAMP, 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, "", NO_TIMESTAMP, 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")) { + File.createFromFileName(aFilename).then(file => { + reader.readAsText(file); + }); + return; + } + + // Read compressed gzip file. + let converter = new nsGzipConverter(); + converter.asyncConvertData( + "gzip", + "uncompressed", + { + data: [], + onStartRequest(aR, aC) {}, + onDataAvailable(aR, aStream, aO, aCount) { + let bi = new nsBinaryStream(aStream); + this.data.push(bi.readBytes(aCount)); + }, + onStopRequest(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.asyncOpen(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. +let 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(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(aJr) { + this.assertCompatible(aJr.kind, aJr.units); + this._amount += aJr.amount; + this._nMerged++; + }, + + toJSON(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; + +/** + * Return true if the report contains a webIsolated process, + * which is a good indication that Fission is enabled. + */ +function hasWebIsolatedProcess(aJSONReports) { + for (let jr of aJSONReports) { + assert(jr.process !== undefined, "Missing process"); + if (jr.process.startsWith("webIsolated")) { + return true; + } + } + return false; +} + +/** + * 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. + * @param aForgetIsolation + If this is true, treat webIsolated processes like web processes. + * @return The constructed report map. + */ +function makeDReportMap(aJSONReports, aForgetIsolation) { + let dreportMap = {}; + for (let jr of aJSONReports) { + 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); + + if (aForgetIsolation && process.startsWith("webIsolated")) { + process = "web (pid NNN)"; + } + + // Strip TIDs and threadpool IDs. + path = path.replace(/\(tid=(\d+)\)/, "(tid=NNN)"); + path = path.replace(/#\d+ \(tid=NNN\)/, "#N (tid=NNN)"); + + // 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)/... + // - event-counts/window-objects/top(<URL>, id=123)/... + path = path.replace( + /^((?:explicit|event-counts)\/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}" + ); + + // Strip segment counts from address-space. + if (path.startsWith("address-space")) { + path = path.replace(/\(segments=\d+\)/g, "(segments=NNNN)"); + } + + // Normalize omni.ja! paths. + path = path.replace( + /jar:file:\\\\\\(.+)\\omni.ja!/, + "jar:file:\\\\\\...\\omni.ja!" + ); + + // Normalize script source counts. + path = path.replace(/source\(scripts=(\d+), /, "source(scripts=NNN, "); + + 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]; + } + + // If one report we're diffing contains webIsolated processes, but the other + // does not, then we're probably comparing a report with Fission enabled with + // one where it is not enabled. In this case, we want to make all of the + // webIsolated processes look like plain old web processes to get a better + // diff. + let hasIsolated1 = hasWebIsolatedProcess(aJson1.reports); + let hasIsolated2 = hasWebIsolatedProcess(aJson2.reports); + let eitherIsolated = hasIsolated1 || hasIsolated2; + let forgetIsolation = hasIsolated1 != hasIsolated2 && eitherIsolated; + + return { + version: simpleProp("version"), + + hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"), + + reports: makeJSONReports( + diffDReportMaps( + makeDReportMap(aJson1.reports, forgetIsolation), + makeDReportMap(aJson2.reports, forgetIsolation) + ) + ), + }; +} + +// --------------------------------------------------------------------------- + +// |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 aFilter + * String or RegExp used to filter reports by their path. + * @param aHasMozMallocUsableSize + * Boolean indicating if moz_malloc_usable_size works. + */ +function appendAboutMemoryMain( + aProcessReports, + aFilter, + aHasMozMallocUsableSize +) { + let pcollsByProcess = {}; + let infoByProcess = {}; + + 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" + ); + + // If the process is empty, that means this process -- which is the main + // process, because this is chrome JS code -- is doing the dumping. + // Generate the process identifier: `Main Process (pid $PID)`. + // + // Note that `HandleReportAndFinishReportingCallbacks::Callback()` handles + // this when saving memory reports to file. So, if we are loading memory + // reports from file then `aProcess` will already be non-empty. + let process = aProcess + ? aProcess + : gMainProcessPrefix + " (pid " + Services.appinfo.processID + ")"; + + // Store the "resident" value for each process, so that if we filter it + // out, we can still use it to correctly sort processes and generate the + // process index. + let info = infoByProcess[process]; + if (!info) { + info = infoByProcess[process] = {}; + } + if (aUnsafePath == "resident") { + infoByProcess[process].resident = aAmount; + } + + // Ignore reports that don't match the current filter. + if (!stringMatchesFilter(aUnsafePath, aFilter)) { + return; + } + + 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(infoByProcess); + 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.startsWith(gMainProcessPrefix)) { + return -1; + } + if (aProcessB.startsWith(gMainProcessPrefix)) { + return 1; + } + + // Then sort by resident size. + let residentA = infoByProcess[aProcessA].resident || -1; + let residentB = infoByProcess[aProcessB].resident || -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; + }); + + // We set up this general layout inside gMain: + // + // <div class="outputContainer"> + // <div class="sections"></div> + // <div class="sidebar"> + // <div class="sidebarContents"> + // <div class="sidebarItem filterItem"></div> + // <div class="sidebarItem indexItem"></div> + // </div> + // </div> + // </div> + // + // If we detect that outputContainer already exists, then this is an update + // (due to typing in a filter string) to an already-displayed memory report. + // In this case we preserve the structure of the layout and only replace + // div.sections and #indexItem. Preserving the filter sidebar item means we + // preserve any editing state in its <input>. + + // Generate the main process sections. + let sections = newElement("div", "sections"); + sections.setAttribute("role", "main"); + + for (let [i, process] of processes.entries()) { + let pcolls = pcollsByProcess[process]; + if (!pcolls) { + continue; + } + + let section = appendElement(sections, "div", "section"); + appendProcessAboutMemoryElements( + section, + i, + process, + pcolls._trees, + pcolls._degenerates, + pcolls._heapTotal, + aHasMozMallocUsableSize, + aFilter != "" + ); + } + + if (!sections.firstChild) { + appendElementWithText(sections, "div", "section", "No results found."); + } + + // Generate the process index. + let indexItem = newElement("div", "sidebarItem"); + indexItem.classList.add("indexItem"); + appendElementWithText(indexItem, "div", "sidebarLabel", "Process index"); + let indexList = appendElement(indexItem, "ul", "index"); + + for (let [i, process] of processes.entries()) { + let indexListItem = appendElement(indexList, "li"); + let pcolls = pcollsByProcess[process]; + if (pcolls) { + let indexLink = appendElementWithText(indexListItem, "a", "", process); + indexLink.href = "#start" + i; + } else { + // We've filtered out all reports from this process. Generate a non-link + // entry in the process index, and skip creating a process report + // section. + indexListItem.textContent = process; + } + } + + // If we are updating, just swap in the new process output. + let outputContainer = gMain.querySelector(".outputContainer"); + if (outputContainer) { + outputContainer.querySelector(".sections").replaceWith(sections); + outputContainer.querySelector(".indexItem").replaceWith(indexItem); + return; + } + + // Otherwise, generate the rest of the layout. + outputContainer = appendElement(gMain, "div", "outputContainer"); + outputContainer.appendChild(sections); + + let sidebar = appendElement(outputContainer, "div", "sidebar"); + sidebar.setAttribute("role", "navigation"); + let sidebarContents = appendElement(sidebar, "div", "sidebarContents"); + + // Generate the filter input and checkbox. + let filterItem = appendElement(sidebarContents, "div", "sidebarItem"); + filterItem.classList.add("filterItem"); + appendElementWithText(filterItem, "div", "sidebarLabel", "Filter"); + + let filterInput = appendElement(filterItem, "input", "filterInput"); + filterInput.placeholder = "Memory report path filter"; + + let filterOptions = appendElement(filterItem, "div"); + let filterRegExLabel = appendElement(filterOptions, "label"); + let filterRegExCheckbox = appendElement(filterRegExLabel, "input"); + filterRegExCheckbox.type = "checkbox"; + filterRegExLabel.append(" Regular expression"); + + // Set up event handlers to update the display if the filter input or + // checkbox changes. + let filterUpdateTimeout; + let filterUpdate = function() { + if (filterUpdateTimeout) { + window.clearTimeout(filterUpdateTimeout); + } + filterUpdateTimeout = window.setTimeout(function() { + try { + gFilter = + filterRegExCheckbox.checked && filterInput.value != "" + ? new RegExp(filterInput.value) + : filterInput.value; + } catch (ex) { + // Match nothing if the regex was invalid. + gFilter = new RegExp("^$"); + } + updateAboutMemoryFromCurrentData(); + }, gFilterUpdateDelayMS); + }; + filterInput.oninput = filterUpdate; + filterRegExCheckbox.onchange = filterUpdate; + + // Append the process list item after the filter item. + sidebarContents.appendChild(indexItem); + } + + 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(aUnsafeName) { + if (this._kids) { + for (let kid of this._kids) { + if (kid._unsafeName === aUnsafeName) { + return kid; + } + } + } + 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() { + 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 kid of this._kids) { + max = Math.max(max, kid.maxAbsDescendant()); + } + this._maxAbsDescendant = max; + return max; + }, + + toString() { + switch (this._units) { + case UNITS_BYTES: + return formatBytes(this._amount); + case UNITS_COUNT: + case UNITS_COUNT_CUMULATIVE: + return formatNum(this._amount); + case UNITS_PERCENTAGE: + return formatPercentage(this._amount); + default: + throw new Error( + "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 kid of aT._kids) { + kidsBytes += fillInNonLeafNodes(kid); + } + + // ... 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 kid of aT._kids) { + sortTreeAndInsertAggregateNodes(aTotalBytes, kid); + } + 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 (let kid of aggT._kids) { + sortTreeAndInsertAggregateNodes(aTotalBytes, kid); + } + 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. +let gUnsafePathsWithInvalidValuesForThisProcess = []; + +function appendWarningElements( + aP, + aHasKnownHeapAllocated, + aHasMozMallocUsableSize, + aFiltered +) { + // These warnings may not make sense if the reporters they reference have been + // filtered out, so just skip them if we have a filter applied. + if (!aFiltered && !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 (!aFiltered && !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 (!aFiltered && !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) { + 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. + * @param aFiltered + * Boolean indicating whether the reports were filtered. + * @return The generated text. + */ +function appendProcessAboutMemoryElements( + aP, + aN, + aProcess, + aTrees, + aDegenerates, + aHeapTotal, + aHasMozMallocUsableSize, + aFiltered +) { + 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 gives nice spacing when we copy and paste. + appendElementWithText(aP, "span", "", "\n"); + }; + + appendElementWithText(aP, "h1", "", aProcess); + appendLink("start", "end", "↓"); + + // 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. + if (otherTrees.length || otherDegenerates.length) { + let pre = appendSectionHeader(aP, "Other Measurements"); + for (let t of otherTrees) { + appendTreeElements(pre, t, aProcess, ""); + appendTextNode(pre, "\n"); // blank lines after non-degenerate trees + } + for (let t of otherDegenerates) { + let padText = "".padStart(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, + aFiltered + ); + } + + appendElementWithText(aP, "h3", "", "End of " + aProcess); + appendLink("end", "start", "↑"); +} + +// The locale used when formatting a number as a human-readable string in any +// format. +const kStyleLocale = "en-US"; + +// Used for UNITS_BYTES values that are printed as MiB. +const kMBFormat = new Intl.NumberFormat(kStyleLocale, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); + +// Used for UNITS_PERCENTAGE values. +const kPercFormatter = new Intl.NumberFormat(kStyleLocale, { + style: "percent", + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); + +// Used for fractions within the tree. +const kFracFormatter = new Intl.NumberFormat(kStyleLocale, { + style: "percent", + minimumIntegerDigits: 2, + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); + +// Used for special-casing 100% fractions within the tree. +const kFrac1Formatter = new Intl.NumberFormat(kStyleLocale, { + style: "percent", + minimumIntegerDigits: 3, + minimumFractionDigits: 1, + maximumFractionDigits: 1, +}); + +// Used when no custom formatting was requested. +const kDefaultNumFormatter = new Intl.NumberFormat(kStyleLocale); + +/** + * Formats an int as a human-readable string. + * + * @param aN + * The integer to format. + * @param aFormatter + * Optional formatter object. + * @return A human-readable string representing the int. + */ +function formatNum(aN, aFormatter) { + return (aFormatter || kDefaultNumFormatter).format(aN); +} + +/** + * Converts a byte count to an appropriate string representation. + * + * @param aBytes + * The byte count. + * @return The string representation. + */ +function formatBytes(aBytes) { + return gVerbose.checked + ? `${formatNum(aBytes)} B` + : `${formatNum(aBytes / (1024 * 1024), kMBFormat)} MB`; +} + +/** + * Converts a UNITS_PERCENTAGE value to an appropriate string representation. + * + * @param aPerc100x + * The percentage, multiplied by 100 (see nsIMemoryReporter). + * @return The string representation + */ +function formatPercentage(aPerc100x) { + // A percentage like 12.34% will have an aPerc100x value of 1234, and we need + // to divide that by 10,000 to get the 0.1234 that toLocaleString() wants. + return formatNum(aPerc100x / 10000, kPercFormatter); +} + +/* + * Converts a tree fraction to an appropriate string representation. + * + * @param aNum + * The numerator. + * @param aDenom + * The denominator. + * @return The string representation + */ +function formatTreeFrac(aNum, aDenom) { + // Two special behaviours here: + // - We treat 0 / 0 as 100%. + // - We want 4 digits, as much as possible, because it gives good vertical + // alignment. For positive numbers, 00.00%--99.99% works straighforwardly, + // but 100.0% needs special handling. + let num = aDenom === 0 ? 1 : aNum / aDenom; + return 0.99995 <= num && num <= 1 + ? formatNum(1, kFrac1Formatter) + : formatNum(num, kFracFormatter); +} + +const kNoKidsSep = " ── ", + 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. +let gShowSubtreesBySafeTreeId = {}; + +function assertClassListContains(aElem, aClassName) { + assert(aElem, "undefined " + aClassName); + assert(aElem.classList.contains(aClassName), "classname isn't " + aClassName); +} + +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. + + // We want the outer span. |aEvent.target| will normally be one of the inner + // spans. However, if the click was dispatched via a11y, it might be the outer + // span because some of the inner spans are pruned from the a11y tree. + let outerSpan = aEvent.target.classList.contains("hasKids") + ? aEvent.target + : 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; + outerSpan.setAttribute("aria-expanded", "true"); + } else if (sepSpan.textContent === kShowKidsSep) { + isExpansion = false; + sepSpan.textContent = kHideKidsSep; + outerSpan.setAttribute("aria-expanded", "false"); + } 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; + aElement.setAttribute("aria-expanded", "true"); + expandPathToThisElement(aElement.parentNode.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. 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. + * + * @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 aTlThis + * The treeline for this entry. + * @param aTlKids + * 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, + aTlThis, + aTlKids, + aParentStringLength + ) { + function appendN(aS, aC, aN) { + for (let i = 0; i < aN; i++) { + aS += aC; + } + return aS; + } + + // The entire entry including children needs to be treated as a list item + // for a11y purposes. + let p = document.createElement("span"); + p.setAttribute("role", "listitem"); + aP.appendChild(p); + + // The tree line. Indent more if this entry is narrower than its parent. + let valueText = aT.toString(); + let extraTlLength = Math.max(aParentStringLength - valueText.length, 0); + if (extraTlLength > 0) { + aTlThis = appendN(aTlThis, "─", extraTlLength); + aTlKids = appendN(aTlKids, " ", extraTlLength); + } + let treeLine = appendElementWithText(p, "span", "treeline", aTlThis); + treeLine.setAttribute("aria-hidden", "true"); + + // 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(p, "span", "hasKids"); + d.id = safeTreeId; + d.onclick = toggle; + d.setAttribute("role", "button"); + sep = showSubtrees ? kShowKidsSep : kHideKidsSep; + d.setAttribute("aria-expanded", showSubtrees ? "true" : "false"); + } else { + assert(!aT._hideKids, "leaf node with _hideKids set"); + sep = kNoKidsSep; + d = p; + } + + // The value. + appendElementWithText( + d, + "span", + "mrValue" + (tIsInvalid ? " invalid" : ""), + valueText + ); + + // The percentage (omitted for single entries). + if (!aT._isDegenerate) { + let percText = formatTreeFrac(aT._amount, aRoot._amount); + 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(aT._kids ? d : aP); + } + + // Recurse over children. + if (aT._kids) { + // The 'kids' class is just used for sanity checking in toggle(). + d = appendElement(p, "span", showSubtrees ? "kids" : "kids hidden"); + d.setAttribute("role", "list"); + + let tlThisForMost, tlKidsForMost; + if (aT._kids.length > 1) { + tlThisForMost = aTlKids + "├──"; + tlKidsForMost = aTlKids + "│ "; + } + let tlThisForLast = aTlKids + "└──"; + let tlKidsForLast = aTlKids + " "; + + for (let [i, kid] of aT._kids.entries()) { + let isLast = i == aT._kids.length - 1; + aUnsafeNames.push(kid._unsafeName); + appendTreeElements2( + d, + aProcess, + aUnsafeNames, + aRoot, + kid, + !isLast ? tlThisForMost : tlThisForLast, + !isLast ? tlKidsForMost : tlKidsForLast, + valueText.length + ); + aUnsafeNames.pop(); + } + } + } + + let rootStringLength = aRoot.toString().length; + appendTreeElements2( + aP, + aProcess, + [aRoot._unsafeName], + aRoot, + aRoot, + aPadText, + aPadText, + rootStringLength + ); +} + +// --------------------------------------------------------------------------- + +function appendSectionHeader(aP, aText) { + appendElementWithText(aP, "h2", "", aText + "\n"); + let entries = appendElement(aP, "pre", "entries"); + entries.setAttribute("role", "list"); + return 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(aFile) { + let dumper = Cc["@mozilla.org/memory-info-dumper;1"].getService( + Ci.nsIMemoryInfoDumper + ); + let finishDumping = () => { + updateMainAndFooter( + "Saved memory reports to " + aFile.path, + SHOW_TIMESTAMP, + HIDE_FOOTER + ); + }; + dumper.dumpMemoryReportsToNamedFile( + aFile.path, + finishDumping, + null, + gAnonymize.checked, + /* minimize memory usage = */ false + ); + }; + + 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(aDirPath) { + let file = FileUtils.File(aDirPath); + 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 0000000000..99955b445a --- /dev/null +++ b/toolkit/components/aboutmemory/content/aboutMemory.xhtml @@ -0,0 +1,28 @@ +<?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/. --> + +<!-- We explicitly set the language to English, which more-or-less guarantees + that the box-drawing characters used are the correct width. Without that, + in a Japanese or Chinese locale we might end up with box-drawing + characters that are twice the width of English characters, which messes up + the tree layout. See bug 1561153 for details. Note that about:memory is + not localized, so setting it explicitly to English should be fine. + + We also set the fonts in aboutMemory.css in such a way that maximizes the + chances that the font chosen supports the box-drawing chars. +--> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en"> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" /> + <meta name="viewport" content="width=device-width"/> + <meta name="color-scheme" content="light dark"/> + <title>Memory Analyzer</title> + <link rel="stylesheet" href="chrome://global/skin/aboutMemory.css" type="text/css"/> + <script src="chrome://global/content/aboutMemory.js"/> + </head> + + <body></body> +</html> diff --git a/toolkit/components/aboutmemory/jar.mn b/toolkit/components/aboutmemory/jar.mn new file mode 100644 index 0000000000..0a6b01ed78 --- /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 0000000000..017d8d1264 --- /dev/null +++ b/toolkit/components/aboutmemory/moz.build @@ -0,0 +1,15 @@ +# -*- 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"] +XPCSHELL_TESTS_MANIFESTS += [ + "tests/xpcshell/xpcshell.ini", +] + +JAR_MANIFESTS += ["jar.mn"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "about:memory") diff --git a/toolkit/components/aboutmemory/tests/chrome.ini b/toolkit/components/aboutmemory/tests/chrome.ini new file mode 100644 index 0000000000..4e5071e4a0 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/chrome.ini @@ -0,0 +1,31 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + crash-dump-diff1.json + crash-dump-diff2.json + crash-dump-good.json + fiss-diff1.json + fiss-diff2.json + memory-reports-bad.json + memory-reports-diff1.json + memory-reports-diff2.json + memory-reports-good.json + remote.xhtml + +[test_aboutmemory.xhtml] +[test_aboutmemory2.xhtml] +[test_aboutmemory3.xhtml] +[test_aboutmemory4.xhtml] +skip-if = + os == "linux" && bits == 64 && debug # Bug 1683002 +[test_aboutmemory5.xhtml] +skip-if = + asan # Bug 1116230 + os == "linux" && bits == 64 && debug # Bug 1683002 +[test_aboutmemory6.xhtml] +[test_aboutmemory7.xhtml] +[test_memoryReporters.xhtml] +[test_memoryReporters2.xhtml] +[test_sqliteMultiReporter.xhtml] +[test_dumpGCAndCCLogsToFile.xhtml] +skip-if = (verify && debug && (os == 'mac')) 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 0000000000..d41bbcc61e --- /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 0000000000..8f9451f625 --- /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 0000000000..6bee54d591 --- /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/fiss-diff1.json b/toolkit/components/aboutmemory/tests/fiss-diff1.json new file mode 100644 index 0000000000..e4fa2bd954 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/fiss-diff1.json @@ -0,0 +1,18 @@ +{ + "foo": 1, + "blah": 2, + "memory_report": { + "version": 1, + "hasMozMallocUsableSize": true, + "reports": [ + {"process": "P (pid 12345)", "path": "explicit/foobar", "kind": 1, "units": 0, "amount": 100, "description": "Desc."}, + {"process": "P (pid 12345)", "path": "explicit/zero1", "kind": 1, "units": 0, "amount": 0, "description": "Desc."}, + {"process": "P (pid 12345)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 10000000, "description": "Heap allocated."}, + + {"process": "web (pid 12345)", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 2000000, "description": "Desc."}, + {"process": "web (pid 12345)", "path": "explicit/a/c/d", "kind": 1, "units": 0, "amount": 2000000, "description": "Desc."}, + {"process": "web (pid 12345)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 10000000, "description": "Heap allocated."} + ] + } +} + diff --git a/toolkit/components/aboutmemory/tests/fiss-diff2.json b/toolkit/components/aboutmemory/tests/fiss-diff2.json new file mode 100644 index 0000000000..884e434748 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/fiss-diff2.json @@ -0,0 +1,18 @@ +{ + "foo": 1, + "blah": 2, + "memory_report": { + "version": 1, + "hasMozMallocUsableSize": true, + "reports": [ + {"process": "P (pid 12345)", "path": "explicit/foobar", "kind": 1, "units": 0, "amount": 400, "description": "Desc."}, + {"process": "P (pid 12345)", "path": "explicit/zero1", "kind": 1, "units": 0, "amount": 0, "description": "Desc."}, + {"process": "P (pid 12345)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 13000000, "description": "Heap allocated."}, + + {"process": "webIsolated=https://example.com (pid 12345)", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 2000000, "description": "Desc."}, + {"process": "webIsolated=https://example.com (pid 12345)", "path": "explicit/a/c/d", "kind": 1, "units": 0, "amount": 3000000, "description": "Desc."}, + {"process": "webIsolated=https://example.com (pid 12345)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 12000000, "description": "Heap allocated."} + ] + } +} + 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 0000000000..61a2092b1b --- /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 0000000000..0a7e55c323 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/memory-reports-diff1.json @@ -0,0 +1,49 @@ +{ + "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": "P2 (pid 22)", "path": "explicit/js-non-window/runtime/script-sources/source(scripts=1011, <non-notable files>)/misc", "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."}, + + {"process": "P9", "path": "explicit/threads/stacks/DNS Resolver #1 (tid=11)", "kind": 0, "units": 0, "amount": 2000, "description": "Desc."}, + {"process": "P9", "path": "explicit/threads/stacks/DNS Resolver #2 (tid=22)", "kind": 0, "units": 0, "amount": 4000, "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 0000000000..df9899c709 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/memory-reports-diff2.json @@ -0,0 +1,49 @@ +{ + "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": "P2 (pid 22)", "path": "explicit/js-non-window/runtime/script-sources/source(scripts=1, <non-notable files>)/misc", "kind": 2, "units": 0, "amount": 33, "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."}, + + {"process": "P9", "path": "explicit/threads/stacks/DNS Resolver #1 (tid=33)", "kind": 0, "units": 0, "amount": 2000, "description": "Desc."}, + {"process": "P9", "path": "explicit/threads/stacks/DNS Resolver #2 (tid=44)", "kind": 0, "units": 0, "amount": 4000, "description": "Desc."}, + {"process": "P9", "path": "explicit/threads/stacks/DNS Resolver #3 (tid=45)", "kind": 0, "units": 0, "amount": 5000, "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 0000000000..ecfcddc5f7 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/memory-reports-good.json @@ -0,0 +1,27 @@ +{ + "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": "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.xhtml b/toolkit/components/aboutmemory/tests/remote.xhtml new file mode 100644 index 0000000000..7d69101305 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/remote.xhtml @@ -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.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory.xhtml new file mode 100644 index 0000000000..4617a019ad --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xhtml @@ -0,0 +1,599 @@ +<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script 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.xhtml 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); + + 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(aCbObj, aClosure, aAnonymize) { + function f(aP, aK, aU, aA) { + aCbObj.callback("Main Process", 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(aCbObj, aClosure, aAnonymize) { + function f(aP, aK, aU, aA) { + aCbObj.callback("Main Process", 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(aCbObj, aClosure, aAnonymize) { + function f(aP, aK, aU, aA) { + aCbObj.callback("Main Process", 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(aCbObj, aClosure, aAnonymize) { + function f(aP) { + aCbObj.callback("Main Process", 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(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 (-02.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 (-00.01%) ── neg2 [?!]\n\ + │ └──-0.02 MB (-00.02%) ── neg1 [?!]\n\ + └──-0.02 MB (-00.02%) -- b/c [?!]\n\ + ├───0.01 MB (00.01%) ── g/h\n\ + ├───0.00 MB (00.00%) ── i/j\n\ + └──-0.04 MB (-00.04%) -- d [?!]\n\ + ├───0.02 MB (00.02%) ── e\n\ + └──-0.06 MB (-00.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 (-02.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 (-00.01%) ── neg2 [?!]\n\ +│ └──-20,480 B (-00.02%) ── neg1 [?!]\n\ +└──────-25,600 B (-00.02%) -- b/c [?!]\n\ + ├───10,240 B (00.01%) ── g/h\n\ + ├────5,120 B (00.00%) ── i/j\n\ + └──-40,960 B (-00.04%) -- d [?!]\n\ + ├───20,480 B (00.02%) ── e\n\ + └──-61,440 B (-00.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.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xhtml new file mode 100644 index 0000000000..28c8f7fa4f --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xhtml @@ -0,0 +1,420 @@ +<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script 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"; + + 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(aCbObj, aClosure, aAnonymize) { + function f(aP, aK, aA) { + aCbObj.callback("Main Process ABC", 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)); } + } + return function() { finish(); }; + } + + let startExpected = +"\ +Main Process ABC\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 ABC\n\ +"; + + let acCollapsedExpected = +"\ +Main Process ABC\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 ABC\n\ +"; + + let alExpandedExpected = +"\ +Main Process ABC\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 ABC\n\ +"; + + let aCollapsedExpected = +"\ +Main Process ABC\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 ABC\n\ +"; + + let hCollapsedExpected = +"\ +Main Process ABC\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 ABC\n\ +"; + + let jExpandedExpected = +"\ +Main Process ABC\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 ABC\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 ABC\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 ABC\n\ +"; + + let aExpandedExpected = +"\ +Main Process ABC\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 ABC\n\ +"; + + let acExpandedExpected = +"\ +Main Process ABC\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 ABC\n\ +"; + + let alCollapsedExpected = +"\ +Main Process ABC\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 ABC\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 ABC:explicit/a/c", swap: 0, expected: acCollapsedExpected }, + { id: "Main Process ABC:explicit/a/l", swap: 0, expected: alExpandedExpected }, + { id: "Main Process ABC:explicit/a", swap: 0, expected: aCollapsedExpected }, + { id: "Main Process ABC:explicit/h", swap: 0, expected: hCollapsedExpected }, + { id: "Main Process ABC:explicit/j", swap: 0, expected: jExpandedExpected }, + { id: "measureButton", swap: 1, expected: updatedExpected }, + { id: "Main Process ABC:explicit/a", swap: 0, expected: aExpandedExpected }, + { id: "Main Process ABC:explicit/a/c", swap: 0, expected: acExpandedExpected }, + { id: "Main Process ABC:explicit/a/l", swap: 0, expected: alCollapsedExpected } + ]; + + SimpleTest.waitForFocus(chain(idsToClick)); + + SimpleTest.waitForExplicitFinish(); + ]]> + </script> +</window> diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory3.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xhtml new file mode 100644 index 0000000000..50112f1330 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xhtml @@ -0,0 +1,558 @@ +<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script 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"; + + 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(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 = SpecialPowers.Services.dirsvc.get("CurWorkD", Ci.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, + /* minimize memory usage = */ 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)); } + } + return function() { finish(); }; + } + + let expectedGood = +"\ +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\ +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\ +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\ +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 (-04.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 (-09.09%) ── (fake child) [!]\n\ +\n\ +End of P8\n\ +P9\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%) ── threads/stacks/DNS Resolver #N (tid=NNN) [3]\n\ +\n\ +End of P9\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 (-00.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 (-04.55%) ── (fake child) [!]\n\ + └──-11 B (50.00%) -- g\n\ + ├───-7 B (31.82%) ── i [-]\n\ + ├───-6 B (27.27%) ── h [-]\n\ + └────2 B (-09.09%) ── (fake child) [!]\n\ +\n\ +End of P8\n\ +P9\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\ +5,000 B (100.0%) -- explicit\n\ +└──5,000 B (100.0%) ── threads/stacks/DNS Resolver #N (tid=NNN) [3]\n\ +\n\ +End of P9\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 expectedDiffFiss = +"\ +P (pid NNN)\n\ +Explicit Allocations\n\ +\n\ +3,000,000 B (100.0%) -- explicit\n\ +├──2,999,700 B (99.99%) ── heap-unclassified\n\ +└────────300 B (00.01%) ── foobar\n\ +\n\ +Other Measurements\n\ +\n\ +3,000,000 B ── heap-allocated\n\ +\n\ +End of P (pid NNN)\n\ +web (pid NNN)\n\ +Explicit Allocations\n\ +\n\ +2,000,000 B (100.0%) -- explicit\n\ +├──1,000,000 B (50.00%) ── a/c/d\n\ +└──1,000,000 B (50.00%) ── heap-unclassified\n\ +\n\ +Other Measurements\n\ +\n\ +2,000,000 B ── heap-allocated\n\ +\n\ +End of web (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 }, + + // This diffs a non-Fission and a Fission memory report. + { filename: "fiss-diff1.json", filename2: "fiss-diff2.json", expected: expectedDiffFiss, dumpFirst: false, verbose: true } + ]; + + SimpleTest.waitForFocus(chain(frames)); + + SimpleTest.waitForExplicitFinish(); + ]]> + </script> +</window> diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory4.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory4.xhtml new file mode 100644 index 0000000000..91edc7cbb3 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory4.xhtml @@ -0,0 +1,176 @@ +<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script 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 = SpecialPowers.Services.dirsvc.get("CurWorkD", Ci.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.addEventListener("load", function onFrameLoad(e) { + 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(); + }, {once: true}); + } + + // 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)); } + } + 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 = +"\ +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\ +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\ +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\ +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.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory5.xhtml new file mode 100644 index 0000000000..dcec9620aa --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory5.xhtml @@ -0,0 +1,169 @@ +<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script 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"; + + 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 + ]; + + SpecialPowers.pushPrefEnv({"set": prefs}).then(function() { + for (let i = 0; i < numRemotes; i++) { + let w = remotes[i] = window.browsingContext.topChromeWindow.open("remote.xhtml", "", "chrome"); + + w.addEventListener("load", function 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 = SpecialPowers.Services.dirsvc.get("CurWorkD", Ci.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, + /* minimize memory usage = */ 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 [wW]eb /g); + } + + let socketProcessRunning = 0; + if (SpecialPowers.Services.io.socketProcessLaunched) { + socketProcessRunning = 1; + } + + let m2 = (vsizes.length == (4 + socketProcessRunning) && + endOfBrowsers.length == 3); + ok(m2, "three content 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.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory6.xhtml new file mode 100644 index 0000000000..a3a76532e7 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory6.xhtml @@ -0,0 +1,85 @@ +<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script 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"; + + 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.nsIFile); + 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_aboutmemory7.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory7.xhtml new file mode 100644 index 0000000000..28872cd516 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory7.xhtml @@ -0,0 +1,215 @@ +<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <!-- This file tests filtering 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"; + + 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 fakeReporters = [ + { collectReports(aCbObj, aClosure, aAnonymize) { + function f(aP, aK, aA) { + aCbObj.callback("Main Process", 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("explicit/h/i", HEAP, 10 * MB); + f("explicit/h/i2", HEAP, 9 * MB); + f("explicit/j/k", HEAP, 0.5 * MB); + f("explicit/j/k2", 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 testClick(aId, aExpected, aNext) { + let win = document.getElementById("amFrame").contentWindow; + + win.document.getElementById(aId).click(); + + testClipboard(aExpected, aNext, 0); + } + + // Apply the specified filter, then cut+paste the entire page and + // check that the cut text matches what we expect. + function testFilter(aFilterString, aRegEx, aExpected, aNext) { + let win = document.getElementById("amFrame").contentWindow; + + let filterInput = win.document.querySelector(".filterInput"); + let filterRegExCheckbox = + win.document.querySelector(".filterInput + * input[type=checkbox]"); + + filterInput.value = aFilterString; + filterRegExCheckbox.checked = aRegEx; + + // Dispatch a synthetic input event, since assigning to .value above + // doesn't trigger this. + filterInput.dispatchEvent(new win.Event("input")); + + // about:memory delays 300 ms before applying the filter, so we wait a + // a bit longer than that before checking the clipboard. + testClipboard(aExpected, aNext, /* delay */ 600); + } + + function testClipboard(aExpected, aNext, aDelay) { + setTimeout(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(); + } + ); + }, aDelay); + } + + // Returns a function that chains together one test() call per id. + function chain(aIds) { + let x = aIds.shift(); + if (x) { + if (x.click) { + return function() { testClick(x.click, x.expected, chain(aIds)); } + } + return function() { testFilter(x.filter, x.regex, x.expected, chain(aIds)); } + } + 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 acFilterExpected = +"\ +Main Process\n\ +Explicit Allocations\n\ +\n\ +40.00 MB (100.0%) -- explicit\n\ +└──40.00 MB (100.0%) -- a/c\n\ + ├──25.00 MB (62.50%) ── d\n\ + └──15.00 MB (37.50%) ── e\n\ +\n\ +End of Main Process\n\ +"; + + let hjFilterExpected = +"\ +Main Process\n\ +Explicit Allocations\n\ +\n\ +19.80 MB (100.0%) -- explicit\n\ +├──19.00 MB (95.96%) -- h\n\ +│ ├──10.00 MB (50.51%) ── i\n\ +│ └───9.00 MB (45.45%) ── i2\n\ +└───0.80 MB (04.04%) -- j\n\ + ├──0.50 MB (02.53%) ── k\n\ + └──0.30 MB (01.52%) ── k2\n\ +\n\ +End of Main Process\n\ +"; + + let filtersToApplyOrIdsToClick = [ + { click: "measureButton", expected: startExpected }, + { filter: "a/c", regex: false, expected: acFilterExpected }, + { filter: "/[hj]", regex: false, expected: "No results found." }, + { filter: "/[hj]", regex: true, expected: hjFilterExpected }, + ]; + + SimpleTest.waitForFocus(chain(filtersToApplyOrIdsToClick)); + + SimpleTest.waitForExplicitFinish(); + ]]> + </script> +</window> diff --git a/toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xhtml b/toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xhtml new file mode 100644 index 0000000000..9f3cdfe774 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xhtml @@ -0,0 +1,90 @@ +<?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 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[ + + 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]]}).then(function() { + for (let i = 0; i < numRemotes; i++) { + let w = remotes[i] = window.browsingContext.topChromeWindow.open("remote.xhtml", "", "chrome"); + + w.addEventListener("load", function 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); + }, {once: 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(gcLog, ccLog, isParent) { + if (isParent) { + numParents++; + } else { + numChildren++; + } + checkAndRemoveLog(gcLog); + checkAndRemoveLog(ccLog); + }, + onFinish() { + 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.xhtml b/toolkit/components/aboutmemory/tests/test_memoryReporters.xhtml new file mode 100644 index 0000000000..1f563eec5b --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_memoryReporters.xhtml @@ -0,0 +1,427 @@ +<?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 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.xhtml 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 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. We will create these strings in the tenured heap to + // avoid nursery string deduplication. + let shortStrings = []; + let newString = Cu.getJSTestingFunctions().newString; + for (let i = 0; i < 10000; i++) { + let str = (Math.random() > 0.5 ? "!" : "@") + ")(*&"; + shortStrings.push(newString(str, {tenured: true})); + } + + // Strings in the nursery are not reported, so make sure the above test + // strings are tenured (not all are created tenured). + Cu.forceGC(); + + let mySandbox = Cu.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\/.*realm\(/) >= 0) { + present.jsNonWindowRealms = true; + } else if (aPath.search(/^explicit\/window-objects\/top\(.*\/js-realm\(/) >= 0) { + present.windowObjectsJsRealms = 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\/atoms\/dynamic-objects-and-chars$/) >= 0) { + present.dynamicObjectsAndChars = 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", + "JSMainRuntimeRealmsSystem", + "JSMainRuntimeRealmsUser", + "imagesContentUsedUncompressed", + "storageSQLite", + "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) + { + let socketProcessRunning = 0; + if (SpecialPowers.Services.io.socketProcessLaunched) { + socketProcessRunning = 1; + } + ok(aAmounts.length == (1 + socketProcessRunning), + 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. + // eslint-disable-next-line no-unused-vars + 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.jsNonWindowRealms, "js-non-window realms are present"); + ok(present.windowObjectsJsRealms, "window-objects/.../js realms are present"); + ok(present.places, "places is present"); + ok(present.images, "images is present"); + ok(present.dynamicObjectsAndChars, "dynamic-objects-and-chars 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(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); + } + } + }, + // nsIHandleReportCallback + callback(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(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.includes("NS_ERROR_"), + "WrappedJS reporter got rejected: " + ex); + } + + runNext(); + } + + // Kick-off the async tests. + runNext(); + + ]]> + </script> +</window> diff --git a/toolkit/components/aboutmemory/tests/test_memoryReporters2.xhtml b/toolkit/components/aboutmemory/tests/test_memoryReporters2.xhtml new file mode 100644 index 0000000000..d457c15164 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_memoryReporters2.xhtml @@ -0,0 +1,109 @@ +<?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 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[ + + SimpleTest.waitForExplicitFinish(); + let socketProcessRunning = 0; + if (SpecialPowers.Services.io.socketProcessLaunched) { + socketProcessRunning = 1; + } + let numToOpen = 3; + const expectedNumRemotes = numToOpen + socketProcessRunning; + 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]]}).then(function() { + for (let i = 0; i < numToOpen; i++) { + let w = remotes[i] = window.browsingContext.topChromeWindow.open("remote.xhtml", "", "chrome"); + + w.addEventListener("load", function 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 == numToOpen) { + // All the remote processes are ready. Do memory reporting. + doReports(); + } + }); + mm.loadFrameScript("data:," + encodeURI("sendAsyncMessage('test:ready');"), true); + }, {once: 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 < numToOpen; i++) { + remotes[i].close(); + } + + // Check the results. + + let processes = Object.keys(residents); + ok(processes.length == expectedNumRemotes + 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 (") || + (processes[i].startsWith("Socket (") && socketProcessRunning) + || processes[i].startsWith("web ("), + "correct non-empty process name prefix: " + processes[i]); + numNonEmptyProcesses++; + } + } + ok(numEmptyProcesses == 1, "correct empty process name count"); + ok(numNonEmptyProcesses == expectedNumRemotes, + "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.xhtml b/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xhtml new file mode 100644 index 0000000000..206cee5434 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xhtml @@ -0,0 +1,46 @@ +<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script 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. + + SimpleTest.waitForExplicitFinish(); + + // Make a fake DB file. + let file = SpecialPowers.Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("test_sqliteMultiReporter-fake-DB-tmp.sqlite"); + + // Open and close the DB. + let db = SpecialPowers.Services.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> diff --git a/toolkit/components/aboutmemory/tests/xpcshell/test_gpuprocess.js b/toolkit/components/aboutmemory/tests/xpcshell/test_gpuprocess.js new file mode 100644 index 0000000000..1e55f9c024 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/xpcshell/test_gpuprocess.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function run_test() { + let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService( + Ci.nsIMemoryReporterManager + ); + + let ok = gfxInfo.controlGPUProcessForXPCShell(true); + Assert.equal(ok, true); + + let endTesting = function() { + gfxInfo.controlGPUProcessForXPCShell(false); + do_test_finished(); + }; + + let foundGPUProcess = false; + let onHandleReport = function( + aProcess, + aPath, + aKind, + aUnits, + aAmount, + aDescription + ) { + if (/GPU \(pid \d+\)/.test(aProcess)) { + foundGPUProcess = true; + } + }; + let onFinishReporting = function() { + Assert.equal(foundGPUProcess, true); + endTesting(); + }; + + mgr.getReports(onHandleReport, null, onFinishReporting, null, false); + do_test_pending(); +} diff --git a/toolkit/components/aboutmemory/tests/xpcshell/xpcshell.ini b/toolkit/components/aboutmemory/tests/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..3a0598bf49 --- /dev/null +++ b/toolkit/components/aboutmemory/tests/xpcshell/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +head = +skip-if = toolkit != 'windows' + +[test_gpuprocess.js] |