/* -*- 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 ***** */ // This file is used for both about:memory and about:compartments. "use strict"; //--------------------------------------------------------------------------- // Code shared by about:memory and about:compartments //--------------------------------------------------------------------------- const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; const KIND_NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP; const KIND_HEAP = Ci.nsIMemoryReporter.KIND_HEAP; const KIND_OTHER = Ci.nsIMemoryReporter.KIND_OTHER; const UNITS_BYTES = Ci.nsIMemoryReporter.UNITS_BYTES; const UNITS_COUNT = Ci.nsIMemoryReporter.UNITS_COUNT; const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE; const UNITS_PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE; // Because about:memory and about:compartments are non-standard URLs, // location.search is undefined, so we have to use location.href here. // The toLowerCase() calls ensure that addresses like "ABOUT:MEMORY" work. let gVerbose; { let split = document.location.href.split('?'); document.title = split[0].toLowerCase(); gVerbose = split.length == 2 && split[1].toLowerCase() == 'verbose'; } let gChildMemoryListener = undefined; // This is a useful function and an efficient way to implement it. String.prototype.startsWith = function(s) { return this.lastIndexOf(s, 0) === 0; } //--------------------------------------------------------------------------- // Forward slashes in URLs in paths are represented with backslashes to avoid // being mistaken for path separators. Paths/names where this hasn't been // undone are prefixed with "unsafe"; the rest are prefixed with "safe". function flipBackslashes(aUnsafeStr) { return aUnsafeStr.replace(/\\/g, '/'); } const gAssertionFailureMsgPrefix = "aboutMemory.js assertion failed: "; function assert(aCond, aMsg) { if (!aCond) { reportAssertionFailure(aMsg) throw(gAssertionFailureMsgPrefix + aMsg); } } function reportAssertionFailure(aMsg) { let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); if (debug.isDebugBuild) { debug.assertion(aMsg, "false", "aboutMemory.js", 0); } } function debug(x) { appendElementWithText(document.body, "div", "debug", JSON.stringify(x)); } //--------------------------------------------------------------------------- function addChildObserversAndUpdate(aUpdateFn) { let os = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); os.notifyObservers(null, "child-memory-reporter-request", null); gChildMemoryListener = aUpdateFn; os.addObserver(gChildMemoryListener, "child-memory-reporter-update", false); gChildMemoryListener(); } function onLoad() { if (document.title === "about:memory") { onLoadAboutMemory(); } else if (document.title === "about:compartments") { onLoadAboutCompartments(); } else { assert(false, "Unknown location: " + document.title); } } 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 // onLoadAbout{Memory,Compartments} might not fire. if (gChildMemoryListener) { let os = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); os.removeObserver(gChildMemoryListener, "child-memory-reporter-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 minimizeMemoryUsage3x(fAfter) { let i = 0; function runSoon(f) { let tm = Cc["@mozilla.org/thread-manager;1"] .getService(Ci.nsIThreadManager); tm.mainThread.dispatch({ run: f }, Ci.nsIThread.DISPATCH_NORMAL); } function sendHeapMinNotificationsInner() { let os = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); os.notifyObservers(null, "memory-pressure", "heap-minimize"); if (++i < 3) runSoon(sendHeapMinNotificationsInner); else runSoon(fAfter); } sendHeapMinNotificationsInner(); } //--------------------------------------------------------------------------- /** * Iterates over each reporter and multi-reporter. * * @param aMgr * The memory reporter manager. * @param aIgnoreSingle * Function that indicates if we should skip a single reporter, based * on its path. * @param aIgnoreMulti * Function that indicates if we should skip a multi-reporter, based on * its name. * @param aHandleReport * The function that's called for each report. */ function processMemoryReporters(aMgr, aIgnoreSingle, aIgnoreMulti, aHandleReport) { // Process each memory reporter with aHandleReport. // // - 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. // // - After this point we never use the original memory report again. function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount, aDescription) { checkReport(aUnsafePath, aKind, aUnits, aAmount, aDescription); aHandleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount, aDescription); } function handleException(aReporterStr, aUnsafePathOrName, aE) { // There are two exception cases that must be distinguished here. // // - We want to halt proceedings on exceptions thrown within this file // (i.e. assertion failures in handleReport); such exceptions contain // gAssertionFailureMsgPrefix in their string representation. // // - We want to continue on when faced with exceptions thrown outside this // file (i.e. in collectReports). let str = aE.toString(); if (str.search(gAssertionFailureMsgPrefix) >= 0) { throw(aE); } else { debug("Bad memory " + aReporterStr + " '" + aUnsafePathOrName + "': " + aE); } } let e = aMgr.enumerateReporters(); while (e.hasMoreElements()) { let rOrig = e.getNext().QueryInterface(Ci.nsIMemoryReporter); let unsafePath = rOrig.path; try { if (!aIgnoreSingle(unsafePath)) { handleReport(rOrig.process, unsafePath, rOrig.kind, rOrig.units, rOrig.amount, rOrig.description); } } catch (e) { handleException("reporter", unsafePath, e); } } let e = aMgr.enumerateMultiReporters(); while (e.hasMoreElements()) { let mrOrig = e.getNext().QueryInterface(Ci.nsIMemoryMultiReporter); let name = mrOrig.name; try { if (!aIgnoreMulti(name)) { mrOrig.collectReports(handleReport, null); } } catch (e) { handleException("multi-reporter", name, e); } } } // This regexp matches sentences and sentence fragments, i.e. strings that // start with a capital letter and ends with a '.'. (The final sentence may be // in parentheses, so a ')' might appear after the '.'.) const gSentenceRegExp = /^[A-Z].*\.\)?$/; function checkReport(aUnsafePath, aKind, aUnits, aAmount, aDescription) { if (aUnsafePath.startsWith("explicit/")) { assert(aKind === KIND_HEAP || aKind === KIND_NONHEAP, "bad explicit kind"); assert(aUnits === UNITS_BYTES, "bad explicit units"); assert(aDescription.match(gSentenceRegExp), "non-sentence explicit description"); } else if (aUnsafePath.startsWith("smaps/")) { assert(aKind === KIND_NONHEAP, "bad smaps kind"); assert(aUnits === UNITS_BYTES, "bad smaps units"); assert(aDescription !== "", "empty smaps description"); } else if (aUnsafePath.startsWith("compartments/")) { assert(aKind === KIND_OTHER, "bad compartments kind"); assert(aUnits === UNITS_COUNT, "bad compartments units"); assert(aAmount === 1, "bad amount"); assert(aDescription === "", "bad description"); } else { assert(aUnsafePath.indexOf("/") === -1, "'other' path contains '/'"); assert(aKind === KIND_OTHER, "bad other kind: " + aUnsafePath); assert(aDescription.match(gSentenceRegExp), "non-sentence other description"); } } //--------------------------------------------------------------------------- function clearBody() { let oldBody = document.body; let body = oldBody.cloneNode(false); oldBody.parentNode.replaceChild(body, oldBody); body.classList.add(gVerbose ? 'verbose' : 'non-verbose'); return body; } function appendTextNode(aP, aText) { let e = document.createTextNode(aText); aP.appendChild(e); return e; } function appendElement(aP, aTagName, aClassName) { let e = document.createElement(aTagName); if (aClassName) { e.className = aClassName; } aP.appendChild(e); return e; } function appendElementWithText(aP, aTagName, aClassName, aText) { let e = appendElement(aP, aTagName, aClassName); appendTextNode(e, aText); return e; } //--------------------------------------------------------------------------- // Code specific to about:memory //--------------------------------------------------------------------------- const kUnknown = -1; // used for an unknown _amount const kTreeDescriptions = { 'explicit' : "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[]).\ \n\n\ 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. \ \n\n\ 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.", 'resident': "This tree shows how much space in physical memory each of the process's \ mappings is currently using (the mapping's 'resident set size', or 'RSS'). \ This is a good measure of the 'cost' of the mapping, although it does not \ take into account the fact that shared libraries may be mapped by multiple \ processes but appear only once in physical memory. \ \n\n\ Note that the 'resident' value here might not equal the value for 'resident' \ under 'Other Measurements' because the two measurements are not taken at \ exactly the same time.", 'pss': "This tree shows how much space in physical memory can be 'blamed' on this \ process. For each mapping, its 'proportional set size' (PSS) is the \ mapping's resident size divided by the number of processes which use the \ mapping. So if a mapping is private to this process, its PSS should equal \ its RSS. But if a mapping is shared between three processes, its PSS in each \ of the processes would be 1/3 its RSS.", 'vsize': "This tree shows how much virtual addres space each of the process's mappings \ takes up (the mapping's 'vsize'). A mapping may have a large vsize but use \ only a small amount of physical memory; the resident set size of a mapping is \ a better measure of the mapping's 'cost'. \ \n\n\ Note that the 'vsize' value here might not equal the value for 'vsize' under \ 'Other Measurements' because the two measurements are not taken at exactly \ the same time.", 'swap': "This tree shows how much space in the swap file each of the process's \ mappings is currently using. Mappings which are not in the swap file (i.e., \ nodes which would have a value of 0 in this tree) are omitted." }; const kTreeNames = { 'explicit': 'Explicit Allocations', 'resident': 'Resident Set Size (RSS) Breakdown', 'pss': 'Proportional Set Size (PSS) Breakdown', 'vsize': 'Virtual Size Breakdown', 'swap': 'Swap Usage Breakdown', 'other': 'Other Measurements' }; const kMapTreePaths = ['smaps/resident', 'smaps/pss', 'smaps/vsize', 'smaps/swap']; //--------------------------------------------------------------------------- function onLoadAboutMemory() { addChildObserversAndUpdate(updateAboutMemory); } function doGlobalGC() { Cu.forceGC(); let os = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); os.notifyObservers(null, "child-gc-request", null); updateAboutMemory(); } function doCC() { window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .cycleCollect(); let os = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); os.notifyObservers(null, "child-cc-request", null); updateAboutMemory(); } //--------------------------------------------------------------------------- /** * Top-level function that does the work of generating the page. */ function updateAboutMemory() { // First, clear the page contents. Necessary because updateAboutMemory() // might be called more than once due to the "child-memory-reporter-update" // observer. let body = clearBody(); let mgr = Cc["@mozilla.org/memory-reporter-manager;1"]. getService(Ci.nsIMemoryReporterManager); // Generate output for one process at a time. Always start with the // Main process. let reportsByProcess = getReportsByProcess(mgr); let hasMozMallocUsableSize = mgr.hasMozMallocUsableSize; appendProcessReportsElements(body, "Main", reportsByProcess["Main"], hasMozMallocUsableSize); for (let process in reportsByProcess) { if (process !== "Main") { appendProcessReportsElements(body, process, reportsByProcess[process], hasMozMallocUsableSize); } } appendElement(body, "hr"); // Memory-related actions. const UpDesc = "Re-measure."; const GCDesc = "Do a global garbage collection."; const CCDesc = "Do a cycle collection."; 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."; function appendButton(aTitle, aOnClick, aText, aId) { let b = appendElementWithText(body, "button", "", aText); b.title = aTitle; b.onclick = aOnClick if (aId) { b.id = aId; } } // The "Update" button has an id so it can be clicked in a test. appendButton(UpDesc, updateAboutMemory, "Update", "updateButton"); appendButton(GCDesc, doGlobalGC, "GC"); appendButton(CCDesc, doCC, "CC"); appendButton(MPDesc, function() { minimizeMemoryUsage3x(updateAboutMemory); }, "Minimize memory usage"); let div1 = appendElement(body, "div"); if (gVerbose) { let a = appendElementWithText(div1, "a", "option", "Less verbose"); a.href = "about:memory"; } else { let a = appendElementWithText(div1, "a", "option", "More verbose"); a.href = "about:memory?verbose"; } let div2 = appendElement(body, "div"); let a = appendElementWithText(div2, "a", "option", "Troubleshooting information"); a.href = "about:support"; let legendText1 = "Click on a non-leaf node in a tree to expand ('++') " + "or collapse ('--') its children."; let legendText2 = "Hover the pointer over the name of a memory report " + "to see a description of what it measures."; appendElementWithText(body, "div", "legend", legendText1); appendElementWithText(body, "div", "legend", legendText2); } //--------------------------------------------------------------------------- function Report(aUnsafePath, aKind, aUnits, aAmount, aDescription) { this._unsafePath = aUnsafePath; this._kind = aKind; this._units = aUnits; this._amount = aAmount; this._description = aDescription; // this._nMerged is only defined if > 1 // this._done is defined and set to true when the Report's amount is read } Report.prototype = { // Sum the values (accounting for possible kUnknown amounts), and mark |this| // as a dup. We mark dups because it's useful to know when a report is // duplicated; it might be worth investigating and splitting up to have // non-duplicated names. merge: function(r) { if (this._amount !== kUnknown && r._amount !== kUnknown) { this._amount += r._amount; } else if (this._amount === kUnknown && r._amount !== kUnknown) { this._amount = r._amount; } this._nMerged = this._nMerged ? this._nMerged + 1 : 2; }, treeNameMatches: function(aTreeName) { // Nb: the '/' must be present, because we have a KIND_OTHER reporter // called "explicit" which is not part of the "explicit" tree. return this._unsafePath.startsWith(aTreeName) && this._unsafePath.charAt(aTreeName.length) === '/'; } }; function getReportsByProcess(aMgr) { // Ignore the "smaps" multi-reporter in non-verbose mode, and the // "compartments" multi-reporter all the time. (Note that reports from these // multi-reporters can reach here as single reports if they were in the child // process.) function ignoreSingle(aPath) { return (aPath.startsWith("smaps/") && !gVerbose) || (aPath.startsWith("compartments/")) } function ignoreMulti(aName) { return ((aName === "smaps" && !gVerbose) || (aName === "compartments")); } let reportsByProcess = {}; function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount, aDescription) { let process = aProcess === "" ? "Main" : aProcess; let r = new Report(aUnsafePath, aKind, aUnits, aAmount, aDescription); if (!reportsByProcess[process]) { reportsByProcess[process] = {}; } let reports = reportsByProcess[process]; let rOld = reports[r._unsafePath]; if (rOld) { // Already an entry; must be a duplicated report. This can happen // legitimately. Merge them. rOld.merge(r); } else { reports[r._unsafePath] = r; } } processMemoryReporters(aMgr, ignoreSingle, ignoreMulti, handleReport); return reportsByProcess; } //--------------------------------------------------------------------------- // There are two kinds of TreeNode. // - Leaf TreeNodes correspond to Reports and have more properties. // - Non-leaf TreeNodes are just scaffolding nodes for the tree; their values // are derived from their children. function TreeNode(aUnsafeName) { // Nb: _units is not needed, it's always UNITS_BYTES. this._unsafeName = aUnsafeName; this._kids = []; // Leaf TreeNodes have these properties added immediately after construction: // - _amount (which is never |kUnknown|) // - _description // - _kind // - _nMerged (only defined if > 1) // - _isUnknown (only defined if true) // // Non-leaf TreeNodes have these properties added later: // - _amount (which is never |kUnknown|) // - _description // - _hideKids (only defined if true) } TreeNode.prototype = { findKid: function(aUnsafeName) { for (let i = 0; i < this._kids.length; i++) { if (this._kids[i]._unsafeName === aUnsafeName) { return this._kids[i]; } } return undefined; }, toString: function() { return formatBytes(this._amount); } }; TreeNode.compare = function(a, b) { return b._amount - a._amount; }; /** * From a table of Reports, builds a tree that mirrors the tree structure that * will be shown as output. * * @param aReports * The table of Reports, indexed by _unsafePath. * @param aTreeName * The name of the tree being built. * @return The built tree. */ function buildTree(aReports, aTreeName) { // We want to process all reports that begin with |aTreeName|. First we // build the tree but only fill the properties that we can with a top-down // traversal. // There should always be at least one matching Report object when // |aTreeName| is "explicit". But there may be zero for "smaps" trees; if // that happens, bail. let foundReport = false; for (let unsafePath in aReports) { if (aReports[unsafePath].treeNameMatches(aTreeName)) { foundReport = true; break; } } if (!foundReport) { assert(aTreeName !== 'explicit', "aTreeName !== 'explicit'"); return null; } let t = new TreeNode("falseRoot"); for (let unsafePath in aReports) { // Add any missing nodes in the tree implied by the unsafePath. let r = aReports[unsafePath]; if (r.treeNameMatches(aTreeName)) { let unsafeNames = r._unsafePath.split('/'); let u = t; for (let i = 0; i < unsafeNames.length; i++) { let unsafeName = unsafeNames[i]; let uMatch = u.findKid(unsafeName); if (uMatch) { u = uMatch; } else { let v = new TreeNode(unsafeName); u._kids.push(v); u = v; } } // Fill in extra details in the leaf node from the Report object. if (r._amount !== kUnknown) { u._amount = r._amount; } else { u._amount = 0; u._isUnknown = true; } u._description = r._description; u._kind = r._kind; if (r._nMerged) { u._nMerged = r._nMerged; } r._done = true; } } // Using falseRoot makes the above code simpler. Now discard it, leaving // aTreeName at the root. t = t._kids[0]; // Next, fill in the remaining properties bottom-up. // Note that this function never returns kUnknown. function fillInNonLeafNodes(aT) { if (aT._kids.length === 0) { // Leaf node. Has already been filled in. assert(aT._kind !== undefined, "aT._kind is undefined for leaf node"); } else { // Non-leaf node. Derive its _amount and _description entirely // from its children. assert(aT._kind === undefined, "aT._kind is defined for non-leaf node"); let childrenBytes = 0; for (let i = 0; i < aT._kids.length; i++) { childrenBytes += fillInNonLeafNodes(aT._kids[i]); } aT._amount = childrenBytes; aT._description = "The sum of all entries below '" + flipBackslashes(aT._unsafeName) + "'."; } assert(aT._amount !== kUnknown, "aT._amount !== kUnknown"); return aT._amount; } fillInNonLeafNodes(t); // Reduce the depth of the tree by the number of occurrences of '/' in // aTreeName. (Thus the tree named 'foo/bar/baz' will be rooted at 'baz'.) let slashCount = 0; for (let i = 0; i < aTreeName.length; i++) { if (aTreeName[i] == '/') { assert(t._kids.length == 1, "Not expecting multiple kids here."); t = t._kids[0]; } } // Set the (unsafe) description on the root node. t._description = kTreeDescriptions[t._unsafeName]; return t; } /** * Ignore all the memory reports that belong to a "smaps" tree; this involves * explicitly marking them as done. * * @param aReports * The table of Reports, indexed by _unsafePath. */ function ignoreSmapsTrees(aReports) { for (let unsafePath in aReports) { let r = aReports[unsafePath]; if (r.treeNameMatches("smaps")) { r._done = true; } } } /** * Do some work which only makes sense for the 'explicit' tree. * * @param aT * The tree. * @param aReports * Table of Reports for this process, indexed by _unsafePath. * @return A boolean indicating if "heap-allocated" is known for the process. */ function fixUpExplicitTree(aT, aReports) { // Determine how many bytes are in heap reports. function getKnownHeapUsedBytes(aT) { let n = 0; if (aT._kids.length === 0) { // Leaf node. assert(aT._kind !== undefined, "aT._kind is undefined for leaf node"); n = aT._kind === KIND_HEAP ? aT._amount : 0; } else { for (let 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. let heapAllocatedReport = aReports["heap-allocated"]; assert(heapAllocatedReport, "no 'heap-allocated' report"); let heapAllocatedBytes = heapAllocatedReport._amount; let heapUnclassifiedT = new TreeNode("heap-unclassified"); let hasKnownHeapAllocated = heapAllocatedBytes !== kUnknown; if (hasKnownHeapAllocated) { heapUnclassifiedT._amount = heapAllocatedBytes - getKnownHeapUsedBytes(aT); } else { heapUnclassifiedT._amount = 0; heapUnclassifiedT._isUnknown = true; } // This kindToString() ensures the "(Heap)" prefix is set without having to // set the _kind property, which would mean that there is a corresponding // Report object for this TreeNode object (which isn't true) heapUnclassifiedT._description = kindToString(KIND_HEAP) + "Memory not classified by a more specific reporter. This includes " + "slop bytes due to internal fragmentation in the heap allocator " + "(caused when the allocator rounds up request sizes)."; aT._kids.push(heapUnclassifiedT); aT._amount += heapUnclassifiedT._amount; return hasKnownHeapAllocated; } /** * Sort all kid nodes from largest to smallest, and insert aggregate nodes * where appropriate. * * @param aTotalBytes * The size of the tree's root node. * @param aT * The tree. */ function sortTreeAndInsertAggregateNodes(aTotalBytes, aT) { const kSignificanceThresholdPerc = 1; function isInsignificant(aT) { return !gVerbose && aTotalBytes !== kUnknown && (100 * aT._amount / aTotalBytes) < kSignificanceThresholdPerc; } if (aT._kids.length === 0) { return; } aT._kids.sort(TreeNode.compare); // If the first child is insignificant, they all are, and there's no point // creating an aggregate node that lacks siblings. Just set the parent's // _hideKids property and process all children. if (isInsignificant(aT._kids[0])) { aT._hideKids = true; for (let i = 0; i < aT._kids.length; i++) { sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]); } return; } // Look at all children except the last one. let i; for (i = 0; i < aT._kids.length - 1; i++) { if (isInsignificant(aT._kids[i])) { // This child is below the significance threshold. If there are other // (smaller) children remaining, move them under an aggregate node. let i0 = i; let nAgg = aT._kids.length - i0; // Create an aggregate node. let aggT = new TreeNode("(" + nAgg + " tiny)"); let aggBytes = 0; for ( ; i < aT._kids.length; i++) { aggBytes += aT._kids[i]._amount; aggT._kids.push(aT._kids[i]); } aggT._hideKids = true; aggT._amount = aggBytes; aggT._description = nAgg + " sub-trees that are below the " + kSignificanceThresholdPerc + "% significance threshold."; aT._kids.splice(i0, nAgg, aggT); aT._kids.sort(TreeNode.compare); // Process the moved children. for (i = 0; i < aggT._kids.length; i++) { sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]); } return; } sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]); } // The first n-1 children were significant. Don't consider if the last child // is significant; there's no point creating an aggregate node that only has // one child. Just process it. sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]); } // Global variable indicating if we've seen any invalid values for this // process; it holds the unsafePaths of any such reports. It is reset for // each new process. let gUnsafePathsWithInvalidValuesForThisProcess = []; function appendWarningElements(aP, aHasKnownHeapAllocated, aHasMozMallocUsableSize) { if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) { appendElementWithText(aP, "p", "", "WARNING: the 'heap-allocated' memory reporter and the " + "moz_malloc_usable_size() function do not work for this platform " + "and/or configuration. This means that 'heap-unclassified' is zero " + "and the 'explicit' tree shows much less memory than it should."); appendTextNode(aP, "\n\n"); } else if (!aHasKnownHeapAllocated) { appendElementWithText(aP, "p", "", "WARNING: the 'heap-allocated' memory reporter does not work for this " + "platform and/or configuration. This means that 'heap-unclassified' " + "is zero and the 'explicit' tree shows less memory than it should."); appendTextNode(aP, "\n\n"); } else if (!aHasMozMallocUsableSize) { appendElementWithText(aP, "p", "", "WARNING: the moz_malloc_usable_size() function does not work for " + "this platform and/or configuration. This means that much of the " + "heap-allocated memory is not measured by individual memory reporters " + "and so will fall under 'heap-unclassified'."); appendTextNode(aP, "\n\n"); } if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) { let div = appendElement(aP, "div"); appendElementWithText(div, "p", "", "WARNING: the following values are negative or unreasonably large."); appendTextNode(div, "\n"); let ul = appendElement(div, "ul"); for (let i = 0; i < gUnsafePathsWithInvalidValuesForThisProcess.length; i++) { appendTextNode(ul, " "); appendElementWithText(ul, "li", "", flipBackslashes(gUnsafePathsWithInvalidValuesForThisProcess[i])); appendTextNode(ul, "\n"); } appendElementWithText(div, "p", "", "This indicates a defect in one or more memory reporters. The " + "invalid values are highlighted."); appendTextNode(div, "\n\n"); gUnsafePathsWithInvalidValuesForThisProcess = []; // reset for the next process } } /** * Appends the elements for a single process's Reports. * * @param aP * The parent DOM node. * @param aProcess * The name of the process. * @param aReports * Table of Reports for this process, indexed by _unsafePath. * @param aHasMozMallocUsableSize * Boolean indicating if moz_malloc_usable_size works. * @return The generated text. */ function appendProcessReportsElements(aP, aProcess, aReports, aHasMozMallocUsableSize) { appendElementWithText(aP, "h1", "", aProcess + " Process"); appendTextNode(aP, "\n\n"); // gives nice spacing when we cut and paste // We'll fill this in later. let warningsDiv = appendElement(aP, "div", "accuracyWarning"); let explicitTree = buildTree(aReports, 'explicit'); let hasKnownHeapAllocated = fixUpExplicitTree(explicitTree, aReports); sortTreeAndInsertAggregateNodes(explicitTree._amount, explicitTree); appendTreeElements(aP, explicitTree, aProcess); // We only show these breakdown trees in verbose mode. if (gVerbose) { kMapTreePaths.forEach(function(t) { let tree = buildTree(aReports, t); // |tree| will be null if we don't have any reports for the given // unsafePath. if (tree) { sortTreeAndInsertAggregateNodes(tree._amount, tree); tree._hideKids = true; // smaps trees are always initially collapsed appendTreeElements(aP, tree, aProcess); } }); } else { // Although we skip the "smaps" multi-reporter in getReportsByProcess(), // we might get some smaps reports from a child process, and they must be // explicitly ignored. ignoreSmapsTrees(aReports); } // We have to call appendOtherElements after we process all the trees, // because it looks at all the reports which aren't part of a tree. appendOtherElements(aP, aReports); // Add any warnings about inaccuracies due to platform limitations. // These must be computed after generating all the text. The newlines give // nice spacing if we cut+paste into a text buffer. appendWarningElements(warningsDiv, hasKnownHeapAllocated, aHasMozMallocUsableSize); } /** * Determines if a number has a negative sign when converted to a string. * Works even for -0. * * @param aN * The number. * @return A boolean. */ function hasNegativeSign(aN) { if (aN === 0) { // this succeeds for 0 and -0 return 1 / aN === -Infinity; // this succeeds for -0 } return aN < 0; } /** * Formats an int as a human-readable string. * * @param aN * The integer to format. * @param aExtra * An extra string to tack onto the end. * @return A human-readable string representing the int. * * Note: building an array of chars and converting that to a string with * Array.join at the end is more memory efficient than using string * concatenation. See bug 722972 for details. */ function formatInt(aN, aExtra) { let neg = false; if (hasNegativeSign(aN)) { neg = true; aN = -aN; } let s = []; while (true) { let k = aN % 1000; aN = Math.floor(aN / 1000); if (aN > 0) { if (k < 10) { s.unshift(",00", k); } else if (k < 100) { s.unshift(",0", k); } else { s.unshift(",", k); } } else { s.unshift(k); break; } } if (neg) { s.unshift("-"); } if (aExtra) { s.push(aExtra); } return s.join(""); } /** * Converts a byte count to an appropriate string representation. * * @param aBytes * The byte count. * @return The string representation. */ function formatBytes(aBytes) { let unit = gVerbose ? " B" : " MB"; let s; if (gVerbose) { s = formatInt(aBytes, unit); } else { let mbytes = (aBytes / (1024 * 1024)).toFixed(2); let a = String(mbytes).split("."); // If the argument to formatInt() is -0, it will print the negative sign. s = formatInt(Number(a[0])) + "." + a[1] + unit; } return s; } /** * Converts a percentage to an appropriate string representation. * * @param aPerc100x * The percentage, multiplied by 100 (see nsIMemoryReporter). * @return The string representation */ function formatPercentage(aPerc100x) { return (aPerc100x / 100).toFixed(2) + "%"; } /** * Right-justifies a string in a field of a given width, padding as necessary. * * @param aS * The string. * @param aN * The field width. * @param aC * The char used to pad. * @return The string representation. */ function pad(aS, aN, aC) { let padding = ""; let n2 = aN - aS.length; for (let i = 0; i < n2; i++) { padding += aC; } return padding + aS; } // There's a subset of the Unicode "light" box-drawing chars that 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", kDoubleHorizontalSep = " \u2500\u2500 "; function appendMrValueSpan(aP, aValue, aIsInvalid) { appendElementWithText(aP, "span", "mrValue" + (aIsInvalid ? " invalid" : ""), aValue); } function kindToString(aKind) { switch (aKind) { case KIND_NONHEAP: return "(Non-heap) "; case KIND_HEAP: return "(Heap) "; case KIND_OTHER: case undefined: return ""; default: assert(false, "bad kind in kindToString"); } } // Possible states for kids. const kNoKids = 0; const kHideKids = 1; const kShowKids = 2; function appendMrNameSpan(aP, aKind, aKidsState, aDescription, aUnsafeName, aIsUnknown, aIsInvalid, aNMerged) { let text = ""; if (aKidsState === kNoKids) { appendElementWithText(aP, "span", "mrSep", kDoubleHorizontalSep); } else if (aKidsState === kHideKids) { appendElementWithText(aP, "span", "mrSep", " ++ "); appendElementWithText(aP, "span", "mrSep hidden", " -- "); } else if (aKidsState === kShowKids) { appendElementWithText(aP, "span", "mrSep hidden", " ++ "); appendElementWithText(aP, "span", "mrSep", " -- "); } else { assert(false, "bad aKidsState"); } let nameSpan = appendElementWithText(aP, "span", "mrName", flipBackslashes(aUnsafeName)); nameSpan.title = kindToString(aKind) + aDescription; if (aIsUnknown) { let noteSpan = appendElementWithText(aP, "span", "mrNote", " [*]"); noteSpan.title = "Warning: this memory reporter was unable to compute a useful value. "; } if (aIsInvalid) { let noteSpan = appendElementWithText(aP, "span", "mrNote", " [?!]"); noteSpan.title = "Warning: this value is invalid and indicates a bug in one or more " + "memory reporters. "; } if (aNMerged) { let noteSpan = appendElementWithText(aP, "span", "mrNote", " [" + aNMerged + "]"); noteSpan.title = "This value is the sum of " + aNMerged + " memory reporters that all have the same path."; } } // This is used to record the (safe) IDs of which sub-trees have been toggled, // so the collapsed/expanded state can be replicated when the page is // regenerated. It can end up holding IDs of nodes that no longer exist, e.g. // for compartments that have been closed. This doesn't seem like a big deal, // because the number is limited by the number of entries the user has changed // from their original state. let gTogglesBySafeTreeId = {}; function assertClassListContains(e, className) { assert(e, "undefined " + className); assert(e.classList.contains(className), "classname isn't " + className); } function toggle(aEvent) { // This relies on each line being a span that contains at least five spans: // mrValue, mrPerc, mrSep ('++'), mrSep ('--'), mrName, and then zero or more // mrNotes. All whitespace must be within one of these spans for this // function to find the right nodes. And the span containing the children of // this line must immediately follow. Assertions check this. // |aEvent.target| will be one of the five spans. Get the outer span. let outerSpan = aEvent.target.parentNode; assertClassListContains(outerSpan, "hasKids"); // Toggle visibility of the '++' and '--' separators. let plusSpan = outerSpan.childNodes[2]; let minusSpan = outerSpan.childNodes[3]; assertClassListContains(plusSpan, "mrSep"); assertClassListContains(minusSpan, "mrSep"); plusSpan .classList.toggle("hidden"); minusSpan.classList.toggle("hidden"); // Toggle visibility of the span containing this node's children. let subTreeSpan = outerSpan.nextSibling; assertClassListContains(subTreeSpan, "kids"); subTreeSpan.classList.toggle("hidden"); // Record/unrecord that this sub-tree was toggled. let safeTreeId = outerSpan.id; if (gTogglesBySafeTreeId[safeTreeId]) { delete gTogglesBySafeTreeId[safeTreeId]; } else { gTogglesBySafeTreeId[safeTreeId] = true; } } function expandPathToThisElement(aElement) { if (aElement.classList.contains("kids")) { // Unhide the kids. aElement.classList.remove("hidden"); expandPathToThisElement(aElement.previousSibling); // hasKids } else if (aElement.classList.contains("hasKids")) { // Unhide the '--' separator and hide the '++' separator. let plusSpan = aElement.childNodes[2]; let minusSpan = aElement.childNodes[3]; assertClassListContains(plusSpan, "mrSep"); assertClassListContains(minusSpan, "mrSep"); plusSpan.classList.add("hidden"); minusSpan.classList.remove("hidden"); expandPathToThisElement(aElement.parentNode); // kids or pre.entries } else { assertClassListContains(aElement, "entries"); } } /** * Appends the elements for the tree, including its heading. * * @param aPOuter * The parent DOM node. * @param aT * The tree. * @param aProcess * The process the tree corresponds to. * @return The generated text. */ function appendTreeElements(aPOuter, aT, aProcess) { let treeBytes = aT._amount; let rootStringLength = aT.toString().length; let isExplicitTree = aT._unsafeName == 'explicit'; /** * Appends the elements for a particular tree, without a heading. * * @param aP * The parent DOM node. * @param aUnsafePrePath * The partial unsafePath leading up to this node. * @param aT * The tree. * @param aBaseIndentText * The base text of the indent, which may be augmented within the * functton. * @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 aParentStringLength * The length of the formatted byte count of the top node in the tree. * @return The generated text. */ function appendTreeElements2(aP, aUnsafePrePath, aT, aIndentGuide, aBaseIndentText, aParentStringLength) { function repeatStr(aA, aC, aN) { for (let i = 0; i < aN; i++) { aA.push(aC); } } let unsafePath = aUnsafePrePath + aT._unsafeName; // Indent more if this entry is narrower than its parent, and update // aIndentGuide accordingly. let tString = aT.toString(); let extraIndentArray = []; let extraIndentLength = Math.max(aParentStringLength - tString.length, 0); if (extraIndentLength > 0) { repeatStr(extraIndentArray, kHorizontal, extraIndentLength); aIndentGuide[aIndentGuide.length - 1]._depth += extraIndentLength; } let indentText = aBaseIndentText + extraIndentArray.join(""); appendElementWithText(aP, "span", "treeLine", indentText); // Generate the percentage; detect and record invalid values at the same // time. let percText = ""; let tIsInvalid = false; if (aT._amount === treeBytes) { percText = "100.0"; } else { if (!(0 <= aT._amount && aT._amount <= treeBytes)) { tIsInvalid = true; gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath); reportAssertionFailure("Invalid value for " + flipBackslashes(unsafePath)); } percText = (100 * aT._amount / treeBytes).toFixed(2); percText = pad(percText, 5, '0'); } percText = " (" + percText + "%)"; // For non-leaf nodes, the entire sub-tree is put within a span so it can // be collapsed if the node is clicked on. let d; let hasKids = aT._kids.length > 0; let kidsState; let showSubtrees; if (hasKids) { // Determine if we should show the sub-tree below this entry; this // involves reinstating any previous toggling of the sub-tree. let safeTreeId = flipBackslashes(aProcess + ":" + unsafePath); showSubtrees = !aT._hideKids; if (gTogglesBySafeTreeId[safeTreeId]) { showSubtrees = !showSubtrees; } d = appendElement(aP, "span", "hasKids"); d.id = safeTreeId; d.onclick = toggle; kidsState = showSubtrees ? kShowKids : kHideKids; } else { assert(!aT._hideKids, "leaf node with _hideKids set") kidsState = kNoKids; d = aP; } appendMrValueSpan(d, tString, tIsInvalid); appendElementWithText(d, "span", "mrPerc", percText); // We don't want to show '(nonheap)' on a tree like 'smaps/vsize', since // the whole tree is non-heap. let kind = isExplicitTree ? aT._kind : undefined; appendMrNameSpan(d, kind, kidsState, aT._description, aT._unsafeName, aT._isUnknown, tIsInvalid, aT._nMerged); appendTextNode(d, "\n"); // In non-verbose mode, invalid nodes can be hidden in collapsed sub-trees. // But it's good to always see them, so force this. if (!gVerbose && tIsInvalid) { expandPathToThisElement(d); } if (hasKids) { // The 'kids' class is just used for sanity checking in toggle(). d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden"); for (let 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 }); // Generate the base indent. let baseIndentArray = []; if (aIndentGuide.length > 0) { let j; for (j = 0; j < aIndentGuide.length - 1; j++) { baseIndentArray.push(aIndentGuide[j]._isLastKid ? " " : kVertical); repeatStr(baseIndentArray, " ", aIndentGuide[j]._depth - 1); } baseIndentArray.push(aIndentGuide[j]._isLastKid ? kUpAndRight : kVerticalAndRight); repeatStr(baseIndentArray, kHorizontal, aIndentGuide[j]._depth - 1); } let baseIndentText = baseIndentArray.join(""); appendTreeElements2(d, unsafePath + "/", aT._kids[i], aIndentGuide, baseIndentText, tString.length); aIndentGuide.pop(); } } } appendSectionHeader(aPOuter, kTreeNames[aT._unsafeName]); let pre = appendElement(aPOuter, "pre", "entries"); appendTreeElements2(pre, /* prePath = */"", aT, [], "", rootStringLength); appendTextNode(aPOuter, "\n"); // gives nice spacing when we cut and paste } //--------------------------------------------------------------------------- function OtherReport(aUnsafePath, aUnits, aAmount, aDescription, aNMerged) { // Nb: _kind is not needed, it's always KIND_OTHER. this._unsafePath = aUnsafePath; this._units = aUnits; if (aAmount === kUnknown) { this._amount = 0; this._isUnknown = true; } else { this._amount = aAmount; } this._description = aDescription; this._asString = this.toString(); } OtherReport.prototype = { toString: function() { switch (this._units) { case UNITS_BYTES: return formatBytes(this._amount); case UNITS_COUNT: case UNITS_COUNT_CUMULATIVE: return formatInt(this._amount); case UNITS_PERCENTAGE: return formatPercentage(this._amount); default: assert(false, "bad units in OtherReport.toString"); } }, isInvalid: function() { let n = this._amount; switch (this._units) { case UNITS_BYTES: case UNITS_COUNT: case UNITS_COUNT_CUMULATIVE: return (n !== kUnknown && n < 0); case UNITS_PERCENTAGE: return (n !== kUnknown && !(0 <= n && n <= 10000)); default: assert(false, "bad units in OtherReport.isInvalid"); } } }; OtherReport.compare = function(a, b) { return a._unsafePath < b._unsafePath ? -1 : a._unsafePath > b._unsafePath ? 1 : 0; }; /** * Appends the elements for the "Other Measurements" section. * * @param aP * The parent DOM node. * @param aReportsByProcess * Table of Reports for this process, indexed by _unsafePath. * @param aProcess * The process these Reports correspond to. * @return The generated text. */ function appendOtherElements(aP, aReportsByProcess) { appendSectionHeader(aP, kTreeNames['other']); let pre = appendElement(aP, "pre", "entries"); // Generate an array of Report-like elements, stripping out all the // Reports that have already been handled. Also find the width of the // widest element, so we can format things nicely. let maxStringLength = 0; let otherReports = []; for (let unsafePath in aReportsByProcess) { let r = aReportsByProcess[unsafePath]; if (!r._done) { assert(r._nMerged === undefined, "dup'd OTHER report"); let o = new OtherReport(r._unsafePath, r._units, r._amount, r._description); otherReports.push(o); if (o._asString.length > maxStringLength) { maxStringLength = o._asString.length; } } } otherReports.sort(OtherReport.compare); // Generate text for the not-yet-printed values. let text = ""; for (let i = 0; i < otherReports.length; i++) { let o = otherReports[i]; let oIsInvalid = o.isInvalid(); if (oIsInvalid) { gUnsafePathsWithInvalidValuesForThisProcess.push(o._unsafePath); reportAssertionFailure("Invalid value for " + flipBackslashes(o._unsafePath)); } appendMrValueSpan(pre, pad(o._asString, maxStringLength, ' '), oIsInvalid); appendMrNameSpan(pre, KIND_OTHER, kNoKids, o._description, o._unsafePath, o._isUnknown, oIsInvalid); appendTextNode(pre, "\n"); } appendTextNode(aP, "\n"); // gives nice spacing when we cut and paste } function appendSectionHeader(aP, aText) { appendElementWithText(aP, "h2", "", aText); appendTextNode(aP, "\n"); } //----------------------------------------------------------------------------- // Code specific to about:compartments //----------------------------------------------------------------------------- function onLoadAboutCompartments() { // First generate the page, then minimize memory usage to collect any dead // compartments, then update the page. The first generation step may sound // unnecessary, but it avoids a short delay in showing content when the page // is loaded, which makes test_aboutcompartments.xul more reliable (see bug // 729018 for details). updateAboutCompartments(); minimizeMemoryUsage3x( function() { addChildObserversAndUpdate(updateAboutCompartments); }); } /** * Top-level function that does the work of generating the page. */ function updateAboutCompartments() { // First, clear the page contents. Necessary because // updateAboutCompartments() might be called more than once due to the // "child-memory-reporter-update" observer. let body = clearBody(); let mgr = Cc["@mozilla.org/memory-reporter-manager;1"]. getService(Ci.nsIMemoryReporterManager); // Generate output for one process at a time. Always start with the // Main process. let compartmentsByProcess = getCompartmentsByProcess(mgr); appendProcessCompartmentsElements(body, "Main", compartmentsByProcess["Main"]); for (let process in compartmentsByProcess) { if (process !== "Main") { appendProcessCompartmentsElements(body, process, compartmentsByProcess[process]); } } appendElement(body, "hr"); let div1 = appendElement(body, "div"); let a; if (gVerbose) { let a = appendElementWithText(div1, "a", "option", "Less verbose"); a.href = "about:compartments"; } else { let a = appendElementWithText(div1, "a", "option", "More verbose"); a.href = "about:compartments?verbose"; } } //--------------------------------------------------------------------------- function Compartment(aUnsafeName, aIsSystemCompartment) { this._unsafeName = aUnsafeName; this._isSystemCompartment = aIsSystemCompartment; // this._nMerged is only defined if > 1 } Compartment.prototype = { merge: function(r) { this._nMerged = this._nMerged ? this._nMerged + 1 : 2; } }; function getCompartmentsByProcess(aMgr) { // Ignore anything that didn't come from the "compartments" multi-reporter. // (Note that some such reports can reach here as single reports if they were // in the child process.) function ignoreSingle(aPath) { return !aPath.startsWith("compartments/"); } function ignoreMulti(aName) { return aName !== "compartments"; } let compartmentsByProcess = {}; function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount, aDescription) { let process = aProcess === "" ? "Main" : aProcess; let unsafeNames = aUnsafePath.split('/'); let isSystemCompartment; if (unsafeNames[0] === "compartments" && unsafeNames[1] == "system" && unsafeNames.length == 3) { isSystemCompartment = true; } else if (unsafeNames[0] === "compartments" && unsafeNames[1] == "user" && unsafeNames.length == 3) { isSystemCompartment = false; // These null principal compartments are user compartments according to // the JS engine, but they look odd being shown with content // compartments, so we put them in the system compartments list. if (unsafeNames[2].startsWith("moz-nullprincipal:{")) { isSystemCompartment = true; } } else { assert(false, "bad compartments path: " + aUnsafePath); } let c = new Compartment(unsafeNames[2], isSystemCompartment); if (!compartmentsByProcess[process]) { compartmentsByProcess[process] = {}; } let compartments = compartmentsByProcess[process]; let cOld = compartments[c._unsafeName]; if (cOld) { // Already an entry; must be a duplicated compartment. This can happen // legitimately. Merge them. cOld.merge(c); } else { compartments[c._unsafeName] = c; } } processMemoryReporters(aMgr, ignoreSingle, ignoreMulti, handleReport); return compartmentsByProcess; } //--------------------------------------------------------------------------- function appendProcessCompartmentsElementsHelper(aP, aCompartments, aKindString) { appendElementWithText(aP, "h2", "", aKindString + " Compartments\n"); let compartmentTextArray = []; let uPre = appendElement(aP, "pre", "entries"); for (let name in aCompartments) { let c = aCompartments[name]; let isSystemKind = aKindString === "System"; if (c._isSystemCompartment === isSystemKind) { let text = flipBackslashes(c._unsafeName); if (c._nMerged) { text += " [" + c._nMerged + "]"; } text += "\n"; compartmentTextArray.push(text); } } compartmentTextArray.sort(); for (let i = 0; i < compartmentTextArray.length; i++) { appendElementWithText(uPre, "span", "", compartmentTextArray[i]); } appendTextNode(aP, "\n"); // gives nice spacing when we cut and paste } /** * Appends the elements for a single process. * * @param aP * The parent DOM node. * @param aProcess * The name of the process. * @param aCompartments * Table of Compartments for this process, indexed by _unsafeName. * @return The generated text. */ function appendProcessCompartmentsElements(aP, aProcess, aCompartments) { appendElementWithText(aP, "h1", "", aProcess + " Process"); appendTextNode(aP, "\n\n"); // gives nice spacing when we cut and paste appendProcessCompartmentsElementsHelper(aP, aCompartments, "User"); appendProcessCompartmentsElementsHelper(aP, aCompartments, "System"); }