Bug 760352 (part 1) - Allow KIND_OTHER memory reports to be in trees. r=jlebar.
authorNicholas Nethercote <nnethercote@mozilla.com>
Mon, 11 Jun 2012 20:29:12 -0700
changeset 101345 5896112ab18ff0086fe5dea0a0bd2bf0fd5b4e0f
parent 101344 3f1c7f0a6ec495c9ddebd4fdfca8b5439ec30f46
child 101346 b6adf27298db88ba4315fb12b6e30a17b258f726
push id1316
push userakeybl@mozilla.com
push dateMon, 27 Aug 2012 22:37:00 +0000
treeherdermozilla-beta@db4b09302ee2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlebar
bugs760352
milestone16.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 760352 (part 1) - Allow KIND_OTHER memory reports to be in trees. r=jlebar.
dom/base/nsWindowMemoryReporter.cpp
js/xpconnect/src/XPCJSRuntime.cpp
toolkit/components/aboutmemory/content/aboutMemory.js
toolkit/components/aboutmemory/tests/test_aboutcompartments.xul
toolkit/components/aboutmemory/tests/test_aboutmemory.xul
toolkit/components/aboutmemory/tests/test_memoryReporters.xul
toolkit/components/telemetry/TelemetryPing.js
xpcom/base/MapsMemoryReporter.cpp
xpcom/base/nsIMemoryReporter.idl
--- a/dom/base/nsWindowMemoryReporter.cpp
+++ b/dom/base/nsWindowMemoryReporter.cpp
@@ -673,17 +673,17 @@ ReportGhostWindowsEnumerator(nsUint64Has
 
   nsCAutoString path;
   path.AppendLiteral("ghost-windows/");
   AppendWindowURI(window, path);
 
   nsresult rv = data->callback->Callback(
     /* process = */ EmptyCString(),
     path,
-    nsIMemoryReporter::KIND_SUMMARY,
+    nsIMemoryReporter::KIND_OTHER,
     nsIMemoryReporter::UNITS_COUNT,
     /* amount = */ 1,
     /* desc = */ EmptyCString(),
     data->closure);
 
   if (NS_FAILED(rv) && NS_SUCCEEDED(data->rv)) {
     data->rv = rv;
   }
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -1658,17 +1658,17 @@ class JSCompartmentsMultiReporter : publ
         Paths paths; 
         JS_IterateCompartments(nsXPConnect::GetRuntimeInstance()->GetJSRuntime(),
                                &paths, CompartmentCallback);
  
         // Report.
         for (size_t i = 0; i < paths.length(); i++)
             // These ones don't need a description, hence the "".
             REPORT(nsCString(paths[i]),
-                   nsIMemoryReporter::KIND_SUMMARY,
+                   nsIMemoryReporter::KIND_OTHER,
                    nsIMemoryReporter::UNITS_COUNT,
                    1, "");
 
         return NS_OK;
     }
 
     NS_IMETHOD
     GetExplicitNonHeap(PRInt64 *n)
--- a/toolkit/components/aboutmemory/content/aboutMemory.js
+++ b/toolkit/components/aboutmemory/content/aboutMemory.js
@@ -13,17 +13,16 @@
 
 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 KIND_SUMMARY           = Ci.nsIMemoryReporter.KIND_SUMMARY;
 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.
@@ -81,17 +80,17 @@ function debug(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") {
@@ -141,17 +140,17 @@ function minimizeMemoryUsage3x(fAfter)
       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.
@@ -169,98 +168,61 @@ function processMemoryReporters(aMgr, aI
   // - 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);
-  }
-
   let e = aMgr.enumerateReporters();
   while (e.hasMoreElements()) {
     let rOrig = e.getNext().QueryInterface(Ci.nsIMemoryReporter);
     let unsafePath;
     try {
       unsafePath = rOrig.path;
       if (!aIgnoreSingle(unsafePath)) {
-        handleReport(rOrig.process, unsafePath, rOrig.kind, rOrig.units, 
-                     rOrig.amount, rOrig.description);
+        aHandleReport(rOrig.process, unsafePath, rOrig.kind, rOrig.units,
+                      rOrig.amount, rOrig.description);
       }
     }
     catch (ex) {
       debug("Exception thrown by memory reporter: " + unsafePath + ": " + ex);
     }
   }
 
   let e = aMgr.enumerateMultiReporters();
   while (e.hasMoreElements()) {
     let mr = e.getNext().QueryInterface(Ci.nsIMemoryMultiReporter);
     let name = mr.name;
     try {
       if (!aIgnoreMulti(name)) {
-        mr.collectReports(handleReport, null);
+        mr.collectReports(aHandleReport, null);
       }
     }
     catch (ex) {
       // 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
+      //   (i.e. assertion failures in aHandleReport);  such exceptions contain
       //   gAssertionFailureMsgPrefix in their string representation.
       //
       // - We want to continue on when faced with exceptions thrown outside
       //   this file (i.e. when measuring an amount in collectReports).
       let str = ex.toString();
       if (str.search(gAssertionFailureMsgPrefix) >= 0) {
-        throw(ex); 
+        throw(ex);
       } else {
         debug("Exception thrown within memory multi-reporter: " + name + ": " +
               ex);
       }
     }
   }
 }
 
-// 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].*\.\)?$/m;
-
-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(gSentenceRegExp.test(aDescription),
-           "non-sentence explicit description");
-
-  } else if (isSmapsPath(aUnsafePath)) {
-    assert(aKind === KIND_NONHEAP, "bad smaps kind");
-    assert(aUnits === UNITS_BYTES, "bad smaps units");
-    assert(aDescription !== "", "empty smaps description");
-
-  } else if (aKind === KIND_SUMMARY) {
-    assert(!aUnsafePath.startsWith("explicit/") && !isSmapsPath(aUnsafePath),
-           "bad SUMMARY path");
-
-  } else {
-    assert(aUnsafePath.indexOf("/") === -1, "'other' path contains '/'");
-    assert(aKind === KIND_OTHER, "bad other kind");
-    assert(gSentenceRegExp.test(aDescription),
-           "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');
@@ -308,61 +270,67 @@ as malloc, calloc, realloc, memalign, op
 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':
+  'rss':
 "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' \
+Note that the 'rss' 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':
+  'size':
 "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 \
+takes up (a.k.a. the mapping's 'vsize').  A mapping may have a large size 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 \
+Note that the 'size' 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 kSectionNames = {
   'explicit': 'Explicit Allocations',
-  'resident': 'Resident Set Size (RSS) Breakdown',
+  'rss':      'Resident Set Size (RSS) Breakdown',
   'pss':      'Proportional Set Size (PSS) Breakdown',
-  'vsize':    'Virtual Size Breakdown',
-  'swap':     'Swap Usage Breakdown',
+  'size':     'Virtual Size Breakdown',
+  'swap':     'Swap Breakdown',
   'other':    'Other Measurements'
 };
 
-const kSmapsTreePrefixes = ['resident/', 'pss/', 'vsize/', 'swap/'];
+const kSmapsTreeNames    = ['rss',  'pss',  'size',  'swap' ];
+const kSmapsTreePrefixes = ['rss/', 'pss/', 'size/', 'swap/'];
+
+function isExplicitPath(aUnsafePath)
+{
+  return aUnsafePath.startsWith("explicit/");
+}
 
 function isSmapsPath(aUnsafePath)
 {
   for (let i = 0; i < kSmapsTreePrefixes.length; i++) {
     if (aUnsafePath.startsWith(kSmapsTreePrefixes[i])) {
       return true;
     }
   }
@@ -406,31 +374,31 @@ 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);
 
-  let treesByProcess = {}, othersByProcess = {}, heapTotalByProcess = {};
-  getTreesAndOthersByProcess(mgr, treesByProcess, othersByProcess,
-                             heapTotalByProcess);
+  let treesByProcess = {}, degeneratesByProcess = {}, heapTotalByProcess = {};
+  getTreesByProcess(mgr, treesByProcess, degeneratesByProcess,
+                    heapTotalByProcess);
 
   // Generate output for one process at a time.  Always start with the
   // Main process.
   let hasMozMallocUsableSize = mgr.hasMozMallocUsableSize;
   appendProcessAboutMemoryElements(body, "Main", treesByProcess["Main"],
-                                   othersByProcess["Main"],
+                                   degeneratesByProcess["Main"],
                                    heapTotalByProcess["Main"],
                                    hasMozMallocUsableSize);
   for (let process in treesByProcess) {
     if (process !== "Main") {
       appendProcessAboutMemoryElements(body, process, treesByProcess[process],
-                                       othersByProcess[process],
+                                       degeneratesByProcess[process],
                                        heapTotalByProcess[process],
                                        hasMozMallocUsableSize);
     }
   }
 
   appendElement(body, "hr");
 
   // Memory-related actions.
@@ -480,131 +448,156 @@ function updateAboutMemory()
                     "to see a description of what it measures.";
 
   appendElementWithText(body, "div", "legend", legendText1);
   appendElementWithText(body, "div", "legend", legendText2);
 }
 
 //---------------------------------------------------------------------------
 
+// 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].*\.\)?$/m;
+
 /**
  * This function reads all the memory reports, and puts that data in structures
  * that will be used to generate the page.
  *
  * @param aMgr
  *        The memory reporter manager.
  * @param aTreesByProcess
- *        Table of trees, indexed by process, which this function appends to.
- * @param aOthersByProcess
- *        Table of other lists, indexed by process, which this function appends
- *        to.
+ *        Table of non-degenerate trees, indexed by process, which this
+ *        function appends to.
+ * @param aDegeneratesByProcess
+ *        Table of degenerate trees, indexed by process, which this function
+ *        appends to.
  * @param aHeapTotalByProcess
  *        Table of heap total counts, indexed by process, which this function
  *        appends to.
  */
-function getTreesAndOthersByProcess(aMgr, aTreesByProcess, aOthersByProcess,
-                                    aHeapTotalByProcess)
+function getTreesByProcess(aMgr, aTreesByProcess, aDegeneratesByProcess,
+                           aHeapTotalByProcess)
 {
   // Ignore the "smaps" multi-reporter in non-verbose mode, and the
   // "compartments" and "ghost-windows" multi-reporters 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(aUnsafePath) 
+  function ignoreSingle(aUnsafePath)
   {
     return (isSmapsPath(aUnsafePath) && !gVerbose) ||
            aUnsafePath.startsWith("compartments/") ||
            aUnsafePath.startsWith("ghost-windows/");
   }
 
   function ignoreMulti(aMRName)
   {
     return (aMRName === "smaps" && !gVerbose) ||
            aMRName === "compartments" ||
            aMRName === "ghost-windows";
   }
 
   function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
                         aDescription)
   {
-    let process = aProcess === "" ? "Main" : aProcess;
+    if (isExplicitPath(aUnsafePath)) {
+      assert(aKind === KIND_HEAP || aKind === KIND_NONHEAP, "bad explicit kind");
+      assert(aUnits === UNITS_BYTES, "bad explicit units");
+      assert(gSentenceRegExp.test(aDescription),
+             "non-sentence explicit description");
+
+    } else if (isSmapsPath(aUnsafePath)) {
+      assert(aKind === KIND_NONHEAP, "bad smaps kind");
+      assert(aUnits === UNITS_BYTES, "bad smaps units");
+      assert(aDescription !== "", "empty smaps description");
+
+    } else {
+      assert(aKind === KIND_OTHER, "bad other kind");
+      assert(gSentenceRegExp.test(aDescription),
+             "non-sentence other description");
+    }
 
-    if (aUnsafePath.indexOf('/') !== -1) {
-      // Tree report.  Get the tree for the process, creating it if necessary.
-      // All the trees for each process ("explicit", "vsize", etc) are stored
-      // in a "tree-of-trees".  This makes things simple later.
-      if (!aTreesByProcess[process]) {
-        aTreesByProcess[process] = new TreeNode("tree-of-trees");
-      }
-      let t = aTreesByProcess[process]; 
+    let process = aProcess === "" ? "Main" : aProcess;
+    let unsafeNames = aUnsafePath.split('/');
+    let unsafeName0 = unsafeNames[0];
+    let isDegenerate = unsafeNames.length === 1;
 
+    // Get the appropriate trees table (non-degenerate or degenerate) for the
+    // process, creating it if necessary.
+    let t;
+    let thingsByProcess =
+      isDegenerate ? aDegeneratesByProcess : aTreesByProcess;
+    let things = thingsByProcess[process];
+    if (!thingsByProcess[process]) {
+      things = thingsByProcess[process] = {};
+    }
+
+    // Get the root node, creating it if necessary.
+    t = things[unsafeName0];
+    if (!t) {
+      t = things[unsafeName0] =
+        new TreeNode(unsafeName0, aUnits, isDegenerate);
+    }
+
+    if (!isDegenerate) {
       // Add any missing nodes in the tree implied by aUnsafePath, and fill in
       // the properties that we can with a top-down traversal.
-      let unsafeNames = aUnsafePath.split('/');
-      let u = t;
-      for (let i = 0; i < unsafeNames.length; i++) {
+      for (let i = 1; i < unsafeNames.length; i++) {
         let unsafeName = unsafeNames[i];
-        let uMatch = u.findKid(unsafeName);
-        if (uMatch) {
-          u = uMatch;
-        } else {
-          let v = new TreeNode(unsafeName);
-          if (!u._kids) {
-            u._kids = [];
+        let u = t.findKid(unsafeName);
+        if (!u) {
+          u = new TreeNode(unsafeName, aUnits, isDegenerate);
+          if (!t._kids) {
+            t._kids = [];
           }
-          u._kids.push(v);
-          u = v;
+          t._kids.push(u);
         }
-      }
-    
-      if (u._amount) {
-        // Duplicate!  Sum the values and mark it as a dup.
-        u._amount += aAmount;
-        u._nMerged = u._nMerged ? u._nMerged + 1 : 2;
-      } else {
-        // New leaf node.  Fill in extra details node from the report.
-        u._amount = aAmount;
-        u._description = aDescription;
+        t = u;
       }
 
-      if (unsafeNames[0] === "explicit" && aKind == KIND_HEAP) {
+      // Update the heap total if necessary.
+      if (unsafeName0 === "explicit" && aKind == KIND_HEAP) {
         if (!aHeapTotalByProcess[process]) {
           aHeapTotalByProcess[process] = 0;
         }
         aHeapTotalByProcess[process] += aAmount;
       }
+    }
 
+    if (t._amount) {
+      // Duplicate!  Sum the values and mark it as a dup.
+      t._amount += aAmount;
+      t._nMerged = t._nMerged ? t._nMerged + 1 : 2;
     } else {
-      // "Other" (non-tree) report.  Get the "others" for the process, creating
-      // it if necessary.
-      if (!aOthersByProcess[process]) {
-        aOthersByProcess[process] = {};
-      }
-      let others = aOthersByProcess[process]; 
-
-      // Record the report.
-      assert(!others[aUnsafePath], "dup'd OTHER report");
-      others[aUnsafePath] =
-        new OtherReport(aUnsafePath, aUnits, aAmount, aDescription);
+      // New leaf node.  Fill in extra details node from the report.
+      t._amount = aAmount;
+      t._description = aDescription;
     }
   }
 
   processMemoryReporters(aMgr, ignoreSingle, ignoreMulti, handleReport);
 }
 
 //---------------------------------------------------------------------------
 
 // There are two kinds of TreeNode.
 // - Leaf TreeNodes correspond to reports.
 // - Non-leaf TreeNodes are just scaffolding nodes for the tree;  their values
 //   are derived from their children.
-function TreeNode(aUnsafeName)
+// Some trees are "degenerate", i.e. they contain a single node, i.e. they
+// correspond to a report whose path has no '/' separators.
+function TreeNode(aUnsafeName, aUnits, aIsDegenerate)
 {
-  // Nb: _units is not needed, it's always UNITS_BYTES.
+  this._units = aUnits;
   this._unsafeName = aUnsafeName;
+  if (aIsDegenerate) {
+    this._isDegenerate = true;
+  }
+
   // Leaf TreeNodes have these properties added immediately after construction:
   // - _amount
   // - _description
   // - _nMerged (only defined if > 1)
   //
   // Non-leaf TreeNodes have these properties added later:
   // - _kids
   // - _amount
@@ -620,56 +613,56 @@ TreeNode.prototype = {
           return this._kids[i];
         }
       }
     }
     return undefined;
   },
 
   toString: function() {
-    return formatBytes(this._amount);
+    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 TreeNode.toString");
+    }
   }
 };
 
-TreeNode.compare = function(a, b) {
+TreeNode.compareAmounts = function(a, b) {
   return b._amount - a._amount;
 };
 
+TreeNode.compareUnsafeNames = function(a, b) {
+  return a._unsafeName < b._unsafeName ? -1 :
+         a._unsafeName > b._unsafeName ?  1 :
+         0;
+};
+
+
 /**
  * Fill in the remaining properties for the specified tree in a bottom-up
  * fashion.
  *
- * @param aTreeOfTrees
- *        The tree-of-trees.
- * @param aTreePrefix
- *        The prefix (name) of the tree being built.  Must have '/' on the end.
- * @return The built tree.
+ * @param aRoot
+ *        The tree root.
  */
-function fillInTree(aTreeOfTrees, aTreePrefix)
+function fillInTree(aRoot)
 {
-  assert(aTreePrefix.indexOf('/') == aTreePrefix.length - 1,
-         "aTreePrefix doesn't end in '/'");
-
-  // There should always be an "explicit/" tree.  But smaps trees might not be
-  // present;  if that happens, return early.
-  let t = aTreeOfTrees.findKid(aTreePrefix.replace(/\//g, ''));
-  if (!t) {
-    assert(aTreePrefix !== 'explicit/', "missing explicit tree");
-    return null;
-  }
-
-  // Next, fill in the remaining properties bottom-up.
-  function fillInNonLeafNodes(aT, aCannotMerge)
+  // Fill in the remaining properties bottom-up.
+  function fillInNonLeafNodes(aT)
   {
     if (!aT._kids) {
       // Leaf node.  Has already been filled in.
 
-    } else if (aT._kids.length === 1 && !aCannotMerge) {
-      // Non-leaf node with one child.  Merge the child with the node to avoid
-      // redundant entries.
+    } else if (aT._kids.length === 1 && aT != aRoot) {
+      // Non-root, non-leaf node with one child.  Merge the child with the node
+      // to avoid redundant entries.
       let kid = aT._kids[0];
       let kidBytes = fillInNonLeafNodes(kid);
       aT._unsafeName += '/' + kid._unsafeName;
       if (kid._kids) {
         aT._kids = kid._kids;
       } else {
         delete aT._kids;
       }
@@ -682,51 +675,47 @@ function fillInTree(aTreeOfTrees, aTreeP
 
     } else {
       // Non-leaf node with multiple children.  Derive its _amount and
       // _description entirely from its children.
       let kidsBytes = 0;
       for (let i = 0; i < aT._kids.length; i++) {
         kidsBytes += fillInNonLeafNodes(aT._kids[i]);
       }
+      assert(aT._amount === undefined, "_amount already set for non-leaf node");
       aT._amount = kidsBytes;
       aT._description = "The sum of all entries below this one.";
     }
     return aT._amount;
   }
 
   // cannotMerge is set because don't want to merge into a tree's root node.
-  fillInNonLeafNodes(t, /* cannotMerge = */true);
-
-  // Set the (unsafe) description on the root node.
-  t._description = kTreeDescriptions[t._unsafeName];
-
-  return t;
+  fillInNonLeafNodes(aRoot);
 }
 
 /**
  * Compute the "heap-unclassified" value and insert it into the "explicit"
  * tree.
  *
  * @param aT
  *        The "explicit" tree.
- * @param aOthers
- *        "Other measurements" for this process, indexed by _unsafePath.
+ * @param aHeapAllocatedNode
+ *        The "heap-allocated" tree node.
  * @param aHeapTotal
  *        The sum of all explicit HEAP reporters for this process.
  * @return A boolean indicating if "heap-allocated" is known for the process.
  */
-function addHeapUnclassifiedNode(aT, aOthers, aHeapTotal)
+function addHeapUnclassifiedNode(aT, aHeapAllocatedNode, aHeapTotal)
 {
-  let heapAllocatedReport = aOthers["heap-allocated"];
-  if (heapAllocatedReport === undefined)
+  if (aHeapAllocatedNode === undefined)
     return false;
 
-  let heapAllocatedBytes = heapAllocatedReport._amount;
-  let heapUnclassifiedT = new TreeNode("heap-unclassified");
+  assert(aHeapAllocatedNode._isDegenerate, "heap-allocated is not degenerate");
+  let heapAllocatedBytes = aHeapAllocatedNode._amount;
+  let heapUnclassifiedT = new TreeNode("heap-unclassified", UNITS_BYTES);
   heapUnclassifiedT._amount = heapAllocatedBytes - aHeapTotal;
   heapUnclassifiedT._description =
       "Memory not classified by a more specific 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 true;
@@ -750,17 +739,17 @@ function sortTreeAndInsertAggregateNodes
     return !gVerbose &&
            (100 * aT._amount / aTotalBytes) < kSignificanceThresholdPerc;
   }
 
   if (!aT._kids) {
     return;
   }
 
-  aT._kids.sort(TreeNode.compare);
+  aT._kids.sort(TreeNode.compareAmounts);
 
   // If the first child is insignificant, they all are, and there's no point
   // creating an aggregate node that lacks siblings.  Just set the parent's
   // _hideKids property and process all children.
   if (isInsignificant(aT._kids[0])) {
     aT._hideKids = true;
     for (let i = 0; i < aT._kids.length; i++) {
       sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
@@ -771,31 +760,32 @@ function sortTreeAndInsertAggregateNodes
   // 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)");
+      // Create an aggregate node.  Inherit units from the parent;  everything
+      // in the tree should have the same units anyway (we test this later).
+      let aggT = new TreeNode("(" + nAgg + " tiny)", aT._units);
       aggT._kids = [];
       let aggBytes = 0;
       for ( ; i < aT._kids.length; i++) {
         aggBytes += aT._kids[i]._amount;
         aggT._kids.push(aT._kids[i]);
       }
       aggT._hideKids = true;
       aggT._amount = aggBytes;
       aggT._description =
         nAgg + " sub-trees that are below the " + kSignificanceThresholdPerc +
         "% significance threshold.";
       aT._kids.splice(i0, nAgg, aggT);
-      aT._kids.sort(TreeNode.compare);
+      aT._kids.sort(TreeNode.compareAmounts);
 
       // Process the moved children.
       for (i = 0; i < aggT._kids.length; i++) {
         sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]);
       }
       return;
     }
 
@@ -812,48 +802,48 @@ function sortTreeAndInsertAggregateNodes
 // 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", "", 
+    appendElementWithText(aP, "p", "",
       "WARNING: the 'heap-allocated' memory reporter and the " +
       "moz_malloc_usable_size() function do not work for this platform " +
       "and/or configuration.  This means that 'heap-unclassified' is not " +
       "shown and the 'explicit' tree shows much less memory than it should.\n\n");
 
   } else if (!aHasKnownHeapAllocated) {
-    appendElementWithText(aP, "p", "", 
+    appendElementWithText(aP, "p", "",
       "WARNING: the 'heap-allocated' memory reporter does not work for this " +
       "platform and/or configuration. This means that 'heap-unclassified' " +
       "is not shown and the 'explicit' tree shows less memory than it should.\n\n");
 
   } else if (!aHasMozMallocUsableSize) {
-    appendElementWithText(aP, "p", "", 
+    appendElementWithText(aP, "p", "",
       "WARNING: the moz_malloc_usable_size() function does not work for " +
       "this platform and/or configuration.  This means that much of the " +
       "heap-allocated memory is not measured by individual memory reporters " +
       "and so will fall under 'heap-unclassified'.\n\n");
   }
 
   if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) {
     let div = appendElement(aP, "div");
-    appendElementWithText(div, "p", "", 
+    appendElementWithText(div, "p", "",
       "WARNING: the following values are negative or unreasonably large.\n");
 
     let ul = appendElement(div, "ul");
     for (let i = 0;
          i < gUnsafePathsWithInvalidValuesForThisProcess.length;
          i++)
     {
       appendTextNode(ul, " ");
-      appendElementWithText(ul, "li", "", 
+      appendElementWithText(ul, "li", "",
         flipBackslashes(gUnsafePathsWithInvalidValuesForThisProcess[i]) + "\n");
     }
 
     appendElementWithText(div, "p", "",
       "This indicates a defect in one or more memory reporters.  The " +
       "invalid values are highlighted.\n\n");
     gUnsafePathsWithInvalidValuesForThisProcess = [];  // reset for the next process
   }
@@ -861,56 +851,107 @@ function appendWarningElements(aP, aHasK
 
 /**
  * Appends the about:memory elements for a single process.
  *
  * @param aP
  *        The parent DOM node.
  * @param aProcess
  *        The name of the process.
- * @param aTreeOfTrees
- *        The tree-of-trees for this process.
- * @param aOthers
- *        The "other measurements" for this process.
+ * @param aTrees
+ *        The table of non-degenerate trees for this process.
+ * @param aDegenerates
+ *        The table of degenerate trees for this process.
  * @param aHasMozMallocUsableSize
  *        Boolean indicating if moz_malloc_usable_size works.
  * @return The generated text.
  */
-function appendProcessAboutMemoryElements(aP, aProcess, aTreeOfTrees, aOthers,
+function appendProcessAboutMemoryElements(aP, aProcess, aTrees, aDegenerates,
                                           aHeapTotal, aHasMozMallocUsableSize)
 {
   appendElementWithText(aP, "h1", "", aProcess + " Process\n\n");
 
   // We'll fill this in later.
   let warningsDiv = appendElement(aP, "div", "accuracyWarning");
 
-  let explicitTree = fillInTree(aTreeOfTrees, "explicit/");
-  let hasKnownHeapAllocated =
-    addHeapUnclassifiedNode(explicitTree, aOthers, aHeapTotal);
-  sortTreeAndInsertAggregateNodes(explicitTree._amount, explicitTree);
-  appendTreeElements(aP, explicitTree, aProcess);
+  // The explicit tree.
+  let hasKnownHeapAllocated;
+  {
+    let treeName = "explicit";
+    let t = aTrees[treeName];
+    assert(t, "no explicit tree");
+    fillInTree(t);
+    hasKnownHeapAllocated =
+      addHeapUnclassifiedNode(t, aDegenerates["heap-allocated"], aHeapTotal);
+    sortTreeAndInsertAggregateNodes(t._amount, t);
+    t._description = kTreeDescriptions[treeName];
+    let pre = appendSectionHeader(aP, kSectionNames[treeName]);
+    appendTreeElements(pre, t, aProcess, "");
+    appendTextNode(aP, "\n");  // gives nice spacing when we cut and paste
+    delete aTrees[treeName];
+  }
 
-  // We only show these breakdown trees in verbose mode.
+  // The smaps trees, which are only shown in verbose mode.
   if (gVerbose) {
-    kSmapsTreePrefixes.forEach(function(aTreePrefix) {
-      let t = fillInTree(aTreeOfTrees, aTreePrefix);
-
+    kSmapsTreeNames.forEach(function(aTreeName) {
       // |t| will be undefined if we don't have any reports for the given
       // unsafePath.
+      let t = aTrees[aTreeName];
       if (t) {
+        fillInTree(t);
         sortTreeAndInsertAggregateNodes(t._amount, t);
+        t._description = kTreeDescriptions[aTreeName];
         t._hideKids = true;   // smaps trees are always initially collapsed
-        appendTreeElements(aP, t, aProcess);
+        let pre = appendSectionHeader(aP, kSectionNames[aTreeName]);
+        appendTreeElements(pre, t, aProcess, "");
+        appendTextNode(aP, "\n");  // gives nice spacing when we cut and paste
+        delete aTrees[aTreeName];
       }
     });
   }
 
-  // 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, aOthers);
+  // Fill in and sort all the non-degenerate other trees.
+  let otherTrees = [];
+  for (let unsafeName in aTrees) {
+    let t = aTrees[unsafeName];
+    assert(!t._isDegenerate, "tree is degenerate");
+    fillInTree(t);
+    sortTreeAndInsertAggregateNodes(t._amount, t);
+    otherTrees.push(t);
+  }
+  otherTrees.sort(TreeNode.compareUnsafeNames);
+
+  // Get the length of the longest root value among the degenerate other trees,
+  // and sort them as well.
+  let otherDegenerates = [];
+  let maxStringLength = 0;
+  for (let unsafeName in aDegenerates) {
+    let t = aDegenerates[unsafeName];
+    assert(t._isDegenerate, "tree is not degenerate");
+    let length = t.toString().length;
+    if (length > maxStringLength) {
+      maxStringLength = length;
+    }
+    otherDegenerates.push(t);
+  }
+  otherDegenerates.sort(TreeNode.compareUnsafeNames);
+
+  // Now generate the elements, putting non-degenerate trees first. 
+  let pre = appendSectionHeader(aP, kSectionNames['other']);
+  for (let i = 0; i < otherTrees.length; i++) {
+    let t = otherTrees[i];
+    appendTreeElements(pre, t, aProcess, "");
+    appendTextNode(pre, "\n");  // blank lines after non-degenerate trees
+  }
+  for (let i = 0; i < otherDegenerates.length; i++) {
+    let t = otherDegenerates[i];
+    let padText = pad("", maxStringLength - t.toString().length, ' ');
+    appendTreeElements(pre, t, aProcess, padText);
+  }
+  appendTextNode(aP, "\n");  // gives nice spacing when we cut and paste
 
   // 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);
 }
 
@@ -1043,22 +1084,16 @@ const kHorizontal                   = "\
       kVerticalAndRight             = "\u251c",
       kVerticalAndRight_Right_Right = "\u251c\u2500\u2500",
       kVertical_Space_Space         = "\u2502  ";
 
 const kNoKidsSep                    = " \u2500\u2500 ",
       kHideKidsSep                  = " ++ ",
       kShowKidsSep                  = " -- ";
 
-function appendMrValueSpan(aP, aValue, aIsInvalid)
-{
-  appendElementWithText(aP, "span", "mrValue" + (aIsInvalid ? " invalid" : ""),
-                        aValue);
-}
-
 function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged)
 {
   let safeName = flipBackslashes(aUnsafeName);
   if (!aIsInvalid && !aNMerged) {
     safeName += "\n";
   }
   let nameSpan = appendElementWithText(aP, "span", "mrName", safeName);
   nameSpan.title = aDescription;
@@ -1154,90 +1189,83 @@ function expandPathToThisElement(aElemen
   } else {
     assertClassListContains(aElement, "entries");
   }
 }
 
 /**
  * Appends the elements for the tree, including its heading.
  *
- * @param aPOuter
+ * @param aP
  *        The parent DOM node.
- * @param aT
- *        The tree.
+ * @param aRoot
+ *        The tree root.
  * @param aProcess
  *        The process the tree corresponds to.
+ * @param aPadText
+ *        A string to pad the start of each entry.
  */
-function appendTreeElements(aPOuter, aT, aProcess)
+function appendTreeElements(aP, aRoot, aProcess, aPadText)
 {
-  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 aProcess
+   *        The process the tree corresponds to.
    * @param aUnsafeNames
    *        An array of the names forming the path to aT.
+   * @param aRoot
+   *        The root of the tree this sub-tree belongs to.
    * @param aT
    *        The tree.
    * @param aTreelineText1
    *        The first part of the treeline for this entry and this entry's
    *        children.
    * @param aTreelineText2a
    *        The second part of the treeline for this entry.
    * @param aTreelineText2b
    *        The second part of the treeline for this entry's children.
    * @param aParentStringLength
    *        The length of the formatted byte count of the top node in the tree.
    */
-  function appendTreeElements2(aP, aUnsafeNames, aT, aTreelineText1,
-                               aTreelineText2a, aTreelineText2b,
-                               aParentStringLength)
+  function appendTreeElements2(aP, aProcess, aUnsafeNames, aRoot, aT,
+                               aTreelineText1, aTreelineText2a,
+                               aTreelineText2b, aParentStringLength)
   {
     function appendN(aS, aC, aN)
     {
       for (let i = 0; i < aN; i++) {
         aS += aC;
       }
       return aS;
     }
 
-    // Indent more if this entry is narrower than its parent.
+    // The tree line.  Indent more if this entry is narrower than its parent.
     let valueText = aT.toString();
     let extraTreelineLength =
       Math.max(aParentStringLength - valueText.length, 0);
     if (extraTreelineLength > 0) {
       aTreelineText2a =
         appendN(aTreelineText2a, kHorizontal, extraTreelineLength);
       aTreelineText2b =
         appendN(aTreelineText2b, " ",         extraTreelineLength);
     }
     let treelineText = aTreelineText1 + aTreelineText2a;
     appendElementWithText(aP, "span", "treeline", treelineText);
 
-    // Generate the percentage;  detect and record invalid values at the same
-    // time.
-    let percText;
+    // Detect and record invalid values.
+    assert(aRoot._units === aT._units, "units within a tree are inconsistent");
     let tIsInvalid = false;
-    if (aT._amount === treeBytes) {
-      percText = " (100.0%)";
-    } else {
-      if (!(0 <= aT._amount && aT._amount <= treeBytes)) {
-        tIsInvalid = true;
-        let unsafePath = aUnsafeNames.join("/");
-        gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath);
-        reportAssertionFailure("Invalid value for " +
-                               flipBackslashes(unsafePath));
-      }
-      let num = 100 * aT._amount / treeBytes;
-      let numText = num.toFixed(2);
-      percText = (0 <= num && num < 10 ? " (0" : " (") + numText + "%)";
+    if (!(0 <= aT._amount && aT._amount <= aRoot._amount)) {
+      tIsInvalid = true;
+      let unsafePath = aUnsafeNames.join("/");
+      gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath);
+      reportAssertionFailure("Invalid value for " + flipBackslashes(unsafePath));
     }
 
     // For non-leaf nodes, the entire sub-tree is put within a span so it can
     // be collapsed if the node is clicked on.
     let d;
     let sep;
     let showSubtrees;
     if (aT._kids) {
@@ -1254,149 +1282,81 @@ function appendTreeElements(aPOuter, aT,
       d.onclick = toggle;
       sep = showSubtrees ? kShowKidsSep : kHideKidsSep;
     } else {
       assert(!aT._hideKids, "leaf node with _hideKids set")
       sep = kNoKidsSep;
       d = aP;
     }
 
-    appendMrValueSpan(d, valueText, tIsInvalid);
-    appendElementWithText(d, "span", "mrPerc", percText);
+    // The value.
+    appendElementWithText(d, "span", "mrValue" + (tIsInvalid ? " invalid" : ""),
+                          valueText);
+
+    // The percentage (omitted for single entries).
+    let percText;
+    if (!aT._isDegenerate) {
+      if (aT._amount === aRoot._amount) {
+        percText = " (100.0%)";
+      } else {
+        let num = 100 * aT._amount / aRoot._amount;
+        let numText = num.toFixed(2);
+        percText = (0 <= num && num < 10 ? " (0" : " (") + numText + "%)";
+      }
+      appendElementWithText(d, "span", "mrPerc", percText);
+    }
+
+    // The separator.
     appendElementWithText(d, "span", "mrSep", sep);
 
+    // The entry's name.
     appendMrNameSpan(d, aT._description, aT._unsafeName,
                      tIsInvalid, aT._nMerged);
 
     // 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);
     }
 
+    // Recurse over children.
     if (aT._kids) {
       // The 'kids' class is just used for sanity checking in toggle().
       d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden");
 
       let kidTreelineText1 = aTreelineText1 + aTreelineText2b;
       for (let i = 0; i < aT._kids.length; i++) {
         let kidTreelineText2a, kidTreelineText2b;
         if (i < aT._kids.length - 1) {
           kidTreelineText2a = kVerticalAndRight_Right_Right;
           kidTreelineText2b = kVertical_Space_Space;
         } else {
           kidTreelineText2a = kUpAndRight_Right_Right;
           kidTreelineText2b = "   ";
         }
         aUnsafeNames.push(aT._kids[i]._unsafeName);
-        appendTreeElements2(d, aUnsafeNames, aT._kids[i], kidTreelineText1,
-                            kidTreelineText2a, kidTreelineText2b,
-                            valueText.length);
+        appendTreeElements2(d, aProcess, aUnsafeNames, aRoot, aT._kids[i],
+                            kidTreelineText1, kidTreelineText2a,
+                            kidTreelineText2b, valueText.length);
         aUnsafeNames.pop();
       }
     }
   }
 
-  appendSectionHeader(aPOuter, kSectionNames[aT._unsafeName]);
- 
-  let pre = appendElement(aPOuter, "pre", "entries");
-  appendTreeElements2(pre, [aT._unsafeName], aT, "", "", "", rootStringLength);
-  appendTextNode(aPOuter, "\n");  // gives nice spacing when we cut and paste
+  let rootStringLength = aRoot.toString().length;
+  appendTreeElements2(aP, aProcess, [aRoot._unsafeName], aRoot, aRoot,
+                      aPadText, "", "", rootStringLength);
 }
 
 //---------------------------------------------------------------------------
 
-function OtherReport(aUnsafePath, aUnits, aAmount, aDescription, aNMerged)
-{
-  this._unsafePath = aUnsafePath;
-  this._units    = aUnits;
-  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 < 0;
-      case UNITS_PERCENTAGE:       return n < 0; /* percentages may be greater than 100% */
-      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 aOthers
- *        The "other measurements" for this process.
- */
-function appendOtherElements(aP, aOthers)
-{
-  appendSectionHeader(aP, kSectionNames['other']);
-
-  let pre = appendElement(aP, "pre", "entries");
-
-  // Convert the table of OtherReports to an array.  Also find the width of the
-  // widest element, so we can format things nicely.
-  let maxStringLength = 0;
-  let otherReports = [];
-  for (let unsafePath in aOthers) {
-    let o = aOthers[unsafePath];
-    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);
-    appendElementWithText(pre, "span", "mrSep", kNoKidsSep);
-    appendMrNameSpan(pre, o._description, o._unsafePath, oIsInvalid);
-  }
-
-  appendTextNode(aP, "\n");  // gives nice spacing when we cut and paste
-}
-
 function appendSectionHeader(aP, aText)
 {
   appendElementWithText(aP, "h2", "", aText + "\n");
+  return appendElement(aP, "pre", "entries");
 }
 
 //-----------------------------------------------------------------------------
 // Code specific to about:compartments
 //-----------------------------------------------------------------------------
 
 function onLoadAboutCompartments()
 {
@@ -1470,17 +1430,17 @@ Compartment.prototype = {
 };
 
 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(aUnsafePath) 
+  function ignoreSingle(aUnsafePath)
   {
     return !aUnsafePath.startsWith("compartments/");
   }
 
   function ignoreMulti(aMRName)
   {
     return aMRName !== "compartments";
   }
@@ -1507,16 +1467,21 @@ function getCompartmentsByProcess(aMgr)
       // 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);
     }
+    assert(aKind === KIND_OTHER,   "bad compartments kind");
+    assert(aUnits === UNITS_COUNT, "bad compartments units");
+    assert(aAmount === 1,          "bad compartments amount");
+    assert(aDescription === "",    "bad compartments description");
+
     let c = new Compartment(unsafeNames[2], isSystemCompartment);
 
     if (!compartmentsByProcess[process]) {
       compartmentsByProcess[process] = {};
     }
     let compartments = compartmentsByProcess[process];
     let cOld = compartments[c._unsafeName];
     if (cOld) {
@@ -1545,34 +1510,38 @@ function GhostWindow(aUnsafeURL)
 GhostWindow.prototype = {
   merge: function(r) {
     this._nMerged = this._nMerged ? this._nMerged + 1 : 2;
   }
 };
 
 function getGhostWindowsByProcess(aMgr)
 {
-  function ignoreSingle(aUnsafePath) 
+  function ignoreSingle(aUnsafePath)
   {
     return !aUnsafePath.startsWith('ghost-windows/')
   }
 
   function ignoreMulti(aName)
   {
     return aName !== "ghost-windows";
   }
 
   let ghostWindowsByProcess = {};
 
   function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
                         aDescription)
   {
     let unsafeSplit = aUnsafePath.split('/');
-    assert(unsafeSplit[0] == 'ghost-windows',
+    assert(unsafeSplit[0] === 'ghost-windows' && unsafeSplit.length === 2,
            'Unexpected path in getGhostWindowsByProcess: ' + aUnsafePath);
+    assert(aKind === KIND_OTHER,   "bad ghost-windows kind");
+    assert(aUnits === UNITS_COUNT, "bad ghost-windows units");
+    assert(aAmount === 1,          "bad ghost-windows amount");
+    assert(aDescription === "",    "bad ghost-windows description");
 
     let unsafeURL = unsafeSplit[1];
     let ghostWindow = new GhostWindow(unsafeURL);
 
     let process = aProcess === "" ? "Main" : aProcess;
     if (!ghostWindowsByProcess[process]) {
       ghostWindowsByProcess[process] = {};
     }
@@ -1643,14 +1612,14 @@ function appendProcessAboutCompartmentsE
     let c = aCompartments[name];
     if (c._isSystemCompartment) {
       systemCompartments[name] = c;
     }
     else {
       userCompartments[name] = c;
     }
   }
-  
+
   appendProcessAboutCompartmentsElementsHelper(aP, userCompartments, "User Compartments");
   appendProcessAboutCompartmentsElementsHelper(aP, systemCompartments, "System Compartments");
   appendProcessAboutCompartmentsElementsHelper(aP, aGhostWindows, "Ghost Windows");
 }
 
--- a/toolkit/components/aboutmemory/tests/test_aboutcompartments.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutcompartments.xul
@@ -42,17 +42,16 @@
   }
 
   // Setup various fake-but-deterministic reporters.
   const KB = 1024;
   const MB = KB * KB;
   const NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
   const HEAP    = Ci.nsIMemoryReporter.KIND_HEAP;
   const OTHER   = Ci.nsIMemoryReporter.KIND_OTHER;
-  const SUMMARY = Ci.nsIMemoryReporter.KIND_SUMMARY;
 
   const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
   const COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
 
   function f(aProcess, aPath, aKind, aUnits, aAmount) {
     return {
       process:     aProcess,
       path:        aPath,
@@ -67,18 +66,18 @@
     // These should be ignored.
     f("", "explicit/a",         HEAP,    BYTES, 222 * MB),
     f("", "explicit/b/a",       HEAP,    BYTES,  85 * MB),
     f("", "explicit/b/b",       NONHEAP, BYTES,  85 * MB),
     f("", "other1",             OTHER,   BYTES, 111 * MB),
     f("", "other2",             OTHER,   COUNT, 888),
 
     f("2nd", "explicit/c",      HEAP,    BYTES, 333 * MB),
-    f("2nd", "compartments/user/child-user-compartment",     SUMMARY, COUNT, 1),
-    f("2nd", "compartments/system/child-system-compartment", SUMMARY, COUNT, 1)
+    f("2nd", "compartments/user/child-user-compartment",     OTHER, COUNT, 1),
+    f("2nd", "compartments/system/child-system-compartment", OTHER, COUNT, 1)
   ];
 
   var fakeMultiReporters = [
     // These shouldn't show up.
     { name: "fake",
       collectReports: function(aCbObj, aClosure) {
         function f(aP, aK, aU, aA) {
           aCbObj.callback("", aP, aK, aU, aA, "Desc.", aClosure);
@@ -86,17 +85,17 @@
         f("explicit/a/d",     HEAP,    BYTES,  13 * MB);
         f("explicit/b/c",     NONHEAP, BYTES,  10 * MB);
        },
        explicitNonHeap: 10*MB
     },
     { name: "compartments",
       collectReports: function(aCbObj, aClosure) {
         function f(aP) {
-          aCbObj.callback("", aP, SUMMARY, COUNT, 1, "", aClosure);
+          aCbObj.callback("", aP, OTHER, COUNT, 1, "", aClosure);
         }
         f("compartments/user/http:\\\\foo.com\\");
         f("compartments/user/https:\\\\bar.com\\bar?baz");
         f("compartments/user/https:\\\\very-long-url.com\\very-long\\oh-so-long\\really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789");
         // This moz-nullprincipal one is shown under "System Compartments" even
         // though its path indicates it's a user compartment.
         f("compartments/user/moz-nullprincipal:{7ddefdaf-34f1-473f-9b03-50a4568ccb06}");
         // This should show up once with a "[3]" suffix
@@ -105,17 +104,17 @@
         f("compartments/system/[System Principal]");
         f("compartments/system/atoms");
       },
       explicitNonHeap: 0
     },
     { name: "ghost-windows",
       collectReports: function(aCbObj, aClosure) {
         function f(aP) {
-          aCbObj.callback("", aP, SUMMARY, COUNT, 1, "", aClosure);
+          aCbObj.callback("", aP, OTHER, COUNT, 1, "", aClosure);
         }
         f("ghost-windows/https:\\\\very-long-url.com\\very-long\\oh-so-long\\really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789");
         f("ghost-windows/http:\\\\foobar.com\\foo?bar#baz");
       },
       explicitNonHeap: 0
     },
     // These shouldn't show up.
     { name: "smaps",
--- a/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
@@ -75,18 +75,28 @@
     f("", "explicit/a",         HEAP,    222 * MB),
     f("", "explicit/b/a",       HEAP,     85 * MB),
     f("", "explicit/b/b",       HEAP,     75 * MB),
     f("", "explicit/b/c/a",     HEAP,     70 * MB),
     f("", "explicit/b/c/b",     HEAP,      2 * MB), // omitted
     f("", "explicit/g/a",       HEAP,      6 * MB),
     f("", "explicit/g/b",       HEAP,      5 * MB),
     f("", "explicit/g/other",   HEAP,      4 * MB),
-    f("", "other1",             OTHER,   111 * MB),
-    f2("", "other4",            OTHER,   COUNT_CUMULATIVE, 888),
+    // A degenerate tree with the same name as a non-degenerate tree should
+    // work ok.
+    f("", "explicit",           OTHER,   888 * MB),
+    f("", "other1/a/b",         OTHER,   111 * MB),
+    f("", "other1/c/d",         OTHER,    22 * MB),
+    f("", "other1/c/e",         OTHER,    33 * MB),
+    f2("", "other4",            OTHER,   COUNT_CUMULATIVE, 777),
+    f2("", "other4",            OTHER,   COUNT_CUMULATIVE, 111),
+    f2("", "other3/a/b/c/d/e",  OTHER,   PERCENTAGE, 2000),
+    f2("", "other3/a/b/c/d/f",  OTHER,   PERCENTAGE, 10),
+    f2("", "other3/a/b/c/d/g",  OTHER,   PERCENTAGE, 5),
+    f2("", "other3/a/b/c/d/g",  OTHER,   PERCENTAGE, 5),
     // These compartments ones shouldn't be displayed.
     f2("", "compartments/user/foo",   OTHER, COUNT, 1),
     f2("", "compartments/system/foo", OTHER, COUNT, 1)
   ];
   let fakeMultiReporters = [
     { name: "fake1",
       collectReports: function(aCbObj, aClosure) {
         function f(aP, aK, aU, aA) {
@@ -117,22 +127,22 @@
       explicitNonHeap: 0
     },
     { name: "smaps",
       collectReports: function(aCbObj, aClosure) {
         // The amounts are given in pages, so multiply here by 4kb.
         function f(aP, aA) {
           aCbObj.callback("", aP, NONHEAP, BYTES, aA * 4 * KB, "Desc.", aClosure);
         }
-        f("vsize/a",     24);
+        f("size/a",      24);
         f("swap/a",       1);
         f("swap/a",       2);
-        f("vsize/a",      19);
+        f("size/a",       19);
         f("swap/b/c",     10);
-        f("resident/a",   42);
+        f("rss/a",        42);
         f("pss/a",        43);
       },
       explicitNonHeap: 0
     },
     { name: "compartments",
       collectReports: function(aCbObj, aClosure) {
         function f(aP) {
           aCbObj.callback("", aP, OTHER, COUNT, 1, "", aClosure);
@@ -177,18 +187,18 @@
                                 HEAP,    200 * MB),
     f("2nd", "other0",          OTHER,   666 * MB),
     f("2nd", "other1",          OTHER,   111 * MB),
     // Even though the "smaps" reporter is a multi-reporter, if its in a
     // child process it'll be passed to the main process as single reports.
     // The fact that we skip the "smaps" multi-reporter in the main
     // process won't cause these to be skipped;  the fall-back skipping will
     // be hit instead.
-    f("2nd", "vsize/e",         NONHEAP, 24*4*KB),
-    f("2nd", "vsize/f",         NONHEAP, 24*4*KB),
+    f("2nd", "size/e",          NONHEAP, 24*4*KB),
+    f("2nd", "size/f",          NONHEAP, 24*4*KB),
 
     // Check that we can handle "heap-allocated" not being present.
     f("3rd", "explicit/a/b",    HEAP,    333 * MB),
     f("3rd", "explicit/a/c",    HEAP,    444 * MB),
     f2("3rd", "other1",         OTHER,   BYTES, 1 * MB),
 
     // Invalid values (negative, too-big) should be identified.
     f("4th", "heap-allocated",   OTHER,   100 * MB),
@@ -251,22 +261,33 @@ 623.58 MB (100.0%) -- explicit\n\
 ├───20.00 MB (03.21%) -- f/g/h\n\
 │   ├──10.00 MB (01.60%) ── i\n\
 │   └──10.00 MB (01.60%) ── j\n\
 ├───15.00 MB (02.41%) ++ g\n\
 ├───11.00 MB (01.76%) ── heap-unclassified\n\
 └────0.58 MB (00.09%) ++ (2 tiny)\n\
 \n\
 Other Measurements\n\
+166.00 MB (100.0%) -- other1\n\
+├──111.00 MB (66.87%) ── a/b\n\
+└───55.00 MB (33.13%) -- c\n\
+    ├──33.00 MB (19.88%) ── e\n\
+    └──22.00 MB (13.25%) ── d\n\
+\n\
+20.20% (100.0%) -- other3\n\
+└──20.20% (100.0%) -- a/b/c/d\n\
+   ├──20.00% (99.01%) ── e\n\
+   └───0.20% (00.99%) ++ (2 tiny)\n\
+\n\
+888.00 MB ── explicit\n\
 500.00 MB ── heap-allocated\n\
 100.00 MB ── heap-unallocated\n\
-111.00 MB ── other1\n\
 222.00 MB ── other2\n\
       777 ── other3\n\
-      888 ── other4\n\
+      888 ── other4 [2]\n\
    45.67% ── perc1\n\
   100.00% ── perc2\n\
 \n\
 2nd Process\n\
 \n\
 Explicit Allocations\n\
 1,000.00 MB (100.0%) -- explicit\n\
 ├────499.00 MB (49.90%) ── a/b/c [3]\n\
@@ -379,48 +400,60 @@ 653,876,224 B (100.0%) -- explicit\n\
 │   ├───6,291,456 B (00.96%) ── a\n\
 │   ├───5,242,880 B (00.80%) ── b\n\
 │   └───4,194,304 B (00.64%) ── other\n\
 ├───11,534,336 B (01.76%) ── heap-unclassified\n\
 ├──────510,976 B (00.08%) ── d\n\
 └──────102,400 B (00.02%) ── e\n\
 \n\
 Resident Set Size (RSS) Breakdown\n\
-172,032 B (100.0%) ++ resident\n\
+172,032 B (100.0%) ++ rss\n\
 \n\
 Proportional Set Size (PSS) Breakdown\n\
 176,128 B (100.0%) ++ pss\n\
 \n\
 Virtual Size Breakdown\n\
-176,128 B (100.0%) ++ vsize\n\
+176,128 B (100.0%) ++ size\n\
 \n\
-Swap Usage Breakdown\n\
+Swap Breakdown\n\
 53,248 B (100.0%) ++ swap\n\
 \n\
 Other Measurements\n\
+174,063,616 B (100.0%) -- other1\n\
+├──116,391,936 B (66.87%) ── a/b\n\
+└───57,671,680 B (33.13%) -- c\n\
+    ├──34,603,008 B (19.88%) ── e\n\
+    └──23,068,672 B (13.25%) ── d\n\
+\n\
+20.20% (100.0%) -- other3\n\
+└──20.20% (100.0%) -- a/b/c/d\n\
+   ├──20.00% (99.01%) ── e\n\
+   ├───0.10% (00.50%) ── f\n\
+   └───0.10% (00.50%) ── g [2]\n\
+\n\
+931,135,488 B ── explicit\n\
 524,288,000 B ── heap-allocated\n\
 104,857,600 B ── heap-unallocated\n\
-116,391,936 B ── other1\n\
 232,783,872 B ── other2\n\
           777 ── other3\n\
-          888 ── other4\n\
+          888 ── other4 [2]\n\
        45.67% ── perc1\n\
       100.00% ── perc2\n\
 \n\
 2nd Process\n\
 \n\
 Explicit Allocations\n\
 1,048,576,000 B (100.0%) -- explicit\n\
 ├────523,239,424 B (49.90%) ── a/b/c [3]\n\
 ├────209,715,200 B (20.00%) ── flip/the/backslashes\n\
 ├────209,715,200 B (20.00%) ── compartment(compartment-url)\n\
 └────105,906,176 B (10.10%) ── heap-unclassified\n\
 \n\
 Virtual Size Breakdown\n\
-196,608 B (100.0%) ++ vsize\n\
+196,608 B (100.0%) ++ size\n\
 \n\
 Other Measurements\n\
 1,048,576,000 B ── heap-allocated\n\
   104,857,600 B ── heap-unallocated\n\
   698,351,616 B ── other0\n\
   116,391,936 B ── other1\n\
 \n\
 3rd Process\n\
--- a/toolkit/components/aboutmemory/tests/test_memoryReporters.xul
+++ b/toolkit/components/aboutmemory/tests/test_memoryReporters.xul
@@ -122,17 +122,17 @@
     // Access |name| and |explicitNonHeap| to make sure they don't crash or
     // assert.
     dummy = r.name;
     dummy = r.explicitNonHeap;
   }
 
   function checkSpecialReport(aName, aAmounts)
   {
-    ok(aAmounts.length == 1, aName + " has exactly 1 report");
+    ok(aAmounts.length == 1, aName + " has " + aAmounts.length + " report");
     let n = aAmounts[0];
     // Check the size is reasonable -- i.e. not ridiculously large or small.
     ok(n === kUnknown || (100 * 1000 <= n && n <= 10 * 1000 * 1000 * 1000),
        aName + "'s size is reasonable");
   }
 
   // If mgr.explicit failed, we won't have "heap-allocated" either.
   if (haveExplicit) {
--- a/toolkit/components/telemetry/TelemetryPing.js
+++ b/toolkit/components/telemetry/TelemetryPing.js
@@ -49,17 +49,16 @@ const MEM_HISTOGRAMS = {
   "storage-sqlite": "MEMORY_STORAGE_SQLITE",
   "images-content-used-uncompressed":
     "MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED",
   "heap-allocated": "MEMORY_HEAP_ALLOCATED",
   "heap-committed-unused": "MEMORY_HEAP_COMMITTED_UNUSED",
   "heap-committed-unused-ratio": "MEMORY_HEAP_COMMITTED_UNUSED_RATIO",
   "page-faults-hard": "PAGE_FAULTS_HARD",
   "low-memory-events-virtual": "LOW_MEMORY_EVENTS_VIRTUAL",
-  "low-memory-events-commit-space": "LOW_MEMORY_EVENTS_COMMIT_SPACE",
   "low-memory-events-physical": "LOW_MEMORY_EVENTS_PHYSICAL",
   "ghost-windows": "GHOST_WINDOWS"
 };
 
 // Seconds of idle time before pinging.
 // On idle-daily a gather-telemetry notification is fired, during it probes can
 // start asynchronous tasks to gather data.  On the next idle the data is sent.
 const IDLE_TIMEOUT_SECONDS = 5 * 60;
--- a/xpcom/base/MapsMemoryReporter.cpp
+++ b/xpcom/base/MapsMemoryReporter.cpp
@@ -86,29 +86,29 @@ void GetBasename(const nsCString &aPath,
     out.Assign(Substring(out, 0, out.RFind("(deleted)")));
   }
   out.StripChars(" ");
 
   aOut.Assign(out);
 }
 
 // MapsReporter::CollectReports uses this stuct to keep track of whether it's
-// seen a mapping under 'resident', 'pss', 'vsize', and 'swap'.
+// seen a mapping under 'rss', 'pss', 'size', and 'swap'.
 struct CategoriesSeen {
   CategoriesSeen() :
-    mSeenResident(false),
+    mSeenRss(false),
     mSeenPss(false),
-    mSeenVsize(false),
+    mSeenSize(false),
     mSeenSwap(false)
   {
   }
 
-  bool mSeenResident;
+  bool mSeenRss;
   bool mSeenPss;
-  bool mSeenVsize;
+  bool mSeenSize;
   bool mSeenSwap;
 };
 
 } // anonymous namespace
 
 class MapsReporter : public nsIMemoryMultiReporter
 {
 public:
@@ -190,24 +190,25 @@ MapsReporter::CollectReports(nsIMemoryMu
   while (true) {
     nsresult rv = ParseMapping(f, aCb, aClosure, &categoriesSeen);
     if (NS_FAILED(rv))
       break;
   }
 
   fclose(f);
 
-  // For sure we should have created some node under 'resident' and
-  // 'vsize'; otherwise we're probably not reading smaps correctly.  If we
+  // For sure we should have created some node under 'rss' and
+  // 'size'; otherwise we're probably not reading smaps correctly.  If we
   // didn't create a node under 'swap', create one here so about:memory
   // knows to create an empty 'swap' tree;  it needs a 'total' child because
   // about:memory expects at least one report whose path begins with 'swap/'.
 
-  NS_ASSERTION(categoriesSeen.mSeenVsize, "Didn't create a vsize node?");
-  NS_ASSERTION(categoriesSeen.mSeenVsize, "Didn't create a resident node?");
+  NS_ASSERTION(categoriesSeen.mSeenSize, "Didn't create a size node?");
+  NS_ASSERTION(categoriesSeen.mSeenRss, "Didn't create a rss node?");
+  NS_ASSERTION(categoriesSeen.mSeenPss, "Didn't create a pss node?");
   if (!categoriesSeen.mSeenSwap) {
     nsresult rv;
     rv = aCb->Callback(NS_LITERAL_CSTRING(""),
                        NS_LITERAL_CSTRING("swap/total"),
                        nsIMemoryReporter::KIND_NONHEAP,
                        nsIMemoryReporter::UNITS_BYTES,
                        0,
                        NS_LITERAL_CSTRING("This process uses no swap space."),
@@ -469,22 +470,22 @@ MapsReporter::ParseMapBody(
   }
 
   // Don't report nodes with size 0.
   if (size == 0)
     return NS_OK;
 
   const char* category;
   if (strcmp(desc, "Size") == 0) {
-    category = "vsize";
-    aCategoriesSeen->mSeenVsize = true;
+    category = "size";
+    aCategoriesSeen->mSeenSize = true;
   }
   else if (strcmp(desc, "Rss") == 0) {
-    category = "resident";
-    aCategoriesSeen->mSeenResident = true;
+    category = "rss";
+    aCategoriesSeen->mSeenRss = true;
   }
   else if (strcmp(desc, "Pss") == 0) {
     category = "pss";
     aCategoriesSeen->mSeenPss = true;
   }
   else if (strcmp(desc, "Swap") == 0) {
     category = "swap";
     aCategoriesSeen->mSeenSwap = true;
--- a/xpcom/base/nsIMemoryReporter.idl
+++ b/xpcom/base/nsIMemoryReporter.idl
@@ -18,126 +18,111 @@ interface nsISimpleEnumerator;
  * An nsIMemoryReporter reports a single memory measurement as an object.
  * Use this when it makes sense to gather this measurement without gathering
  * related measurements at the same time.
  *
  * Note that the |amount| field may be implemented as a function, and so
  * accessing it can trigger significant computation;  the other fields can
  * be accessed without triggering this computation.  (Compare and contrast
  * this with nsIMemoryMultiReporter.)  
+ *
+ * aboutMemory.js is the most important consumer of memory reports.  It
+ * places the following constraints on reports.
+ *
+ * - There must be an "explicit" tree.  It represents non-overlapping
+ *   regions of memory that have been explicitly allocated with an
+ *   OS-level allocation (e.g. mmap/VirtualAlloc/vm_allocate) or a
+ *   heap-level allocation (e.g. malloc/calloc/operator new).  Reporters
+ *   in this tree must have kind HEAP or NONHEAP, units BYTES, and a
+ *   description that is a sentence (i.e. starts with a capital letter and
+ *   ends with a period, or similar).
+ *
+ * - The "size", "rss", "pss" and "swap" trees are optional.  They
+ *   represent regions of virtual memory that the process has mapped.
+ *   Reporters in this category must have kind NONHEAP, units BYTES, and a
+ *   non-empty description.
+ *
+ * - The "compartments" and "ghost-windows" trees are optional.  They are
+ *   used by about:compartments.  Reporters in these trees must have kind
+ *   OTHER, units COUNT, an amount of 1, and a description that's an empty
+ *   string.
+ *
+ * - All other reports must have kind OTHER, and a description that is a
+ *   sentence.
  */
 [scriptable, uuid(b2c39f65-1799-4b92-a806-ab3cf6af3cfa)]
 interface nsIMemoryReporter : nsISupports
 {
   /*
    * The name of the process containing this reporter.  Each reporter initially
    * has "" in this field, indicating that it applies to the current process.
    * (This is true even for reporters in a child process.)  When a reporter
    * from a child process is copied into the main process, the copy has its
    * 'process' field set appropriately.
    */
   readonly attribute ACString process;
 
   /*
    * The path that this memory usage should be reported under.  Paths are
-   * '/'-delimited, eg. "a/b/c".  If you want to include a '/' not as a path
-   * separator, e.g. because the path contains a URL, you need to convert
-   * each '/' in the URL to a '\'.  Consumers of the path will undo this
-   * change.  Any other '\' character in a path will also be changed.  This
-   * is clumsy but hasn't caused any problems so far.
+   * '/'-delimited, eg. "a/b/c".  
    *
-   * There are several categories of paths.
-   *
-   * - Paths starting with "explicit/" represent regions of memory that have
-   *   been explicitly allocated with an OS-level allocation (eg.
-   *   mmap/VirtualAlloc/vm_allocate) or a heap-level allocation (eg.
-   *   malloc/calloc/operator new).
+   * Each reporter can be viewed as representing a leaf node in a tree.
+   * Internal nodes of the tree don't have reporters.  So, for example, the
+   * reporters "explicit/a/b", "explicit/a/c", "explicit/d/e", and
+   * "explicit/d/f" define this tree:
    *
-   *   Each reporter can be viewed as representing a leaf node in a tree
-   *   rooted at "explicit".  Internal nodes of the tree don't have
-   *   reporters.  So, for example, the reporters "explicit/a/b",
-   *   "explicit/a/c", "explicit/d/e", and "explicit/d/f" define this tree:
-   *
-   *     explicit
-   *     |--a
-   *     |  |--b [*]
-   *     |  \--c [*]
-   *     \--d
-   *        |--e [*]
-   *        \--f [*]
+   *   explicit
+   *   |--a
+   *   |  |--b [*]
+   *   |  \--c [*]
+   *   \--d
+   *      |--e [*]
+   *      \--f [*]
    *
-   *   Nodes marked with a [*] have a reporter.  Notice that the internal
-   *   nodes are implicitly defined by the paths.
+   * Nodes marked with a [*] have a reporter.  Notice that the internal
+   * nodes are implicitly defined by the paths.
    *
-   *   A node's children divide their parent's memory into disjoint pieces.
-   *   So in the example above, |a| may not count any allocations counted by
-   *   |d|, and vice versa.
-   *
-   *   Reporters in this category must have kind HEAP or NONHEAP, units BYTES,
-   *   and a description that is a sentence (i.e. starts with a capital letter
-   *   and ends with a period, or similar).
+   * Nodes within a tree should not overlap measurements, otherwise the
+   * parent node measurements will be double-counted.  So in the example
+   * above, |b| should not count any allocations counted by |c|, and vice
+   * versa.
    *
-   * - Paths starting with "smaps/" represent regions of virtual memory that the
-   *   process has mapped.  The rest of the path describes the type of
-   *   measurement; for instance, the reporter "smaps/rss/[stack]" might report
-   *   how much of the process's stack is currently in physical memory.
-   *
-   *   Reporters in this category must have kind NONHEAP, units BYTES, and
-   *   a non-empty description.
+   * All nodes within each tree must have the same units.
    *
-   * - Reporters with kind SUMMARY may have any path which doesn't start with
-   *   "explicit/" or "smaps/".
+   * If you want to include a '/' not as a path separator, e.g. because the
+   * path contains a URL, you need to convert each '/' in the URL to a '\'.
+   * Consumers of the path will undo this change.  Any other '\' character
+   * in a path will also be changed.  This is clumsy but hasn't caused any
+   * problems so far.
    *
-   * - All other paths represent cross-cutting values and may overlap with any
-   *   other reporter.  Reporters in this category must have paths that do not
-   *   contain '/' separators, kind OTHER, and a description that is a
-   *   sentence.
+   * The paths of all reporters form a set of trees.  Trees can be
+   * "degenerate", i.e. contain a single entry with no '/'.
    */
   readonly attribute AUTF8String path;
 
   /*
-   * There are three categories of memory reporters:
+   * There are three kinds of memory reporters.
    *
-   *  - HEAP: memory allocated by the heap allocator, e.g. by calling malloc,
-   *    calloc, realloc, memalign, operator new, or operator new[].  Reporters
-   *    in this category must have units UNITS_BYTES and must have a path
-   *    starting with "explicit/".
-   *
-   *  - NONHEAP: memory which the program explicitly allocated, but does not
-   *    live on the heap.  Such memory is commonly allocated by calling one of
-   *    the OS's memory-mapping functions (e.g. mmap, VirtualAlloc, or
-   *    vm_allocate).  Reporters in this category must have units UNITS_BYTES
-   *    and must have a path starting with "explicit/" or "smaps/".
+   *  - HEAP: reporters measuring memory allocated by the heap allocator,
+   *    e.g. by calling malloc, calloc, realloc, memalign, operator new, or
+   *    operator new[].  Reporters in this category must have units
+   *    UNITS_BYTES.
    *
-   *  - OTHER: reporters which don't fit into either of these categories. Such
-   *    reporters must have a path that does not start with "explicit/" or
-   *    "smaps/" and may have any units.
+   *  - NONHEAP: reporters measuring memory which the program explicitly
+   *    allocated, but does not live on the heap.  Such memory is commonly
+   *    allocated by calling one of the OS's memory-mapping functions (e.g.
+   *    mmap, VirtualAlloc, or vm_allocate).  Reporters in this category
+   *    must have units UNITS_BYTES.
    *
-   *  - SUMMARY: reporters which report data that's available in a more
-   *    detailed form via other reporters.  These reporters are sometimes
-   *    useful for efficiency purposes -- for example, a KIND_SUMMARY reporter
-   *    might list all the JS compartments without the overhead of the full JS
-   *    memory reporter, which walks the JS heap.
-   *
-   *    Unlike other reporters, SUMMARY reporters may have empty descriptions.
-   *
-   *    SUMMARY reporters must not have a path starting with "explicit/" or
-   *    "smaps/".
+   *  - OTHER: reporters which don't fit into either of these categories.
+   *    They can have any units.
    */
   const PRInt32 KIND_NONHEAP = 0;
   const PRInt32 KIND_HEAP    = 1;
   const PRInt32 KIND_OTHER   = 2;
-  const PRInt32 KIND_SUMMARY = 3;
-
-  /*
-   * KIND_MAPPED is a deprecated synonym for KIND_NONHEAP.  We keep it around
-   * to as not to break extensions which might use this interface, but we will
-   * remove it eventually.
-   */
-  const PRInt32 KIND_MAPPED  = 0;
 
   /*
    * The reporter kind.  See KIND_* above.
    */
   readonly attribute PRInt32 kind;
 
   /*
    * The amount reported by a memory reporter must have one of the following