michael@0: /* -*- Mode: js2; tab-width: 8; indent-tabs-mode: nil; js2-basic-offset: 2 -*-*/
michael@0: /* vim: set ts=8 sts=2 et sw=2 tw=80: */
michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: // You can direct about:memory to immediately load memory reports from a file
michael@0: // by providing a file= query string. For example,
michael@0: //
michael@0: // about:memory?file=/home/username/reports.json.gz
michael@0: //
michael@0: // "file=" is not case-sensitive. We'll URI-unescape the contents of the
michael@0: // "file=" argument, and obviously the filename is case-sensitive iff you're on
michael@0: // a case-sensitive filesystem. If you specify more than one "file=" argument,
michael@0: // only the first one is used.
michael@0:
michael@0: "use strict";
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: const Cc = Components.classes;
michael@0: const Ci = Components.interfaces;
michael@0: const Cu = Components.utils;
michael@0: const CC = Components.Constructor;
michael@0:
michael@0: const KIND_NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
michael@0: const KIND_HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
michael@0: const KIND_OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
michael@0: const UNITS_BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
michael@0: const UNITS_COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
michael@0: const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
michael@0: const UNITS_PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
michael@0:
michael@0: Cu.import("resource://gre/modules/Services.jsm");
michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0:
michael@0: XPCOMUtils.defineLazyGetter(this, "nsBinaryStream",
michael@0: () => CC("@mozilla.org/binaryinputstream;1",
michael@0: "nsIBinaryInputStream",
michael@0: "setInputStream"));
michael@0: XPCOMUtils.defineLazyGetter(this, "nsFile",
michael@0: () => CC("@mozilla.org/file/local;1",
michael@0: "nsIFile", "initWithPath"));
michael@0: XPCOMUtils.defineLazyGetter(this, "nsGzipConverter",
michael@0: () => CC("@mozilla.org/streamconv;1?from=gzip&to=uncompressed",
michael@0: "nsIStreamConverter"));
michael@0:
michael@0: let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
michael@0: .getService(Ci.nsIMemoryReporterManager);
michael@0:
michael@0: let gUnnamedProcessStr = "Main Process";
michael@0:
michael@0: let gIsDiff = false;
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: // Forward slashes in URLs in paths are represented with backslashes to avoid
michael@0: // being mistaken for path separators. Paths/names where this hasn't been
michael@0: // undone are prefixed with "unsafe"; the rest are prefixed with "safe".
michael@0: function flipBackslashes(aUnsafeStr)
michael@0: {
michael@0: // Save memory by only doing the replacement if it's necessary.
michael@0: return (aUnsafeStr.indexOf('\\') === -1)
michael@0: ? aUnsafeStr
michael@0: : aUnsafeStr.replace(/\\/g, '/');
michael@0: }
michael@0:
michael@0: const gAssertionFailureMsgPrefix = "aboutMemory.js assertion failed: ";
michael@0:
michael@0: // This is used for things that should never fail, and indicate a defect in
michael@0: // this file if they do.
michael@0: function assert(aCond, aMsg)
michael@0: {
michael@0: if (!aCond) {
michael@0: reportAssertionFailure(aMsg)
michael@0: throw(gAssertionFailureMsgPrefix + aMsg);
michael@0: }
michael@0: }
michael@0:
michael@0: // This is used for malformed input from memory reporters.
michael@0: function assertInput(aCond, aMsg)
michael@0: {
michael@0: if (!aCond) {
michael@0: throw "Invalid memory report(s): " + aMsg;
michael@0: }
michael@0: }
michael@0:
michael@0: function handleException(ex)
michael@0: {
michael@0: let str = ex.toString();
michael@0: if (str.startsWith(gAssertionFailureMsgPrefix)) {
michael@0: // Argh, assertion failure within this file! Give up.
michael@0: throw ex;
michael@0: } else {
michael@0: // File or memory reporter problem. Print a message.
michael@0: updateMainAndFooter(ex.toString(), HIDE_FOOTER, "badInputWarning");
michael@0: }
michael@0: }
michael@0:
michael@0: function reportAssertionFailure(aMsg)
michael@0: {
michael@0: let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
michael@0: if (debug.isDebugBuild) {
michael@0: debug.assertion(aMsg, "false", "aboutMemory.js", 0);
michael@0: }
michael@0: }
michael@0:
michael@0: function debug(x)
michael@0: {
michael@0: let section = appendElement(document.body, 'div', 'section');
michael@0: appendElementWithText(section, "div", "debug", JSON.stringify(x));
michael@0: }
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: function onUnload()
michael@0: {
michael@0: }
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: // The
holding everything but the header and footer (if they're present).
michael@0: // It's what is updated each time the page changes.
michael@0: let gMain;
michael@0:
michael@0: // The
holding the footer.
michael@0: let gFooter;
michael@0:
michael@0: // The "verbose" checkbox.
michael@0: let gVerbose;
michael@0:
michael@0: // Values for the second argument to updateMainAndFooter.
michael@0: let HIDE_FOOTER = 0;
michael@0: let SHOW_FOOTER = 1;
michael@0:
michael@0: function updateMainAndFooter(aMsg, aFooterAction, aClassName)
michael@0: {
michael@0: // Clear gMain by replacing it with an empty node.
michael@0: let tmp = gMain.cloneNode(false);
michael@0: gMain.parentNode.replaceChild(tmp, gMain);
michael@0: gMain = tmp;
michael@0:
michael@0: gMain.classList.remove('hidden');
michael@0: gMain.classList.remove('verbose');
michael@0: gMain.classList.remove('non-verbose');
michael@0: if (gVerbose) {
michael@0: gMain.classList.add(gVerbose.checked ? 'verbose' : 'non-verbose');
michael@0: }
michael@0:
michael@0: if (aMsg) {
michael@0: let className = "section"
michael@0: if (aClassName) {
michael@0: className = className + " " + aClassName;
michael@0: }
michael@0: appendElementWithText(gMain, 'div', className, aMsg);
michael@0: }
michael@0:
michael@0: switch (aFooterAction) {
michael@0: case HIDE_FOOTER: gFooter.classList.add('hidden'); break;
michael@0: case SHOW_FOOTER: gFooter.classList.remove('hidden'); break;
michael@0: default: assertInput(false, "bad footer action in updateMainAndFooter");
michael@0: }
michael@0: }
michael@0:
michael@0: function appendTextNode(aP, aText)
michael@0: {
michael@0: let e = document.createTextNode(aText);
michael@0: aP.appendChild(e);
michael@0: return e;
michael@0: }
michael@0:
michael@0: function appendElement(aP, aTagName, aClassName)
michael@0: {
michael@0: let e = document.createElement(aTagName);
michael@0: if (aClassName) {
michael@0: e.className = aClassName;
michael@0: }
michael@0: aP.appendChild(e);
michael@0: return e;
michael@0: }
michael@0:
michael@0: function appendElementWithText(aP, aTagName, aClassName, aText)
michael@0: {
michael@0: let e = appendElement(aP, aTagName, aClassName);
michael@0: // Setting textContent clobbers existing children, but there are none. More
michael@0: // importantly, it avoids creating a JS-land object for the node, saving
michael@0: // memory.
michael@0: e.textContent = aText;
michael@0: return e;
michael@0: }
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: const explicitTreeDescription =
michael@0: "This tree covers explicit memory allocations by the application. It includes \
michael@0: \n\n\
michael@0: * allocations made at the operating system level (via calls to functions such as \
michael@0: VirtualAlloc, vm_allocate, and mmap), \
michael@0: \n\n\
michael@0: * allocations made at the heap allocation level (via functions such as malloc, \
michael@0: calloc, realloc, memalign, operator new, and operator new[]) that have not been \
michael@0: explicitly decommitted (i.e. evicted from memory and swap), and \
michael@0: \n\n\
michael@0: * where possible, the overhead of the heap allocator itself.\
michael@0: \n\n\
michael@0: It excludes memory that is mapped implicitly such as code and data segments, \
michael@0: and thread stacks. \
michael@0: \n\n\
michael@0: 'explicit' is not guaranteed to cover every explicit allocation, but it does cover \
michael@0: most (including the entire heap), and therefore it is the single best number to \
michael@0: focus on when trying to reduce memory usage.";
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: function appendButton(aP, aTitle, aOnClick, aText, aId)
michael@0: {
michael@0: let b = appendElementWithText(aP, "button", "", aText);
michael@0: b.title = aTitle;
michael@0: b.onclick = aOnClick;
michael@0: if (aId) {
michael@0: b.id = aId;
michael@0: }
michael@0: return b;
michael@0: }
michael@0:
michael@0: function appendHiddenFileInput(aP, aId, aChangeListener)
michael@0: {
michael@0: let input = appendElementWithText(aP, "input", "hidden", "");
michael@0: input.type = "file";
michael@0: input.id = aId; // used in testing
michael@0: input.addEventListener("change", aChangeListener);
michael@0: return input;
michael@0: }
michael@0:
michael@0: function onLoad()
michael@0: {
michael@0: // Generate the header.
michael@0:
michael@0: let header = appendElement(document.body, "div", "ancillary");
michael@0:
michael@0: // A hidden file input element that can be invoked when necessary.
michael@0: let fileInput1 = appendHiddenFileInput(header, "fileInput1", function() {
michael@0: let file = this.files[0];
michael@0: let filename = file.mozFullPath;
michael@0: updateAboutMemoryFromFile(filename);
michael@0: });
michael@0:
michael@0: // Ditto.
michael@0: let fileInput2 =
michael@0: appendHiddenFileInput(header, "fileInput2", function(e) {
michael@0: let file = this.files[0];
michael@0: // First time around, we stash a copy of the filename and reinvoke. Second
michael@0: // time around we do the diff and display.
michael@0: if (!this.filename1) {
michael@0: this.filename1 = file.mozFullPath;
michael@0:
michael@0: // e.skipClick is only true when testing -- it allows fileInput2's
michael@0: // onchange handler to be re-called without having to go via the file
michael@0: // picker.
michael@0: if (!e.skipClick) {
michael@0: this.click();
michael@0: }
michael@0: } else {
michael@0: let filename1 = this.filename1;
michael@0: delete this.filename1;
michael@0: updateAboutMemoryFromTwoFiles(filename1, file.mozFullPath);
michael@0: }
michael@0: });
michael@0:
michael@0: const CuDesc = "Measure current memory reports and show.";
michael@0: const LdDesc = "Load memory reports from file and show.";
michael@0: const DfDesc = "Load memory report data from two files and show the " +
michael@0: "difference.";
michael@0: const RdDesc = "Read memory reports from the clipboard and show.";
michael@0:
michael@0: const SvDesc = "Save memory reports to file.";
michael@0:
michael@0: const GCDesc = "Do a global garbage collection.";
michael@0: const CCDesc = "Do a cycle collection.";
michael@0: const MMDesc = "Send three \"heap-minimize\" notifications in a " +
michael@0: "row. Each notification triggers a global garbage " +
michael@0: "collection followed by a cycle collection, and causes the " +
michael@0: "process to reduce memory usage in other ways, e.g. by " +
michael@0: "flushing various caches.";
michael@0:
michael@0: const GCAndCCLogDesc = "Save garbage collection log and concise cycle " +
michael@0: "collection log.\n" +
michael@0: "WARNING: These logs may be large (>1GB).";
michael@0: const GCAndCCAllLogDesc = "Save garbage collection log and verbose cycle " +
michael@0: "collection log.\n" +
michael@0: "WARNING: These logs may be large (>1GB).";
michael@0:
michael@0: let ops = appendElement(header, "div", "");
michael@0:
michael@0: let row1 = appendElement(ops, "div", "opsRow");
michael@0:
michael@0: let labelDiv =
michael@0: appendElementWithText(row1, "div", "opsRowLabel", "Show memory reports");
michael@0: let label = appendElementWithText(labelDiv, "label", "");
michael@0: gVerbose = appendElement(label, "input", "");
michael@0: gVerbose.type = "checkbox";
michael@0: gVerbose.id = "verbose"; // used for testing
michael@0:
michael@0: appendTextNode(label, "verbose");
michael@0:
michael@0: const kEllipsis = "\u2026";
michael@0:
michael@0: // The "measureButton" id is used for testing.
michael@0: appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton");
michael@0: appendButton(row1, LdDesc, () => fileInput1.click(), "Load" + kEllipsis);
michael@0: appendButton(row1, DfDesc, () => fileInput2.click(),
michael@0: "Load and diff" + kEllipsis);
michael@0: appendButton(row1, RdDesc, updateAboutMemoryFromClipboard,
michael@0: "Read from clipboard");
michael@0:
michael@0: let row2 = appendElement(ops, "div", "opsRow");
michael@0:
michael@0: appendElementWithText(row2, "div", "opsRowLabel", "Save memory reports");
michael@0: appendButton(row2, SvDesc, saveReportsToFile, "Measure and save" + kEllipsis);
michael@0:
michael@0: let row3 = appendElement(ops, "div", "opsRow");
michael@0:
michael@0: appendElementWithText(row3, "div", "opsRowLabel", "Free memory");
michael@0: appendButton(row3, GCDesc, doGC, "GC");
michael@0: appendButton(row3, CCDesc, doCC, "CC");
michael@0: appendButton(row3, MMDesc, doMMU, "Minimize memory usage");
michael@0:
michael@0: let row4 = appendElement(ops, "div", "opsRow");
michael@0:
michael@0: appendElementWithText(row4, "div", "opsRowLabel", "Save GC & CC logs");
michael@0: appendButton(row4, GCAndCCLogDesc,
michael@0: saveGCLogAndConciseCCLog, "Save concise", 'saveLogsConcise');
michael@0: appendButton(row4, GCAndCCAllLogDesc,
michael@0: saveGCLogAndVerboseCCLog, "Save verbose", 'saveLogsVerbose');
michael@0:
michael@0: // Generate the main div, where content ("section" divs) will go. It's
michael@0: // hidden at first.
michael@0:
michael@0: gMain = appendElement(document.body, 'div', '');
michael@0: gMain.id = 'mainDiv';
michael@0:
michael@0: // Generate the footer. It's hidden at first.
michael@0:
michael@0: gFooter = appendElement(document.body, 'div', 'ancillary hidden');
michael@0:
michael@0: let a = appendElementWithText(gFooter, "a", "option",
michael@0: "Troubleshooting information");
michael@0: a.href = "about:support";
michael@0:
michael@0: let legendText1 = "Click on a non-leaf node in a tree to expand ('++') " +
michael@0: "or collapse ('--') its children.";
michael@0: let legendText2 = "Hover the pointer over the name of a memory report " +
michael@0: "to see a description of what it measures.";
michael@0:
michael@0: appendElementWithText(gFooter, "div", "legend", legendText1);
michael@0: appendElementWithText(gFooter, "div", "legend hiddenOnMobile", legendText2);
michael@0:
michael@0: // See if we're loading from a file. (Because about:memory is a non-standard
michael@0: // URL, location.search is undefined, so we have to use location.href
michael@0: // instead.)
michael@0: let search = location.href.split('?')[1];
michael@0: if (search) {
michael@0: let searchSplit = search.split('&');
michael@0: for (let i = 0; i < searchSplit.length; i++) {
michael@0: if (searchSplit[i].toLowerCase().startsWith('file=')) {
michael@0: let filename = searchSplit[i].substring('file='.length);
michael@0: updateAboutMemoryFromFile(decodeURIComponent(filename));
michael@0: return;
michael@0: }
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: function doGC()
michael@0: {
michael@0: Services.obs.notifyObservers(null, "child-gc-request", null);
michael@0: Cu.forceGC();
michael@0: updateMainAndFooter("Garbage collection completed", HIDE_FOOTER);
michael@0: }
michael@0:
michael@0: function doCC()
michael@0: {
michael@0: Services.obs.notifyObservers(null, "child-cc-request", null);
michael@0: window.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0: .getInterface(Ci.nsIDOMWindowUtils)
michael@0: .cycleCollect();
michael@0: updateMainAndFooter("Cycle collection completed", HIDE_FOOTER);
michael@0: }
michael@0:
michael@0: function doMMU()
michael@0: {
michael@0: Services.obs.notifyObservers(null, "child-mmu-request", null);
michael@0: gMgr.minimizeMemoryUsage(
michael@0: () => updateMainAndFooter("Memory minimization completed", HIDE_FOOTER));
michael@0: }
michael@0:
michael@0: function doMeasure()
michael@0: {
michael@0: updateAboutMemoryFromReporters();
michael@0: }
michael@0:
michael@0: function saveGCLogAndConciseCCLog()
michael@0: {
michael@0: dumpGCLogAndCCLog(false);
michael@0: }
michael@0:
michael@0: function saveGCLogAndVerboseCCLog()
michael@0: {
michael@0: dumpGCLogAndCCLog(true);
michael@0: }
michael@0:
michael@0: function dumpGCLogAndCCLog(aVerbose)
michael@0: {
michael@0: let gcLogPath = {};
michael@0: let ccLogPath = {};
michael@0:
michael@0: let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
michael@0: .getService(Ci.nsIMemoryInfoDumper);
michael@0:
michael@0: updateMainAndFooter("Saving logs...", HIDE_FOOTER);
michael@0:
michael@0: dumper.dumpGCAndCCLogsToFile("", aVerbose, /* dumpChildProcesses = */ false,
michael@0: gcLogPath, ccLogPath);
michael@0:
michael@0: updateMainAndFooter("", HIDE_FOOTER);
michael@0: let section = appendElement(gMain, 'div', "section");
michael@0: appendElementWithText(section, 'div', "",
michael@0: "Saved GC log to " + gcLogPath.value);
michael@0:
michael@0: let ccLogType = aVerbose ? "verbose" : "concise";
michael@0: appendElementWithText(section, 'div', "",
michael@0: "Saved " + ccLogType + " CC log to " + ccLogPath.value);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Top-level function that does the work of generating the page from the memory
michael@0: * reporters.
michael@0: */
michael@0: function updateAboutMemoryFromReporters()
michael@0: {
michael@0: updateMainAndFooter("Measuring...", HIDE_FOOTER);
michael@0:
michael@0: try {
michael@0: let processLiveMemoryReports =
michael@0: function(aHandleReport, aDisplayReports) {
michael@0: let handleReport = function(aProcess, aUnsafePath, aKind, aUnits,
michael@0: aAmount, aDescription) {
michael@0: aHandleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
michael@0: aDescription, /* presence = */ undefined);
michael@0: }
michael@0:
michael@0: let displayReportsAndFooter = function() {
michael@0: updateMainAndFooter("", SHOW_FOOTER);
michael@0: aDisplayReports();
michael@0: }
michael@0:
michael@0: gMgr.getReports(handleReport, null,
michael@0: displayReportsAndFooter, null);
michael@0: }
michael@0:
michael@0: // Process the reports from the live memory reporters.
michael@0: appendAboutMemoryMain(processLiveMemoryReports,
michael@0: gMgr.hasMozMallocUsableSize);
michael@0:
michael@0: } catch (ex) {
michael@0: handleException(ex);
michael@0: }
michael@0: }
michael@0:
michael@0: // Increment this if the JSON format changes.
michael@0: //
michael@0: var gCurrentFileFormatVersion = 1;
michael@0:
michael@0: /**
michael@0: * Populate about:memory using the data in the given JSON object.
michael@0: *
michael@0: * @param aObj
michael@0: * An object containing JSON data that (hopefully!) conforms to the
michael@0: * schema used by nsIMemoryInfoDumper.
michael@0: */
michael@0: function updateAboutMemoryFromJSONObject(aObj)
michael@0: {
michael@0: try {
michael@0: assertInput(aObj.version === gCurrentFileFormatVersion,
michael@0: "data version number missing or doesn't match");
michael@0: assertInput(aObj.hasMozMallocUsableSize !== undefined,
michael@0: "missing 'hasMozMallocUsableSize' property");
michael@0: assertInput(aObj.reports && aObj.reports instanceof Array,
michael@0: "missing or non-array 'reports' property");
michael@0:
michael@0: let processMemoryReportsFromFile =
michael@0: function(aHandleReport, aDisplayReports) {
michael@0: for (let i = 0; i < aObj.reports.length; i++) {
michael@0: let r = aObj.reports[i];
michael@0:
michael@0: // A hack: for a brief time (late in the FF26 and early in the FF27
michael@0: // cycle) we were dumping memory report files that contained reports
michael@0: // whose path began with "redundant/". Such reports were ignored by
michael@0: // about:memory. These reports are no longer produced, but some older
michael@0: // builds are still floating around and producing files that contain
michael@0: // them, so we need to still handle them (i.e. ignore them). This hack
michael@0: // can be removed once FF26 and associated products (e.g. B2G 1.2) are
michael@0: // no longer in common use.
michael@0: if (!r.path.startsWith("redundant/")) {
michael@0: aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
michael@0: r.description, r._presence);
michael@0: }
michael@0: }
michael@0: aDisplayReports();
michael@0: }
michael@0: appendAboutMemoryMain(processMemoryReportsFromFile,
michael@0: aObj.hasMozMallocUsableSize);
michael@0: } catch (ex) {
michael@0: handleException(ex);
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Populate about:memory using the data in the given JSON string.
michael@0: *
michael@0: * @param aStr
michael@0: * A string containing JSON data conforming to the schema used by
michael@0: * nsIMemoryReporterManager::dumpReports.
michael@0: */
michael@0: function updateAboutMemoryFromJSONString(aStr)
michael@0: {
michael@0: try {
michael@0: let obj = JSON.parse(aStr);
michael@0: updateAboutMemoryFromJSONObject(obj);
michael@0: } catch (ex) {
michael@0: handleException(ex);
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Loads the contents of a file into a string and passes that to a callback.
michael@0: *
michael@0: * @param aFilename
michael@0: * The name of the file being read from.
michael@0: * @param aFn
michael@0: * The function to call and pass the read string to upon completion.
michael@0: */
michael@0: function loadMemoryReportsFromFile(aFilename, aFn)
michael@0: {
michael@0: updateMainAndFooter("Loading...", HIDE_FOOTER);
michael@0:
michael@0: try {
michael@0: let reader = new FileReader();
michael@0: reader.onerror = () => { throw "FileReader.onerror"; };
michael@0: reader.onabort = () => { throw "FileReader.onabort"; };
michael@0: reader.onload = (aEvent) => {
michael@0: updateMainAndFooter("", SHOW_FOOTER); // Clear "Loading..." from above.
michael@0: aFn(aEvent.target.result);
michael@0: };
michael@0:
michael@0: // If it doesn't have a .gz suffix, read it as a (legacy) ungzipped file.
michael@0: if (!aFilename.endsWith(".gz")) {
michael@0: reader.readAsText(new File(aFilename));
michael@0: return;
michael@0: }
michael@0:
michael@0: // Read compressed gzip file.
michael@0: let converter = new nsGzipConverter();
michael@0: converter.asyncConvertData("gzip", "uncompressed", {
michael@0: data: [],
michael@0: onStartRequest: function(aR, aC) {},
michael@0: onDataAvailable: function(aR, aC, aStream, aO, aCount) {
michael@0: let bi = new nsBinaryStream(aStream);
michael@0: this.data.push(bi.readBytes(aCount));
michael@0: },
michael@0: onStopRequest: function(aR, aC, aStatusCode) {
michael@0: try {
michael@0: if (!Components.isSuccessCode(aStatusCode)) {
michael@0: throw aStatusCode;
michael@0: }
michael@0: reader.readAsText(new Blob(this.data));
michael@0: } catch (ex) {
michael@0: handleException(ex);
michael@0: }
michael@0: }
michael@0: }, null);
michael@0:
michael@0: let file = new nsFile(aFilename);
michael@0: let fileChan = Services.io.newChannelFromURI(Services.io.newFileURI(file));
michael@0: fileChan.asyncOpen(converter, null);
michael@0:
michael@0: } catch (ex) {
michael@0: handleException(ex);
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Like updateAboutMemoryFromReporters(), but gets its data from a file instead
michael@0: * of the memory reporters.
michael@0: *
michael@0: * @param aFilename
michael@0: * The name of the file being read from. The expected format of the
michael@0: * file's contents is described in a comment in nsIMemoryInfoDumper.idl.
michael@0: */
michael@0: function updateAboutMemoryFromFile(aFilename)
michael@0: {
michael@0: loadMemoryReportsFromFile(aFilename,
michael@0: updateAboutMemoryFromJSONString);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Like updateAboutMemoryFromFile(), but gets its data from a two files and
michael@0: * diffs them.
michael@0: *
michael@0: * @param aFilename1
michael@0: * The name of the first file being read from.
michael@0: * @param aFilename2
michael@0: * The name of the first file being read from.
michael@0: */
michael@0: function updateAboutMemoryFromTwoFiles(aFilename1, aFilename2)
michael@0: {
michael@0: loadMemoryReportsFromFile(aFilename1, function(aStr1) {
michael@0: loadMemoryReportsFromFile(aFilename2, function f2(aStr2) {
michael@0: try {
michael@0: let obj1 = JSON.parse(aStr1);
michael@0: let obj2 = JSON.parse(aStr2);
michael@0: gIsDiff = true;
michael@0: updateAboutMemoryFromJSONObject(diffJSONObjects(obj1, obj2));
michael@0: gIsDiff = false;
michael@0: } catch (ex) {
michael@0: handleException(ex);
michael@0: }
michael@0: });
michael@0: });
michael@0: }
michael@0:
michael@0: /**
michael@0: * Like updateAboutMemoryFromFile(), but gets its data from the clipboard
michael@0: * instead of a file.
michael@0: */
michael@0: function updateAboutMemoryFromClipboard()
michael@0: {
michael@0: // Get the clipboard's contents.
michael@0: let transferable = Cc["@mozilla.org/widget/transferable;1"]
michael@0: .createInstance(Ci.nsITransferable);
michael@0: let loadContext = window.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0: .getInterface(Ci.nsIWebNavigation)
michael@0: .QueryInterface(Ci.nsILoadContext);
michael@0: transferable.init(loadContext);
michael@0: transferable.addDataFlavor('text/unicode');
michael@0: Services.clipboard.getData(transferable, Ci.nsIClipboard.kGlobalClipboard);
michael@0:
michael@0: var cbData = {};
michael@0: try {
michael@0: transferable.getTransferData('text/unicode', cbData,
michael@0: /* out dataLen (ignored) */ {});
michael@0: let cbString = cbData.value.QueryInterface(Ci.nsISupportsString).data;
michael@0:
michael@0: // Success! Now use the string to generate about:memory.
michael@0: updateAboutMemoryFromJSONString(cbString);
michael@0:
michael@0: } catch (ex) {
michael@0: handleException(ex);
michael@0: }
michael@0: }
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: // Something unlikely to appear in a process name.
michael@0: let kProcessPathSep = "^:^:^";
michael@0:
michael@0: // Short for "diff report".
michael@0: function DReport(aKind, aUnits, aAmount, aDescription, aNMerged, aPresence)
michael@0: {
michael@0: this._kind = aKind;
michael@0: this._units = aUnits;
michael@0: this._amount = aAmount;
michael@0: this._description = aDescription;
michael@0: this._nMerged = aNMerged;
michael@0: if (aPresence !== undefined) {
michael@0: this._presence = aPresence;
michael@0: }
michael@0: }
michael@0:
michael@0: DReport.prototype = {
michael@0: assertCompatible: function(aKind, aUnits)
michael@0: {
michael@0: assert(this._kind == aKind, "Mismatched kinds");
michael@0: assert(this._units == aUnits, "Mismatched units");
michael@0:
michael@0: // We don't check that the "description" properties match. This is because
michael@0: // on Linux we can get cases where the paths are the same but the
michael@0: // descriptions differ, like this:
michael@0: //
michael@0: // "path": "size/other-files/icon-theme.cache/[r--p]",
michael@0: // "description": "/usr/share/icons/gnome/icon-theme.cache (read-only, not executable, private)"
michael@0: //
michael@0: // "path": "size/other-files/icon-theme.cache/[r--p]"
michael@0: // "description": "/usr/share/icons/hicolor/icon-theme.cache (read-only, not executable, private)"
michael@0: //
michael@0: // In those cases, we just use the description from the first-encountered
michael@0: // one, which is what about:memory also does.
michael@0: // (Note: reports with those paths are no longer generated, but allowing
michael@0: // the descriptions to differ seems reasonable.)
michael@0: },
michael@0:
michael@0: merge: function(aJr) {
michael@0: this.assertCompatible(aJr.kind, aJr.units);
michael@0: this._amount += aJr.amount;
michael@0: this._nMerged++;
michael@0: },
michael@0:
michael@0: toJSON: function(aProcess, aPath, aAmount) {
michael@0: return {
michael@0: process: aProcess,
michael@0: path: aPath,
michael@0: kind: this._kind,
michael@0: units: this._units,
michael@0: amount: aAmount,
michael@0: description: this._description,
michael@0: _presence: this._presence
michael@0: };
michael@0: }
michael@0: };
michael@0:
michael@0: // Constants that indicate if a DReport was present only in one of the data
michael@0: // sets, or had to be added for balance.
michael@0: DReport.PRESENT_IN_FIRST_ONLY = 1;
michael@0: DReport.PRESENT_IN_SECOND_ONLY = 2;
michael@0: DReport.ADDED_FOR_BALANCE = 3;
michael@0:
michael@0: /**
michael@0: * Make a report map, which has combined path+process strings for keys, and
michael@0: * DReport objects for values.
michael@0: *
michael@0: * @param aJSONReports
michael@0: * The |reports| field of a JSON object.
michael@0: * @return The constructed report map.
michael@0: */
michael@0: function makeDReportMap(aJSONReports)
michael@0: {
michael@0: let dreportMap = {};
michael@0: for (let i = 0; i < aJSONReports.length; i++) {
michael@0: let jr = aJSONReports[i];
michael@0:
michael@0: assert(jr.process !== undefined, "Missing process");
michael@0: assert(jr.path !== undefined, "Missing path");
michael@0: assert(jr.kind !== undefined, "Missing kind");
michael@0: assert(jr.units !== undefined, "Missing units");
michael@0: assert(jr.amount !== undefined, "Missing amount");
michael@0: assert(jr.description !== undefined, "Missing description");
michael@0:
michael@0: // Strip out some non-deterministic stuff that prevents clean diffs --
michael@0: // e.g. PIDs, addresses.
michael@0: let strippedProcess = jr.process.replace(/pid \d+/, "pid NNN");
michael@0: let strippedPath = jr.path.replace(/0x[0-9A-Fa-f]+/, "0xNNN");
michael@0: let processPath = strippedProcess + kProcessPathSep + strippedPath;
michael@0:
michael@0: let rOld = dreportMap[processPath];
michael@0: if (rOld === undefined) {
michael@0: dreportMap[processPath] =
michael@0: new DReport(jr.kind, jr.units, jr.amount, jr.description, 1, undefined);
michael@0: } else {
michael@0: rOld.merge(jr);
michael@0: }
michael@0: }
michael@0: return dreportMap;
michael@0: }
michael@0:
michael@0: // Return a new dreportMap which is the diff of two dreportMaps. Empties
michael@0: // aDReportMap2 along the way.
michael@0: function diffDReportMaps(aDReportMap1, aDReportMap2)
michael@0: {
michael@0: let result = {};
michael@0:
michael@0: for (let processPath in aDReportMap1) {
michael@0: let r1 = aDReportMap1[processPath];
michael@0: let r2 = aDReportMap2[processPath];
michael@0: let r2_amount, r2_nMerged;
michael@0: let presence;
michael@0: if (r2 !== undefined) {
michael@0: r1.assertCompatible(r2._kind, r2._units);
michael@0: r2_amount = r2._amount;
michael@0: r2_nMerged = r2._nMerged;
michael@0: delete aDReportMap2[processPath];
michael@0: presence = undefined; // represents that it's present in both
michael@0: } else {
michael@0: r2_amount = 0;
michael@0: r2_nMerged = 0;
michael@0: presence = DReport.PRESENT_IN_FIRST_ONLY;
michael@0: }
michael@0: result[processPath] =
michael@0: new DReport(r1._kind, r1._units, r2_amount - r1._amount, r1._description,
michael@0: Math.max(r1._nMerged, r2_nMerged), presence);
michael@0: }
michael@0:
michael@0: for (let processPath in aDReportMap2) {
michael@0: let r2 = aDReportMap2[processPath];
michael@0: result[processPath] = new DReport(r2._kind, r2._units, r2._amount,
michael@0: r2._description, r2._nMerged,
michael@0: DReport.PRESENT_IN_SECOND_ONLY);
michael@0: }
michael@0:
michael@0: return result;
michael@0: }
michael@0:
michael@0: function makeJSONReports(aDReportMap)
michael@0: {
michael@0: let reports = [];
michael@0: for (let processPath in aDReportMap) {
michael@0: let r = aDReportMap[processPath];
michael@0: if (r._amount !== 0) {
michael@0: // If _nMerged > 1, we give the full (aggregated) amount in the first
michael@0: // copy, and then use amount=0 in the remainder. When viewed in
michael@0: // about:memory, this shows up as an entry with a "[2]"-style suffix
michael@0: // and the correct amount.
michael@0: let split = processPath.split(kProcessPathSep);
michael@0: assert(split.length >= 2);
michael@0: let process = split.shift();
michael@0: let path = split.join();
michael@0: reports.push(r.toJSON(process, path, r._amount));
michael@0: for (let i = 1; i < r._nMerged; i++) {
michael@0: reports.push(r.toJSON(process, path, 0));
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: return reports;
michael@0: }
michael@0:
michael@0:
michael@0: // Diff two JSON objects holding memory reports.
michael@0: function diffJSONObjects(aJson1, aJson2)
michael@0: {
michael@0: function simpleProp(aProp)
michael@0: {
michael@0: assert(aJson1[aProp] !== undefined && aJson1[aProp] === aJson2[aProp],
michael@0: aProp + " properties don't match");
michael@0: return aJson1[aProp];
michael@0: }
michael@0:
michael@0: return {
michael@0: version: simpleProp("version"),
michael@0:
michael@0: hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"),
michael@0:
michael@0: reports: makeJSONReports(diffDReportMaps(makeDReportMap(aJson1.reports),
michael@0: makeDReportMap(aJson2.reports)))
michael@0: };
michael@0: }
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: // |PColl| is short for "process collection".
michael@0: function PColl()
michael@0: {
michael@0: this._trees = {};
michael@0: this._degenerates = {};
michael@0: this._heapTotal = 0;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Processes reports (whether from reporters or from a file) and append the
michael@0: * main part of the page.
michael@0: *
michael@0: * @param aProcessReports
michael@0: * Function that extracts the memory reports from the reporters or from
michael@0: * file.
michael@0: * @param aHasMozMallocUsableSize
michael@0: * Boolean indicating if moz_malloc_usable_size works.
michael@0: */
michael@0: function appendAboutMemoryMain(aProcessReports, aHasMozMallocUsableSize)
michael@0: {
michael@0: let pcollsByProcess = {};
michael@0:
michael@0: function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
michael@0: aDescription, aPresence)
michael@0: {
michael@0: if (aUnsafePath.startsWith("explicit/")) {
michael@0: assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP,
michael@0: "bad explicit kind");
michael@0: assertInput(aUnits === UNITS_BYTES, "bad explicit units");
michael@0: }
michael@0:
michael@0: assert(aPresence === undefined ||
michael@0: aPresence == DReport.PRESENT_IN_FIRST_ONLY ||
michael@0: aPresence == DReport.PRESENT_IN_SECOND_ONLY,
michael@0: "bad presence");
michael@0:
michael@0: let process = aProcess === "" ? gUnnamedProcessStr : aProcess;
michael@0: let unsafeNames = aUnsafePath.split('/');
michael@0: let unsafeName0 = unsafeNames[0];
michael@0: let isDegenerate = unsafeNames.length === 1;
michael@0:
michael@0: // Get the PColl table for the process, creating it if necessary.
michael@0: let pcoll = pcollsByProcess[process];
michael@0: if (!pcollsByProcess[process]) {
michael@0: pcoll = pcollsByProcess[process] = new PColl();
michael@0: }
michael@0:
michael@0: // Get the root node, creating it if necessary.
michael@0: let psubcoll = isDegenerate ? pcoll._degenerates : pcoll._trees;
michael@0: let t = psubcoll[unsafeName0];
michael@0: if (!t) {
michael@0: t = psubcoll[unsafeName0] =
michael@0: new TreeNode(unsafeName0, aUnits, isDegenerate);
michael@0: }
michael@0:
michael@0: if (!isDegenerate) {
michael@0: // Add any missing nodes in the tree implied by aUnsafePath, and fill in
michael@0: // the properties that we can with a top-down traversal.
michael@0: for (let i = 1; i < unsafeNames.length; i++) {
michael@0: let unsafeName = unsafeNames[i];
michael@0: let u = t.findKid(unsafeName);
michael@0: if (!u) {
michael@0: u = new TreeNode(unsafeName, aUnits, isDegenerate);
michael@0: if (!t._kids) {
michael@0: t._kids = [];
michael@0: }
michael@0: t._kids.push(u);
michael@0: }
michael@0: t = u;
michael@0: }
michael@0:
michael@0: // Update the heap total if necessary.
michael@0: if (unsafeName0 === "explicit" && aKind == KIND_HEAP) {
michael@0: pcollsByProcess[process]._heapTotal += aAmount;
michael@0: }
michael@0: }
michael@0:
michael@0: if (t._amount) {
michael@0: // Duplicate! Sum the values and mark it as a dup.
michael@0: t._amount += aAmount;
michael@0: t._nMerged = t._nMerged ? t._nMerged + 1 : 2;
michael@0: assert(t._presence === aPresence, "presence mismatch");
michael@0: } else {
michael@0: // New leaf node. Fill in extra node details from the report.
michael@0: t._amount = aAmount;
michael@0: t._description = aDescription;
michael@0: if (aPresence !== undefined) {
michael@0: t._presence = aPresence;
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: function displayReports()
michael@0: {
michael@0: // Sort the processes.
michael@0: let processes = Object.keys(pcollsByProcess);
michael@0: processes.sort(function(aProcessA, aProcessB) {
michael@0: assert(aProcessA != aProcessB,
michael@0: "Elements of Object.keys() should be unique, but " +
michael@0: "saw duplicate '" + aProcessA + "' elem.");
michael@0:
michael@0: // Always put the main process first.
michael@0: if (aProcessA == gUnnamedProcessStr) {
michael@0: return -1;
michael@0: }
michael@0: if (aProcessB == gUnnamedProcessStr) {
michael@0: return 1;
michael@0: }
michael@0:
michael@0: // Then sort by resident size.
michael@0: let nodeA = pcollsByProcess[aProcessA]._degenerates['resident'];
michael@0: let nodeB = pcollsByProcess[aProcessB]._degenerates['resident'];
michael@0: let residentA = nodeA ? nodeA._amount : -1;
michael@0: let residentB = nodeB ? nodeB._amount : -1;
michael@0:
michael@0: if (residentA > residentB) {
michael@0: return -1;
michael@0: }
michael@0: if (residentA < residentB) {
michael@0: return 1;
michael@0: }
michael@0:
michael@0: // Then sort by process name.
michael@0: if (aProcessA < aProcessB) {
michael@0: return -1;
michael@0: }
michael@0: if (aProcessA > aProcessB) {
michael@0: return 1;
michael@0: }
michael@0:
michael@0: return 0;
michael@0: });
michael@0:
michael@0: // Generate output for each process.
michael@0: for (let i = 0; i < processes.length; i++) {
michael@0: let process = processes[i];
michael@0: let section = appendElement(gMain, 'div', 'section');
michael@0:
michael@0: appendProcessAboutMemoryElements(section, i, process,
michael@0: pcollsByProcess[process]._trees,
michael@0: pcollsByProcess[process]._degenerates,
michael@0: pcollsByProcess[process]._heapTotal,
michael@0: aHasMozMallocUsableSize);
michael@0: }
michael@0: }
michael@0:
michael@0: aProcessReports(handleReport, displayReports);
michael@0: }
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: // There are two kinds of TreeNode.
michael@0: // - Leaf TreeNodes correspond to reports.
michael@0: // - Non-leaf TreeNodes are just scaffolding nodes for the tree; their values
michael@0: // are derived from their children.
michael@0: // Some trees are "degenerate", i.e. they contain a single node, i.e. they
michael@0: // correspond to a report whose path has no '/' separators.
michael@0: function TreeNode(aUnsafeName, aUnits, aIsDegenerate)
michael@0: {
michael@0: this._units = aUnits;
michael@0: this._unsafeName = aUnsafeName;
michael@0: if (aIsDegenerate) {
michael@0: this._isDegenerate = true;
michael@0: }
michael@0:
michael@0: // Leaf TreeNodes have these properties added immediately after construction:
michael@0: // - _amount
michael@0: // - _description
michael@0: // - _nMerged (only defined if > 1)
michael@0: // - _presence (only defined if value is PRESENT_IN_{FIRST,SECOND}_ONLY)
michael@0: //
michael@0: // Non-leaf TreeNodes have these properties added later:
michael@0: // - _kids
michael@0: // - _amount
michael@0: // - _description
michael@0: // - _hideKids (only defined if true)
michael@0: }
michael@0:
michael@0: TreeNode.prototype = {
michael@0: findKid: function(aUnsafeName) {
michael@0: if (this._kids) {
michael@0: for (let i = 0; i < this._kids.length; i++) {
michael@0: if (this._kids[i]._unsafeName === aUnsafeName) {
michael@0: return this._kids[i];
michael@0: }
michael@0: }
michael@0: }
michael@0: return undefined;
michael@0: },
michael@0:
michael@0: toString: function() {
michael@0: switch (this._units) {
michael@0: case UNITS_BYTES: return formatBytes(this._amount);
michael@0: case UNITS_COUNT:
michael@0: case UNITS_COUNT_CUMULATIVE: return formatInt(this._amount);
michael@0: case UNITS_PERCENTAGE: return formatPercentage(this._amount);
michael@0: default:
michael@0: assertInput(false, "bad units in TreeNode.toString");
michael@0: }
michael@0: }
michael@0: };
michael@0:
michael@0: // Sort TreeNodes first by size, then by name. This is particularly important
michael@0: // for the about:memory tests, which need a predictable ordering of reporters
michael@0: // which have the same amount.
michael@0: TreeNode.compareAmounts = function(aA, aB) {
michael@0: let a, b;
michael@0: if (gIsDiff) {
michael@0: a = Math.abs(aA._amount);
michael@0: b = Math.abs(aB._amount);
michael@0: } else {
michael@0: a = aA._amount;
michael@0: b = aB._amount;
michael@0: }
michael@0: if (a > b) {
michael@0: return -1;
michael@0: }
michael@0: if (a < b) {
michael@0: return 1;
michael@0: }
michael@0: return TreeNode.compareUnsafeNames(aA, aB);
michael@0: };
michael@0:
michael@0: TreeNode.compareUnsafeNames = function(aA, aB) {
michael@0: return aA._unsafeName < aB._unsafeName ? -1 :
michael@0: aA._unsafeName > aB._unsafeName ? 1 :
michael@0: 0;
michael@0: };
michael@0:
michael@0:
michael@0: /**
michael@0: * Fill in the remaining properties for the specified tree in a bottom-up
michael@0: * fashion.
michael@0: *
michael@0: * @param aRoot
michael@0: * The tree root.
michael@0: */
michael@0: function fillInTree(aRoot)
michael@0: {
michael@0: // Fill in the remaining properties bottom-up.
michael@0: function fillInNonLeafNodes(aT)
michael@0: {
michael@0: if (!aT._kids) {
michael@0: // Leaf node. Has already been filled in.
michael@0:
michael@0: } else if (aT._kids.length === 1 && aT != aRoot) {
michael@0: // Non-root, non-leaf node with one child. Merge the child with the node
michael@0: // to avoid redundant entries.
michael@0: let kid = aT._kids[0];
michael@0: let kidBytes = fillInNonLeafNodes(kid);
michael@0: aT._unsafeName += '/' + kid._unsafeName;
michael@0: if (kid._kids) {
michael@0: aT._kids = kid._kids;
michael@0: } else {
michael@0: delete aT._kids;
michael@0: }
michael@0: aT._amount = kid._amount;
michael@0: aT._description = kid._description;
michael@0: if (kid._nMerged !== undefined) {
michael@0: aT._nMerged = kid._nMerged
michael@0: }
michael@0: assert(!aT._hideKids && !kid._hideKids, "_hideKids set when merging");
michael@0:
michael@0: } else {
michael@0: // Non-leaf node with multiple children. Derive its _amount and
michael@0: // _description entirely from its children...
michael@0: let kidsBytes = 0;
michael@0: for (let i = 0; i < aT._kids.length; i++) {
michael@0: kidsBytes += fillInNonLeafNodes(aT._kids[i]);
michael@0: }
michael@0:
michael@0: // ... except in one special case. When diffing two memory report sets,
michael@0: // if one set has a node with children and the other has the same node
michael@0: // but without children -- e.g. the first has "a/b/c" and "a/b/d", but
michael@0: // the second only has "a/b" -- we need to add a fake node "a/b/(fake)"
michael@0: // to the second to make the trees comparable. It's ugly, but it works.
michael@0: if (aT._amount !== undefined &&
michael@0: (aT._presence === DReport.PRESENT_IN_FIRST_ONLY ||
michael@0: aT._presence === DReport.PRESENT_IN_SECOND_ONLY)) {
michael@0: aT._amount += kidsBytes;
michael@0: let fake = new TreeNode('(fake child)', aT._units);
michael@0: fake._presence = DReport.ADDED_FOR_BALANCE;
michael@0: fake._amount = aT._amount - kidsBytes;
michael@0: aT._kids.push(fake);
michael@0: delete aT._presence;
michael@0: } else {
michael@0: assert(aT._amount === undefined,
michael@0: "_amount already set for non-leaf node")
michael@0: aT._amount = kidsBytes;
michael@0: }
michael@0: aT._description = "The sum of all entries below this one.";
michael@0: }
michael@0: return aT._amount;
michael@0: }
michael@0:
michael@0: // cannotMerge is set because don't want to merge into a tree's root node.
michael@0: fillInNonLeafNodes(aRoot);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Compute the "heap-unclassified" value and insert it into the "explicit"
michael@0: * tree.
michael@0: *
michael@0: * @param aT
michael@0: * The "explicit" tree.
michael@0: * @param aHeapAllocatedNode
michael@0: * The "heap-allocated" tree node.
michael@0: * @param aHeapTotal
michael@0: * The sum of all explicit HEAP reports for this process.
michael@0: * @return A boolean indicating if "heap-allocated" is known for the process.
michael@0: */
michael@0: function addHeapUnclassifiedNode(aT, aHeapAllocatedNode, aHeapTotal)
michael@0: {
michael@0: if (aHeapAllocatedNode === undefined)
michael@0: return false;
michael@0:
michael@0: assert(aHeapAllocatedNode._isDegenerate, "heap-allocated is not degenerate");
michael@0: let heapAllocatedBytes = aHeapAllocatedNode._amount;
michael@0: let heapUnclassifiedT = new TreeNode("heap-unclassified", UNITS_BYTES);
michael@0: heapUnclassifiedT._amount = heapAllocatedBytes - aHeapTotal;
michael@0: heapUnclassifiedT._description =
michael@0: "Memory not classified by a more specific report. This includes " +
michael@0: "slop bytes due to internal fragmentation in the heap allocator " +
michael@0: "(caused when the allocator rounds up request sizes).";
michael@0: aT._kids.push(heapUnclassifiedT);
michael@0: aT._amount += heapUnclassifiedT._amount;
michael@0: return true;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Sort all kid nodes from largest to smallest, and insert aggregate nodes
michael@0: * where appropriate.
michael@0: *
michael@0: * @param aTotalBytes
michael@0: * The size of the tree's root node.
michael@0: * @param aT
michael@0: * The tree.
michael@0: */
michael@0: function sortTreeAndInsertAggregateNodes(aTotalBytes, aT)
michael@0: {
michael@0: const kSignificanceThresholdPerc = 1;
michael@0:
michael@0: function isInsignificant(aT)
michael@0: {
michael@0: return !gVerbose.checked &&
michael@0: (100 * aT._amount / aTotalBytes) < kSignificanceThresholdPerc;
michael@0: }
michael@0:
michael@0: if (!aT._kids) {
michael@0: return;
michael@0: }
michael@0:
michael@0: aT._kids.sort(TreeNode.compareAmounts);
michael@0:
michael@0: // If the first child is insignificant, they all are, and there's no point
michael@0: // creating an aggregate node that lacks siblings. Just set the parent's
michael@0: // _hideKids property and process all children.
michael@0: if (isInsignificant(aT._kids[0])) {
michael@0: aT._hideKids = true;
michael@0: for (let i = 0; i < aT._kids.length; i++) {
michael@0: sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
michael@0: }
michael@0: return;
michael@0: }
michael@0:
michael@0: // Look at all children except the last one.
michael@0: let i;
michael@0: for (i = 0; i < aT._kids.length - 1; i++) {
michael@0: if (isInsignificant(aT._kids[i])) {
michael@0: // This child is below the significance threshold. If there are other
michael@0: // (smaller) children remaining, move them under an aggregate node.
michael@0: let i0 = i;
michael@0: let nAgg = aT._kids.length - i0;
michael@0: // Create an aggregate node. Inherit units from the parent; everything
michael@0: // in the tree should have the same units anyway (we test this later).
michael@0: let aggT = new TreeNode("(" + nAgg + " tiny)", aT._units);
michael@0: aggT._kids = [];
michael@0: let aggBytes = 0;
michael@0: for ( ; i < aT._kids.length; i++) {
michael@0: aggBytes += aT._kids[i]._amount;
michael@0: aggT._kids.push(aT._kids[i]);
michael@0: }
michael@0: aggT._hideKids = true;
michael@0: aggT._amount = aggBytes;
michael@0: aggT._description =
michael@0: nAgg + " sub-trees that are below the " + kSignificanceThresholdPerc +
michael@0: "% significance threshold.";
michael@0: aT._kids.splice(i0, nAgg, aggT);
michael@0: aT._kids.sort(TreeNode.compareAmounts);
michael@0:
michael@0: // Process the moved children.
michael@0: for (i = 0; i < aggT._kids.length; i++) {
michael@0: sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]);
michael@0: }
michael@0: return;
michael@0: }
michael@0:
michael@0: sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
michael@0: }
michael@0:
michael@0: // The first n-1 children were significant. Don't consider if the last child
michael@0: // is significant; there's no point creating an aggregate node that only has
michael@0: // one child. Just process it.
michael@0: sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
michael@0: }
michael@0:
michael@0: // Global variable indicating if we've seen any invalid values for this
michael@0: // process; it holds the unsafePaths of any such reports. It is reset for
michael@0: // each new process.
michael@0: let gUnsafePathsWithInvalidValuesForThisProcess = [];
michael@0:
michael@0: function appendWarningElements(aP, aHasKnownHeapAllocated,
michael@0: aHasMozMallocUsableSize)
michael@0: {
michael@0: if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) {
michael@0: appendElementWithText(aP, "p", "",
michael@0: "WARNING: the 'heap-allocated' memory reporter and the " +
michael@0: "moz_malloc_usable_size() function do not work for this platform " +
michael@0: "and/or configuration. This means that 'heap-unclassified' is not " +
michael@0: "shown and the 'explicit' tree shows much less memory than it should.\n\n");
michael@0:
michael@0: } else if (!aHasKnownHeapAllocated) {
michael@0: appendElementWithText(aP, "p", "",
michael@0: "WARNING: the 'heap-allocated' memory reporter does not work for this " +
michael@0: "platform and/or configuration. This means that 'heap-unclassified' " +
michael@0: "is not shown and the 'explicit' tree shows less memory than it should.\n\n");
michael@0:
michael@0: } else if (!aHasMozMallocUsableSize) {
michael@0: appendElementWithText(aP, "p", "",
michael@0: "WARNING: the moz_malloc_usable_size() function does not work for " +
michael@0: "this platform and/or configuration. This means that much of the " +
michael@0: "heap-allocated memory is not measured by individual memory reporters " +
michael@0: "and so will fall under 'heap-unclassified'.\n\n");
michael@0: }
michael@0:
michael@0: if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) {
michael@0: let div = appendElement(aP, "div");
michael@0: appendElementWithText(div, "p", "",
michael@0: "WARNING: the following values are negative or unreasonably large.\n");
michael@0:
michael@0: let ul = appendElement(div, "ul");
michael@0: for (let i = 0;
michael@0: i < gUnsafePathsWithInvalidValuesForThisProcess.length;
michael@0: i++)
michael@0: {
michael@0: appendTextNode(ul, " ");
michael@0: appendElementWithText(ul, "li", "",
michael@0: flipBackslashes(gUnsafePathsWithInvalidValuesForThisProcess[i]) + "\n");
michael@0: }
michael@0:
michael@0: appendElementWithText(div, "p", "",
michael@0: "This indicates a defect in one or more memory reporters. The " +
michael@0: "invalid values are highlighted.\n\n");
michael@0: gUnsafePathsWithInvalidValuesForThisProcess = []; // reset for the next process
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Appends the about:memory elements for a single process.
michael@0: *
michael@0: * @param aP
michael@0: * The parent DOM node.
michael@0: * @param aN
michael@0: * The number of the process, starting at 0.
michael@0: * @param aProcess
michael@0: * The name of the process.
michael@0: * @param aTrees
michael@0: * The table of non-degenerate trees for this process.
michael@0: * @param aDegenerates
michael@0: * The table of degenerate trees for this process.
michael@0: * @param aHasMozMallocUsableSize
michael@0: * Boolean indicating if moz_malloc_usable_size works.
michael@0: * @return The generated text.
michael@0: */
michael@0: function appendProcessAboutMemoryElements(aP, aN, aProcess, aTrees,
michael@0: aDegenerates, aHeapTotal,
michael@0: aHasMozMallocUsableSize)
michael@0: {
michael@0: const kUpwardsArrow = "\u2191",
michael@0: kDownwardsArrow = "\u2193";
michael@0:
michael@0: let appendLink = function(aHere, aThere, aArrow) {
michael@0: let link = appendElementWithText(aP, "a", "upDownArrow", aArrow);
michael@0: link.href = "#" + aThere + aN;
michael@0: link.id = aHere + aN;
michael@0: link.title = "Go to the " + aThere + " of " + aProcess;
michael@0: link.style = "text-decoration: none";
michael@0:
michael@0: // This jumps to the anchor without the page location getting the anchor
michael@0: // name tacked onto its end, which is what happens with a vanilla link.
michael@0: link.addEventListener("click", function(event) {
michael@0: document.documentElement.scrollTop =
michael@0: document.querySelector(event.target.href).offsetTop;
michael@0: event.preventDefault();
michael@0: }, false);
michael@0:
michael@0: // This gives nice spacing when we copy and paste.
michael@0: appendElementWithText(aP, "span", "", "\n");
michael@0: }
michael@0:
michael@0: appendElementWithText(aP, "h1", "", aProcess);
michael@0: appendLink("start", "end", kDownwardsArrow);
michael@0:
michael@0: // We'll fill this in later.
michael@0: let warningsDiv = appendElement(aP, "div", "accuracyWarning");
michael@0:
michael@0: // The explicit tree.
michael@0: let hasExplicitTree;
michael@0: let hasKnownHeapAllocated;
michael@0: {
michael@0: let treeName = "explicit";
michael@0: let t = aTrees[treeName];
michael@0: if (t) {
michael@0: let pre = appendSectionHeader(aP, "Explicit Allocations");
michael@0: hasExplicitTree = true;
michael@0: fillInTree(t);
michael@0: // Using the "heap-allocated" reporter here instead of
michael@0: // nsMemoryReporterManager.heapAllocated goes against the usual pattern.
michael@0: // But the "heap-allocated" node will go in the tree like the others, so
michael@0: // we have to deal with it, and once we're dealing with it, it's easier
michael@0: // to keep doing so rather than switching to the distinguished amount.
michael@0: hasKnownHeapAllocated =
michael@0: aDegenerates &&
michael@0: addHeapUnclassifiedNode(t, aDegenerates["heap-allocated"], aHeapTotal);
michael@0: sortTreeAndInsertAggregateNodes(t._amount, t);
michael@0: t._description = explicitTreeDescription;
michael@0: appendTreeElements(pre, t, aProcess, "");
michael@0: delete aTrees[treeName];
michael@0: }
michael@0: appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
michael@0: }
michael@0:
michael@0: // Fill in and sort all the non-degenerate other trees.
michael@0: let otherTrees = [];
michael@0: for (let unsafeName in aTrees) {
michael@0: let t = aTrees[unsafeName];
michael@0: assert(!t._isDegenerate, "tree is degenerate");
michael@0: fillInTree(t);
michael@0: sortTreeAndInsertAggregateNodes(t._amount, t);
michael@0: otherTrees.push(t);
michael@0: }
michael@0: otherTrees.sort(TreeNode.compareUnsafeNames);
michael@0:
michael@0: // Get the length of the longest root value among the degenerate other trees,
michael@0: // and sort them as well.
michael@0: let otherDegenerates = [];
michael@0: let maxStringLength = 0;
michael@0: for (let unsafeName in aDegenerates) {
michael@0: let t = aDegenerates[unsafeName];
michael@0: assert(t._isDegenerate, "tree is not degenerate");
michael@0: let length = t.toString().length;
michael@0: if (length > maxStringLength) {
michael@0: maxStringLength = length;
michael@0: }
michael@0: otherDegenerates.push(t);
michael@0: }
michael@0: otherDegenerates.sort(TreeNode.compareUnsafeNames);
michael@0:
michael@0: // Now generate the elements, putting non-degenerate trees first.
michael@0: let pre = appendSectionHeader(aP, "Other Measurements");
michael@0: for (let i = 0; i < otherTrees.length; i++) {
michael@0: let t = otherTrees[i];
michael@0: appendTreeElements(pre, t, aProcess, "");
michael@0: appendTextNode(pre, "\n"); // blank lines after non-degenerate trees
michael@0: }
michael@0: for (let i = 0; i < otherDegenerates.length; i++) {
michael@0: let t = otherDegenerates[i];
michael@0: let padText = pad("", maxStringLength - t.toString().length, ' ');
michael@0: appendTreeElements(pre, t, aProcess, padText);
michael@0: }
michael@0: appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
michael@0:
michael@0: // Add any warnings about inaccuracies in the "explicit" tree due to platform
michael@0: // limitations. These must be computed after generating all the text. The
michael@0: // newlines give nice spacing if we copy+paste into a text buffer.
michael@0: if (hasExplicitTree) {
michael@0: appendWarningElements(warningsDiv, hasKnownHeapAllocated,
michael@0: aHasMozMallocUsableSize);
michael@0: }
michael@0:
michael@0: appendElementWithText(aP, "h3", "", "End of " + aProcess);
michael@0: appendLink("end", "start", kUpwardsArrow);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Determines if a number has a negative sign when converted to a string.
michael@0: * Works even for -0.
michael@0: *
michael@0: * @param aN
michael@0: * The number.
michael@0: * @return A boolean.
michael@0: */
michael@0: function hasNegativeSign(aN)
michael@0: {
michael@0: if (aN === 0) { // this succeeds for 0 and -0
michael@0: return 1 / aN === -Infinity; // this succeeds for -0
michael@0: }
michael@0: return aN < 0;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Formats an int as a human-readable string.
michael@0: *
michael@0: * @param aN
michael@0: * The integer to format.
michael@0: * @param aExtra
michael@0: * An extra string to tack onto the end.
michael@0: * @return A human-readable string representing the int.
michael@0: *
michael@0: * Note: building an array of chars and converting that to a string with
michael@0: * Array.join at the end is more memory efficient than using string
michael@0: * concatenation. See bug 722972 for details.
michael@0: */
michael@0: function formatInt(aN, aExtra)
michael@0: {
michael@0: let neg = false;
michael@0: if (hasNegativeSign(aN)) {
michael@0: neg = true;
michael@0: aN = -aN;
michael@0: }
michael@0: let s = [];
michael@0: while (true) {
michael@0: let k = aN % 1000;
michael@0: aN = Math.floor(aN / 1000);
michael@0: if (aN > 0) {
michael@0: if (k < 10) {
michael@0: s.unshift(",00", k);
michael@0: } else if (k < 100) {
michael@0: s.unshift(",0", k);
michael@0: } else {
michael@0: s.unshift(",", k);
michael@0: }
michael@0: } else {
michael@0: s.unshift(k);
michael@0: break;
michael@0: }
michael@0: }
michael@0: if (neg) {
michael@0: s.unshift("-");
michael@0: }
michael@0: if (aExtra) {
michael@0: s.push(aExtra);
michael@0: }
michael@0: return s.join("");
michael@0: }
michael@0:
michael@0: /**
michael@0: * Converts a byte count to an appropriate string representation.
michael@0: *
michael@0: * @param aBytes
michael@0: * The byte count.
michael@0: * @return The string representation.
michael@0: */
michael@0: function formatBytes(aBytes)
michael@0: {
michael@0: let unit = gVerbose.checked ? " B" : " MB";
michael@0:
michael@0: let s;
michael@0: if (gVerbose.checked) {
michael@0: s = formatInt(aBytes, unit);
michael@0: } else {
michael@0: let mbytes = (aBytes / (1024 * 1024)).toFixed(2);
michael@0: let a = String(mbytes).split(".");
michael@0: // If the argument to formatInt() is -0, it will print the negative sign.
michael@0: s = formatInt(Number(a[0])) + "." + a[1] + unit;
michael@0: }
michael@0: return s;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Converts a percentage to an appropriate string representation.
michael@0: *
michael@0: * @param aPerc100x
michael@0: * The percentage, multiplied by 100 (see nsIMemoryReporter).
michael@0: * @return The string representation
michael@0: */
michael@0: function formatPercentage(aPerc100x)
michael@0: {
michael@0: return (aPerc100x / 100).toFixed(2) + "%";
michael@0: }
michael@0:
michael@0: /**
michael@0: * Right-justifies a string in a field of a given width, padding as necessary.
michael@0: *
michael@0: * @param aS
michael@0: * The string.
michael@0: * @param aN
michael@0: * The field width.
michael@0: * @param aC
michael@0: * The char used to pad.
michael@0: * @return The string representation.
michael@0: */
michael@0: function pad(aS, aN, aC)
michael@0: {
michael@0: let padding = "";
michael@0: let n2 = aN - aS.length;
michael@0: for (let i = 0; i < n2; i++) {
michael@0: padding += aC;
michael@0: }
michael@0: return padding + aS;
michael@0: }
michael@0:
michael@0: // There's a subset of the Unicode "light" box-drawing chars that is widely
michael@0: // implemented in terminals, and this code sticks to that subset to maximize
michael@0: // the chance that copying and pasting about:memory output to a terminal will
michael@0: // work correctly.
michael@0: const kHorizontal = "\u2500",
michael@0: kVertical = "\u2502",
michael@0: kUpAndRight = "\u2514",
michael@0: kUpAndRight_Right_Right = "\u2514\u2500\u2500",
michael@0: kVerticalAndRight = "\u251c",
michael@0: kVerticalAndRight_Right_Right = "\u251c\u2500\u2500",
michael@0: kVertical_Space_Space = "\u2502 ";
michael@0:
michael@0: const kNoKidsSep = " \u2500\u2500 ",
michael@0: kHideKidsSep = " ++ ",
michael@0: kShowKidsSep = " -- ";
michael@0:
michael@0: function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged,
michael@0: aPresence)
michael@0: {
michael@0: let safeName = flipBackslashes(aUnsafeName);
michael@0: if (!aIsInvalid && !aNMerged && !aPresence) {
michael@0: safeName += "\n";
michael@0: }
michael@0: let nameSpan = appendElementWithText(aP, "span", "mrName", safeName);
michael@0: nameSpan.title = aDescription;
michael@0:
michael@0: if (aIsInvalid) {
michael@0: let noteText = " [?!]";
michael@0: if (!aNMerged) {
michael@0: noteText += "\n";
michael@0: }
michael@0: let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
michael@0: noteSpan.title =
michael@0: "Warning: this value is invalid and indicates a bug in one or more " +
michael@0: "memory reporters. ";
michael@0: }
michael@0:
michael@0: if (aNMerged) {
michael@0: let noteText = " [" + aNMerged + "]";
michael@0: if (!aPresence) {
michael@0: noteText += "\n";
michael@0: }
michael@0: let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
michael@0: noteSpan.title =
michael@0: "This value is the sum of " + aNMerged +
michael@0: " memory reports that all have the same path.";
michael@0: }
michael@0:
michael@0: if (aPresence) {
michael@0: let c, title;
michael@0: switch (aPresence) {
michael@0: case DReport.PRESENT_IN_FIRST_ONLY:
michael@0: c = '-';
michael@0: title = "This value was only present in the first set of memory reports.";
michael@0: break;
michael@0: case DReport.PRESENT_IN_SECOND_ONLY:
michael@0: c = '+';
michael@0: title = "This value was only present in the second set of memory reports.";
michael@0: break;
michael@0: case DReport.ADDED_FOR_BALANCE:
michael@0: c = '!';
michael@0: title = "One of the sets of memory reports lacked children for this " +
michael@0: "node's parent. This is a fake child node added to make the " +
michael@0: "two memory sets comparable.";
michael@0: break;
michael@0: default: assert(false, "bad presence");
michael@0: break;
michael@0: }
michael@0: let noteSpan = appendElementWithText(aP, "span", "mrNote",
michael@0: " [" + c + "]\n");
michael@0: noteSpan.title = title;
michael@0: }
michael@0: }
michael@0:
michael@0: // This is used to record the (safe) IDs of which sub-trees have been manually
michael@0: // expanded (marked as true) and collapsed (marked as false). It's used to
michael@0: // replicate the collapsed/expanded state when the page is updated. It can end
michael@0: // up holding IDs of nodes that no longer exist, e.g. for compartments that
michael@0: // have been closed. This doesn't seem like a big deal, because the number is
michael@0: // limited by the number of entries the user has changed from their original
michael@0: // state.
michael@0: let gShowSubtreesBySafeTreeId = {};
michael@0:
michael@0: function assertClassListContains(e, className) {
michael@0: assert(e, "undefined " + className);
michael@0: assert(e.classList.contains(className), "classname isn't " + className);
michael@0: }
michael@0:
michael@0: function toggle(aEvent)
michael@0: {
michael@0: // This relies on each line being a span that contains at least four spans:
michael@0: // mrValue, mrPerc, mrSep, mrName, and then zero or more mrNotes. All
michael@0: // whitespace must be within one of these spans for this function to find the
michael@0: // right nodes. And the span containing the children of this line must
michael@0: // immediately follow. Assertions check this.
michael@0:
michael@0: // |aEvent.target| will be one of the spans. Get the outer span.
michael@0: let outerSpan = aEvent.target.parentNode;
michael@0: assertClassListContains(outerSpan, "hasKids");
michael@0:
michael@0: // Toggle the '++'/'--' separator.
michael@0: let isExpansion;
michael@0: let sepSpan = outerSpan.childNodes[2];
michael@0: assertClassListContains(sepSpan, "mrSep");
michael@0: if (sepSpan.textContent === kHideKidsSep) {
michael@0: isExpansion = true;
michael@0: sepSpan.textContent = kShowKidsSep;
michael@0: } else if (sepSpan.textContent === kShowKidsSep) {
michael@0: isExpansion = false;
michael@0: sepSpan.textContent = kHideKidsSep;
michael@0: } else {
michael@0: assert(false, "bad sepSpan textContent");
michael@0: }
michael@0:
michael@0: // Toggle visibility of the span containing this node's children.
michael@0: let subTreeSpan = outerSpan.nextSibling;
michael@0: assertClassListContains(subTreeSpan, "kids");
michael@0: subTreeSpan.classList.toggle("hidden");
michael@0:
michael@0: // Record/unrecord that this sub-tree was toggled.
michael@0: let safeTreeId = outerSpan.id;
michael@0: if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
michael@0: delete gShowSubtreesBySafeTreeId[safeTreeId];
michael@0: } else {
michael@0: gShowSubtreesBySafeTreeId[safeTreeId] = isExpansion;
michael@0: }
michael@0: }
michael@0:
michael@0: function expandPathToThisElement(aElement)
michael@0: {
michael@0: if (aElement.classList.contains("kids")) {
michael@0: // Unhide the kids.
michael@0: aElement.classList.remove("hidden");
michael@0: expandPathToThisElement(aElement.previousSibling); // hasKids
michael@0:
michael@0: } else if (aElement.classList.contains("hasKids")) {
michael@0: // Change the separator to '--'.
michael@0: let sepSpan = aElement.childNodes[2];
michael@0: assertClassListContains(sepSpan, "mrSep");
michael@0: sepSpan.textContent = kShowKidsSep;
michael@0: expandPathToThisElement(aElement.parentNode); // kids or pre.entries
michael@0:
michael@0: } else {
michael@0: assertClassListContains(aElement, "entries");
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Appends the elements for the tree, including its heading.
michael@0: *
michael@0: * @param aP
michael@0: * The parent DOM node.
michael@0: * @param aRoot
michael@0: * The tree root.
michael@0: * @param aProcess
michael@0: * The process the tree corresponds to.
michael@0: * @param aPadText
michael@0: * A string to pad the start of each entry.
michael@0: */
michael@0: function appendTreeElements(aP, aRoot, aProcess, aPadText)
michael@0: {
michael@0: /**
michael@0: * Appends the elements for a particular tree, without a heading.
michael@0: *
michael@0: * @param aP
michael@0: * The parent DOM node.
michael@0: * @param aProcess
michael@0: * The process the tree corresponds to.
michael@0: * @param aUnsafeNames
michael@0: * An array of the names forming the path to aT.
michael@0: * @param aRoot
michael@0: * The root of the tree this sub-tree belongs to.
michael@0: * @param aT
michael@0: * The tree.
michael@0: * @param aTreelineText1
michael@0: * The first part of the treeline for this entry and this entry's
michael@0: * children.
michael@0: * @param aTreelineText2a
michael@0: * The second part of the treeline for this entry.
michael@0: * @param aTreelineText2b
michael@0: * The second part of the treeline for this entry's children.
michael@0: * @param aParentStringLength
michael@0: * The length of the formatted byte count of the top node in the tree.
michael@0: */
michael@0: function appendTreeElements2(aP, aProcess, aUnsafeNames, aRoot, aT,
michael@0: aTreelineText1, aTreelineText2a,
michael@0: aTreelineText2b, aParentStringLength)
michael@0: {
michael@0: function appendN(aS, aC, aN)
michael@0: {
michael@0: for (let i = 0; i < aN; i++) {
michael@0: aS += aC;
michael@0: }
michael@0: return aS;
michael@0: }
michael@0:
michael@0: // The tree line. Indent more if this entry is narrower than its parent.
michael@0: let valueText = aT.toString();
michael@0: let extraTreelineLength =
michael@0: Math.max(aParentStringLength - valueText.length, 0);
michael@0: if (extraTreelineLength > 0) {
michael@0: aTreelineText2a =
michael@0: appendN(aTreelineText2a, kHorizontal, extraTreelineLength);
michael@0: aTreelineText2b =
michael@0: appendN(aTreelineText2b, " ", extraTreelineLength);
michael@0: }
michael@0: let treelineText = aTreelineText1 + aTreelineText2a;
michael@0: appendElementWithText(aP, "span", "treeline", treelineText);
michael@0:
michael@0: // Detect and record invalid values. But not if gIsDiff is true, because
michael@0: // we expect negative values in that case.
michael@0: assertInput(aRoot._units === aT._units,
michael@0: "units within a tree are inconsistent");
michael@0: let tIsInvalid = false;
michael@0: if (!gIsDiff && !(0 <= aT._amount && aT._amount <= aRoot._amount)) {
michael@0: tIsInvalid = true;
michael@0: let unsafePath = aUnsafeNames.join("/");
michael@0: gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath);
michael@0: reportAssertionFailure("Invalid value (" + aT._amount + " / " +
michael@0: aRoot._amount + ") for " +
michael@0: flipBackslashes(unsafePath));
michael@0: }
michael@0:
michael@0: // For non-leaf nodes, the entire sub-tree is put within a span so it can
michael@0: // be collapsed if the node is clicked on.
michael@0: let d;
michael@0: let sep;
michael@0: let showSubtrees;
michael@0: if (aT._kids) {
michael@0: // Determine if we should show the sub-tree below this entry; this
michael@0: // involves reinstating any previous toggling of the sub-tree.
michael@0: let unsafePath = aUnsafeNames.join("/");
michael@0: let safeTreeId = aProcess + ":" + flipBackslashes(unsafePath);
michael@0: showSubtrees = !aT._hideKids;
michael@0: if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
michael@0: showSubtrees = gShowSubtreesBySafeTreeId[safeTreeId];
michael@0: }
michael@0: d = appendElement(aP, "span", "hasKids");
michael@0: d.id = safeTreeId;
michael@0: d.onclick = toggle;
michael@0: sep = showSubtrees ? kShowKidsSep : kHideKidsSep;
michael@0: } else {
michael@0: assert(!aT._hideKids, "leaf node with _hideKids set")
michael@0: sep = kNoKidsSep;
michael@0: d = aP;
michael@0: }
michael@0:
michael@0: // The value.
michael@0: appendElementWithText(d, "span", "mrValue" + (tIsInvalid ? " invalid" : ""),
michael@0: valueText);
michael@0:
michael@0: // The percentage (omitted for single entries).
michael@0: let percText;
michael@0: if (!aT._isDegenerate) {
michael@0: // Treat 0 / 0 as 100%.
michael@0: let num = aRoot._amount === 0 ? 100 : (100 * aT._amount / aRoot._amount);
michael@0: let numText = num.toFixed(2);
michael@0: percText = numText === "100.00"
michael@0: ? " (100.0%)"
michael@0: : (0 <= num && num < 10 ? " (0" : " (") + numText + "%)";
michael@0: appendElementWithText(d, "span", "mrPerc", percText);
michael@0: }
michael@0:
michael@0: // The separator.
michael@0: appendElementWithText(d, "span", "mrSep", sep);
michael@0:
michael@0: // The entry's name.
michael@0: appendMrNameSpan(d, aT._description, aT._unsafeName,
michael@0: tIsInvalid, aT._nMerged, aT._presence);
michael@0:
michael@0: // In non-verbose mode, invalid nodes can be hidden in collapsed sub-trees.
michael@0: // But it's good to always see them, so force this.
michael@0: if (!gVerbose.checked && tIsInvalid) {
michael@0: expandPathToThisElement(d);
michael@0: }
michael@0:
michael@0: // Recurse over children.
michael@0: if (aT._kids) {
michael@0: // The 'kids' class is just used for sanity checking in toggle().
michael@0: d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden");
michael@0:
michael@0: let kidTreelineText1 = aTreelineText1 + aTreelineText2b;
michael@0: for (let i = 0; i < aT._kids.length; i++) {
michael@0: let kidTreelineText2a, kidTreelineText2b;
michael@0: if (i < aT._kids.length - 1) {
michael@0: kidTreelineText2a = kVerticalAndRight_Right_Right;
michael@0: kidTreelineText2b = kVertical_Space_Space;
michael@0: } else {
michael@0: kidTreelineText2a = kUpAndRight_Right_Right;
michael@0: kidTreelineText2b = " ";
michael@0: }
michael@0: aUnsafeNames.push(aT._kids[i]._unsafeName);
michael@0: appendTreeElements2(d, aProcess, aUnsafeNames, aRoot, aT._kids[i],
michael@0: kidTreelineText1, kidTreelineText2a,
michael@0: kidTreelineText2b, valueText.length);
michael@0: aUnsafeNames.pop();
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: let rootStringLength = aRoot.toString().length;
michael@0: appendTreeElements2(aP, aProcess, [aRoot._unsafeName], aRoot, aRoot,
michael@0: aPadText, "", "", rootStringLength);
michael@0: }
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: function appendSectionHeader(aP, aText)
michael@0: {
michael@0: appendElementWithText(aP, "h2", "", aText + "\n");
michael@0: return appendElement(aP, "pre", "entries");
michael@0: }
michael@0:
michael@0: //---------------------------------------------------------------------------
michael@0:
michael@0: function saveReportsToFile()
michael@0: {
michael@0: let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
michael@0: fp.appendFilter("Zipped JSON files", "*.json.gz");
michael@0: fp.appendFilters(Ci.nsIFilePicker.filterAll);
michael@0: fp.filterIndex = 0;
michael@0: fp.addToRecentDocs = true;
michael@0: fp.defaultString = "memory-report.json.gz";
michael@0:
michael@0: let fpFinish = function(file) {
michael@0: let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
michael@0: .getService(Ci.nsIMemoryInfoDumper);
michael@0:
michael@0: let finishDumping = () => {
michael@0: updateMainAndFooter("Saved reports to " + file.path, HIDE_FOOTER);
michael@0: }
michael@0:
michael@0: dumper.dumpMemoryReportsToNamedFile(file.path, finishDumping, null);
michael@0: }
michael@0:
michael@0: let fpCallback = function(aResult) {
michael@0: if (aResult == Ci.nsIFilePicker.returnOK ||
michael@0: aResult == Ci.nsIFilePicker.returnReplace) {
michael@0: fpFinish(fp.file);
michael@0: }
michael@0: };
michael@0:
michael@0: try {
michael@0: fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave);
michael@0: } catch(ex) {
michael@0: // This will fail on Android, since there is no Save as file picker there.
michael@0: // Just save to the default downloads dir if it does.
michael@0: let file = Services.dirsvc.get("DfltDwnld", Ci.nsIFile);
michael@0: file.append(fp.defaultString);
michael@0: fpFinish(file);
michael@0: return;
michael@0: }
michael@0: fp.open(fpCallback);
michael@0: }