/* -*- Mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil; -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is about:memory * * The Initial Developer of the Original Code is the Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2009 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Vladimir Vukicevic * Nicholas Nethercote * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ "use strict"; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; // Must use .href here instead of .search because "about:memory" is a // non-standard URL. var gVerbose = (location.href.split(/[\?,]/).indexOf("verbose") !== -1); var gAddedObserver = false; 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 kUnknown = -1; // used for _amount if a memory reporter failed function onLoad() { var os = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); os.notifyObservers(null, "child-memory-reporter-request", null); os.addObserver(ChildMemoryListener, "child-memory-reporter-update", false); gAddedObserver = true; update(); } function onUnload() { // We need to check if the observer has been added before removing; in some // circumstances (eg. reloading the page quickly) it might not have because // onLoad might not fire. if (gAddedObserver) { var os = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); os.removeObserver(ChildMemoryListener, "child-memory-reporter-update"); } } function ChildMemoryListener(aSubject, aTopic, aData) { update(); } function $(n) { return document.getElementById(n); } function doGlobalGC() { Cu.forceGC(); update(); } function doGlobalGCandCC() { window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .garbageCollect(); update(); } // For maximum effect, this returns to the event loop between each // notification. See bug 610166 comment 12 for an explanation. // Ideally a single notification would be enough. function sendHeapMinNotifications() { function runSoon(f) { var tm = Cc["@mozilla.org/thread-manager;1"] .getService(Ci.nsIThreadManager); tm.mainThread.dispatch({ run: f }, Ci.nsIThread.DISPATCH_NORMAL); } function sendHeapMinNotificationsInner() { var os = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); os.notifyObservers(null, "memory-pressure", "heap-minimize"); if (++j < 3) runSoon(sendHeapMinNotificationsInner); else runSoon(update); } var j = 0; sendHeapMinNotificationsInner(); } /** * Top-level function that does the work of generating the page. */ function update() { // First, clear the page contents. Necessary because update() might be // called more than once due to ChildMemoryListener. var content = $("content"); content.parentNode.replaceChild(content.cloneNode(false), content); content = $("content"); var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]. getService(Ci.nsIMemoryReporterManager); // Process each memory reporter: // - Make a copy of it into a sub-table indexed by its process. Each copy // looks like this: // // interface Reporter { // _path: string; // _kind: number; // _units: number; // _amount: number; // _description: string; // } // // After this point we never use the original memory reporter again. // // - Note that copying rOrig.amount (which calls a C++ function under the // IDL covers) to r._amount for every reporter now means that the // results as consistent as possible -- measurements are made all at // once before most of the memory required to generate this page is // allocated. var reportersByProcess = {}; function addReporter(aProcess, aPath, aKind, aUnits, aAmount, aDescription) { var process = aProcess === "" ? "Main" : aProcess; var r = { _path: aPath, _kind: aKind, _units: aUnits, _amount: aAmount, _description: aDescription }; if (!reportersByProcess[process]) { reportersByProcess[process] = {}; } var reporters = reportersByProcess[process]; if (reporters[r._path]) { // Already an entry; must be a duplicated reporter. This can // happen legitimately. Sum the values. reporters[r._path]._amount += r._amount; } else { reporters[r._path] = r; } } // Process vanilla reporters first, then multi-reporters. var e = mgr.enumerateReporters(); while (e.hasMoreElements()) { var rOrig = e.getNext().QueryInterface(Ci.nsIMemoryReporter); addReporter(rOrig.process, rOrig.path, rOrig.kind, rOrig.units, rOrig.amount, rOrig.description); } var e = mgr.enumerateMultiReporters(); while (e.hasMoreElements()) { var r = e.getNext().QueryInterface(Ci.nsIMemoryMultiReporter); r.collectReports(addReporter, null); } // Generate output for one process at a time. Always start with the // Main process. var text = genProcessText("Main", reportersByProcess["Main"]); for (var process in reportersByProcess) { if (process !== "Main") { text += genProcessText(process, reportersByProcess[process]); } } // Memory-related actions. const GCDesc = "Do a global garbage collection."; // XXX: once bug 625302 is fixed, should change this button to just do a CC. const CCDesc = "Do a global garbage collection followed by a cycle " + "collection. (It currently is not possible to do a cycle " + "collection on its own, see bug 625302.)"; const MPDesc = "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."; text += "
" + "" + "" + "" + "
"; // Generate verbosity option link at the bottom. text += "
"; text += gVerbose ? "Less verbose" : "More verbose"; text += "
"; text += "
" + "Hover the pointer over the name of a memory " + "reporter to see a detailed description of what it measures." "
"; var div = document.createElement("div"); div.innerHTML = text; content.appendChild(div); } // Compare two memory reporter nodes. The primary sort is on the _units // property. The secondary sort is on the _path property if the _units is // UNIT_COUNT, otherwise it is on the _amount property. function cmpAmount(a, b) { if (a._units != b._units) { return a._units - b._units; // use the enum order from nsIMemoryReporter } if (a._units == UNITS_COUNT) { if (a._path < b._path) return -1; if (a._path > b._path) return 1; return 0; } return b._amount - a._amount; }; /** * Generates the text for a single process. * * @param aProcess * The name of the process * @param aReporters * Table of reporters for this process, indexed by _path * @return The generated text */ function genProcessText(aProcess, aReporters) { /** * From a list of memory reporters, builds a tree that mirrors the tree * structure that will be shown as output. * * @return The built tree. The tree nodes have this structure: * interface Node { * _name: string; * _kind: number; * _amount: number; (non-negative or 'kUnknown') * _description: string; * _kids: [Node]; * _hasReporter: boolean; (only defined if 'true') * _hasProblem: boolean; (only defined if 'true') * } */ function buildTree() { const treeName = "explicit"; const omitThresholdPerc = 0.5; /* percent */ function findKid(aName, aKids) { for (var i = 0; i < aKids.length; i++) { if (aKids[i]._name === aName) { return aKids[i]; } } return undefined; } // We want to process all reporters that begin with 'treeName'. // First we build the tree but only filling in '_name', '_kind', '_kids' // and maybe '._hasReporter'. This is done top-down from the reporters. var t = { _name: "falseRoot", _kind: KIND_OTHER, _kids: [] }; for (var path in aReporters) { var r = aReporters[path]; if (r._path.slice(0, treeName.length) === treeName) { var names = r._path.split('/'); var u = t; for (var i = 0; i < names.length; i++) { var name = names[i]; var uMatch = findKid(name, u._kids); if (uMatch) { u = uMatch; } else { var v = { _name: name, _kind: KIND_OTHER, _kids: [] }; u._kids.push(v); u = v; } } u._kind = r._kind; u._hasReporter = true; } } // Using falseRoot makes the above code simpler. Now discard it, leaving // treeName at the root. t = t._kids[0]; // Next, fill in '_description' and '_amount', and maybe '_hasProblem' // for each node. This is done bottom-up because for most non-leaf nodes // '_amount' and '_description' are determined from the child nodes. function fillInTree(aT, aPrepath) { var path = aPrepath ? aPrepath + '/' + aT._name : aT._name; if (aT._kids.length === 0) { // Leaf node. Must have a reporter. aT._description = getDescription(aReporters, path); var amount = getBytes(aReporters, path); if (amount !== kUnknown) { aT._amount = amount; } else { aT._amount = 0; aT._hasProblem = true; } } else { // Non-leaf node. Get the size of the children. var childrenBytes = 0; for (var i = 0; i < aT._kids.length; i++) { // Allow for kUnknown, treat it like 0. var b = fillInTree(aT._kids[i], path); childrenBytes += (b === kUnknown ? 0 : b); } if (aT._hasReporter === true) { aT._description = getDescription(aReporters, path); var amount = getBytes(aReporters, path); if (amount !== kUnknown) { // Non-leaf node with its own reporter. Use the reporter and add // an "other" child node. aT._amount = amount; var other = { _name: "other", _kind: KIND_OTHER, _description: "All unclassified " + aT._name + " memory.", _amount: aT._amount - childrenBytes, _kids: [] }; aT._kids.push(other); } else { // Non-leaf node with a reporter that returns kUnknown. // Use the sum of the children and mark it as problematic. aT._amount = childrenBytes; aT._hasProblem = true; } } else { // Non-leaf node without its own reporter. Derive its size and // description entirely from its children. aT._amount = childrenBytes; aT._description = "The sum of all entries below '" + aT._name + "'."; } } return aT._amount; } fillInTree(t, ""); // Determine how many bytes are reported by heap reporters. Be careful // with non-leaf reporters; if we count a non-leaf reporter we don't want // to count any of its child reporters. var s = ""; function getKnownHeapUsedBytes(aT) { if (aT._kind === KIND_HEAP) { return aT._amount; } else { var n = 0; for (var i = 0; i < aT._kids.length; i++) { n += getKnownHeapUsedBytes(aT._kids[i]); } return n; } } // A special case: compute the derived "heap-unclassified" value. Don't // mark "heap-allocated" when we get its size because we want it to appear // in the "Other Measurements" list. var heapUsedBytes = getBytes(aReporters, "heap-allocated", true); var unknownHeapUsedBytes = 0; var hasProblem = true; if (heapUsedBytes !== kUnknown) { unknownHeapUsedBytes = heapUsedBytes - getKnownHeapUsedBytes(t); hasProblem = false; } var heapUnclassified = { _name: "heap-unclassified", _kind: KIND_HEAP, _description: "Memory not classified by a more specific reporter. This includes " + "waste due to internal fragmentation in the heap allocator (caused when " + "the allocator rounds up request sizes).", _amount: unknownHeapUsedBytes, _hasProblem: hasProblem, _kids: [] } t._kids.push(heapUnclassified); t._amount += unknownHeapUsedBytes; function shouldOmit(aBytes) { return !gVerbose && t._amount !== kUnknown && (100 * aBytes / t._amount) < omitThresholdPerc; } /** * Sort all kid nodes from largest to smallest and aggregate * insignificant nodes. * * @param aT * The tree */ function filterTree(aT) { aT._kids.sort(cmpAmount); for (var i = 0; i < aT._kids.length; i++) { if (shouldOmit(aT._kids[i]._amount)) { // This sub-tree is below the significance threshold // Remove it and all remaining (smaller) sub-trees, and // replace them with a single aggregate node. var i0 = i; var aggBytes = 0; for ( ; i < aT._kids.length; i++) { aggBytes += aT._kids[i]._amount; } aT._kids.splice(i0); var n = i - i0; var rSub = { _name: "(" + n + " omitted)", _kind: KIND_OTHER, _amount: aggBytes, _description: n + " sub-trees that were below the " + omitThresholdPerc + "% significance threshold. " + "Click 'More verbose' at the bottom of this page " + "to see them.", _kids: [] }; // Add the "omitted" sub-tree at the end and then resort, because the // sum of the omitted sub-trees may be larger than some of the // shown sub-trees. aT._kids[i0] = rSub; aT._kids.sort(cmpAmount); break; } filterTree(aT._kids[i]); } } filterTree(t); return t; } // Nb: the newlines give nice spacing if we cut+paste into a text buffer. var text = ""; text += "

" + aProcess + " Process

\n\n"; text += genTreeText(buildTree()); text += genOtherText(aReporters); text += "
"; return text; } /** * Returns the reporter's amount formatted as a human-readable string (with * units, if applicable). * * @param aReporter * The reporter whose usage we're formatting * @return The reporter's amount formatted as a human-readable string */ function formatReporterAmount(aReporter) { switch(aReporter._units) { case UNITS_BYTES: return formatBytes(aReporter._amount); case UNITS_COUNT: return formatInt(aReporter._amount); default: return "(???)" } } /** * Formats an int as a human-readable string. * * @param aN * The integer to format * @return A human-readable string representing the int */ function formatInt(aN) { var neg = false; if (aN < 0) { neg = true; aN = -aN; } var s = ""; while (true) { var k = aN % 1000; aN = Math.floor(aN / 1000); if (aN > 0) { if (k < 10) { s = ",00" + k + s; } else if (k < 100) { s = ",0" + k + s; } else { s = "," + k + s; } } else { s = k + s; break; } } return neg ? "-" + s : s; } /** * Converts a byte count to an appropriate string representation. * * @param aBytes * The byte count * @return The string representation */ function formatBytes(aBytes) { var unit = gVerbose ? "B" : "MB"; var s; if (gVerbose) { s = formatInt(aBytes) + " " + unit; } else { var mbytes = (aBytes / (1024 * 1024)).toFixed(2); var a = String(mbytes).split("."); s = formatInt(a[0]) + "." + a[1] + " " + unit; } return s; } /** * Right-justifies a string in a field of a given width, padding as necessary * * @param aS * The string * @param aN * The field width * @param aC * The char used to pad * @return The string representation */ function pad(aS, aN, aC) { var padding = ""; var n2 = aN - aS.length; for (var i = 0; i < n2; i++) { padding += aC; } return padding + aS; } /** * Gets the byte count for a particular memory reporter and sets its _done * property. * * @param aReporters * Table of reporters for this process, indexed by _path * @param aPath * The path of the memory reporter * @param aDoNotMark * If set, the _done property is not set. * @return The byte count */ function getBytes(aReporters, aPath, aDoNotMark) { var r = aReporters[aPath]; if (r) { var bytes = r._amount; if (!aDoNotMark) { r._done = true; } return bytes; } // Nb: this should never occur; all paths have been extracted from // the original list of reporters and so the lookup should succeed. Return // an obviously wrong number that will likely be noticed. return -2 * 1024 * 1024; } /** * Gets the description for a particular memory reporter. * * @param aReporters * Table of reporters for this process, indexed by _path * @param aPath * The path of the memory reporter * @return The description */ function getDescription(aReporters, aPath) { var r = aReporters[aPath]; return r ? r._description : "???"; } function genMrValueText(aValue) { return "" + aValue + ""; } function kindToString(aKind) { switch (aKind) { case KIND_NONHEAP: return "(Non-heap) "; case KIND_HEAP: return "(Heap) "; case KIND_OTHER: return ""; default: return "(???) "; } } function escapeQuotes(aStr) { return aStr.replace(/\&/g, '&').replace(/'/g, '''); } // For user-controlled strings. function escapeAll(aStr) { return aStr.replace(/\&/g, '&').replace(/'/g, '''). replace(/\/g, '>'). replace(/\"/g, '"'); } // Compartment reporter names are URLs and so can include forward slashes. But // forward slash is the memory reporter path separator. So the memory // reporters change them to backslashes. Undo that here. function flipBackslashes(aStr) { return aStr.replace(/\\/g, '/'); } // Truncate the URL in a compartment name if not in verbose mode. function truncateCompartmentName(aStr) { return (gVerbose) ? aStr : aStr.replace(/compartment\((.{40}).*\)/, 'compartment($1...)'); } function prepName(aStr) { return escapeAll(flipBackslashes(truncateCompartmentName(aStr))); } function prepDesc(aStr) { return escapeQuotes(flipBackslashes(aStr)); } function genMrNameText(aKind, aDesc, aName, aHasProblem) { const problemDesc = "Warning: this memory reporter was unable to compute a useful value. " + "The reported value is the sum of all entries below '" + aName + "', " + "which is probably less than the true value."; var text = "-- " + prepName(aName) + ""; text += aHasProblem ? " [*]\n" : "\n"; return text; } /** * Generates the text for the tree, including its heading. * * @param aT * The tree * @return The generated text */ function genTreeText(aT) { var treeBytes = aT._amount; var treeBytesLength = formatBytes(treeBytes).length; /** * Generates the text for a particular tree, without a heading. * * @param aT * The tree * @param aIndentGuide * Records what indentation is required for this tree. It has one * entry per level of indentation. For each entry, ._isLastKid * records whether the node in question is the last child, and * ._depth records how many chars of indentation are required. * @param aParentBytesLength * The length of the formatted byte count of the top node in the tree * @return The generated text */ function genTreeText2(aT, aIndentGuide, aParentBytesLength) { function repeatStr(aC, aN) { var s = ""; for (var i = 0; i < aN; i++) { s += aC; } return s; } // Generate the indent. There's a subset of the Unicode "light" // box-drawing chars that are widely implemented in terminals, and // this code sticks to that subset to maximize the chance that // cutting and pasting about:memory output to a terminal will work // correctly: const kHorizontal = "\u2500", kVertical = "\u2502", kUpAndRight = "\u2514", kVerticalAndRight = "\u251c"; var indent = ""; if (aIndentGuide.length > 0) { for (var i = 0; i < aIndentGuide.length - 1; i++) { indent += aIndentGuide[i]._isLastKid ? " " : kVertical; indent += repeatStr(" ", aIndentGuide[i]._depth - 1); } indent += aIndentGuide[i]._isLastKid ? kUpAndRight : kVerticalAndRight; indent += repeatStr(kHorizontal, aIndentGuide[i]._depth - 1); } // Indent more if this entry is narrower than its parent, and update // aIndentGuide accordingly. var tMemoryUsedStr = formatBytes(aT._amount); var tBytesLength = tMemoryUsedStr.length; var extraIndentLength = Math.max(aParentBytesLength - tBytesLength, 0); if (extraIndentLength > 0) { for (var i = 0; i < extraIndentLength; i++) { indent += kHorizontal; } aIndentGuide[aIndentGuide.length - 1]._depth += extraIndentLength; } indent += ""; // Generate the percentage. var perc = ""; if (aT._amount === treeBytes) { perc = "100.0"; } else { perc = (100 * aT._amount / treeBytes).toFixed(2); perc = pad(perc, 5, '0'); } perc = "(" + perc + "%) "; var text = indent + genMrValueText(tMemoryUsedStr) + " " + perc + genMrNameText(aT._kind, aT._description, aT._name, aT._hasProblem); for (var i = 0; i < aT._kids.length; i++) { // 3 is the standard depth, the callee adjusts it if necessary. aIndentGuide.push({ _isLastKid: (i === aT._kids.length - 1), _depth: 3 }); text += genTreeText2(aT._kids[i], aIndentGuide, tBytesLength); aIndentGuide.pop(); } return text; } var text = genTreeText2(aT, [], treeBytesLength); // Nb: the newlines give nice spacing if we cut+paste into a text buffer. const desc = "This tree covers explicit memory allocations by the application, " + "both at the operating system level (via calls to functions such as " + "VirtualAlloc, vm_allocate, and mmap), and at the heap allocation level " + "(via functions such as malloc, calloc, realloc, memalign, operator " + "new, and operator new[]). It excludes memory that is mapped implicitly " + "such as code and data segments, and thread stacks. It also excludes " + "heap memory that has been freed by the application but is still being " + "held onto by the heap allocator. It 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."; return "

Explicit Allocations

\n" + "
" + text + "
\n"; } /** * Generates the text for the "Other Measurements" section. * * @param aReporters * Table of reporters for this process, indexed by _path * @return The generated text */ function genOtherText(aReporters) { // Generate an array of Reporter-like elements, stripping out all the // reporters that have already been handled. Also find the width of the // widest element, so we can format things nicely. var maxAmountLength = 0; var rArray = []; for (var path in aReporters) { var r = aReporters[path]; if (!r._done) { var hasProblem = false; if (r._amount === kUnknown) { hasProblem = true; } var elem = { _path: r._path, _kind: r._kind, _units: r._units, _amount: hasProblem ? 0 : r._amount, _description: r._description, _hasProblem: hasProblem }; rArray.push(elem); var thisAmountLength = formatReporterAmount(elem).length; if (thisAmountLength > maxAmountLength) { maxAmountLength = thisAmountLength; } } } rArray.sort(cmpAmount); // Generate text for the not-yet-printed values. var text = ""; for (var i = 0; i < rArray.length; i++) { var elem = rArray[i]; text += genMrValueText( pad(formatReporterAmount(elem), maxAmountLength, ' ')) + " "; text += genMrNameText(elem._kind, elem._description, elem._path, elem._hasProblem); } // Nb: the newlines give nice spacing if we cut+paste into a text buffer. const desc = "This list contains other memory measurements that cross-cut " + "the requested memory measurements above." return "

Other Measurements

\n" + "
" + text + "
\n"; } function debug(x) { var content = $("content"); var div = document.createElement("div"); div.innerHTML = JSON.stringify(x); content.appendChild(div); }