Bug 702300 (part 8) - Add about:compartments. r=jlebar, ehsan.
authorNicholas Nethercote <nnethercote@mozilla.com>
Thu, 16 Feb 2012 22:10:39 -0800
changeset 87430 17927137a6993f35d7d97a19997112214d191ed2
parent 87429 4edde0ad19d888dcc68420bf6531ca8a8fc7ed12
child 87431 ec21e7e02464f60cabd31bdedb126fd9962dd4a1
push id132
push userMs2ger@gmail.com
push dateTue, 21 Feb 2012 22:54:28 +0000
reviewersjlebar, ehsan
bugs702300
milestone13.0a1
Bug 702300 (part 8) - Add about:compartments. r=jlebar, ehsan.
docshell/base/nsAboutRedirector.cpp
docshell/build/nsDocShellModule.cpp
js/xpconnect/src/XPCJSRuntime.cpp
toolkit/components/aboutmemory/content/aboutMemory.css
toolkit/components/aboutmemory/content/aboutMemory.js
toolkit/components/aboutmemory/content/aboutMemory.xhtml
toolkit/components/aboutmemory/tests/Makefile.in
toolkit/components/aboutmemory/tests/test_aboutcompartments.xul
toolkit/components/aboutmemory/tests/test_aboutmemory.xul
xpcom/base/nsIMemoryReporter.idl
--- a/docshell/base/nsAboutRedirector.cpp
+++ b/docshell/base/nsAboutRedirector.cpp
@@ -81,16 +81,19 @@ static RedirEntry kRedirMap[] = {
     { "buildconfig", "chrome://global/content/buildconfig.html",
       nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT },
     { "license", "chrome://global/content/license.html",
       nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT },
     { "neterror", "chrome://global/content/netError.xhtml",
       nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
       nsIAboutModule::ALLOW_SCRIPT |
       nsIAboutModule::HIDE_FROM_ABOUTABOUT },
+    // aboutMemory.xhtml implements about:compartments
+    { "compartments", "chrome://global/content/aboutMemory.xhtml",
+      nsIAboutModule::ALLOW_SCRIPT },
     { "memory", "chrome://global/content/aboutMemory.xhtml",
       nsIAboutModule::ALLOW_SCRIPT },
     { "addons", "chrome://mozapps/content/extensions/extensions.xul",
       nsIAboutModule::ALLOW_SCRIPT },
     { "newaddon", "chrome://mozapps/content/extensions/newaddon.xul",
       nsIAboutModule::ALLOW_SCRIPT |
       nsIAboutModule::HIDE_FROM_ABOUTABOUT },
     { "support", "chrome://global/content/aboutSupport.xhtml",
--- a/docshell/build/nsDocShellModule.cpp
+++ b/docshell/build/nsDocShellModule.cpp
@@ -205,16 +205,17 @@ const mozilla::Module::ContractIDEntry k
 #endif
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "credits", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "plugins", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "mozilla", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "logo", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "buildconfig", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "license", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "neterror", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
+  { NS_ABOUT_MODULE_CONTRACTID_PREFIX "compartments", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "memory", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "addons", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "newaddon", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "support", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_URI_LOADER_CONTRACTID, &kNS_URI_LOADER_CID },
   { NS_DOCUMENTLOADER_SERVICE_CONTRACTID, &kNS_DOCUMENTLOADER_SERVICE_CID },
   { NS_EXTERNALHELPERAPPSERVICE_CONTRACTID, &kNS_EXTERNALHELPERAPPSERVICE_CID },
   { NS_EXTERNALPROTOCOLSERVICE_CONTRACTID, &kNS_EXTERNALHELPERAPPSERVICE_CID },
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -2079,16 +2079,17 @@ XPCJSRuntime::XPCJSRuntime(nsXPConnect* 
             NS_RUNTIMEABORT("PR_NewCondVar failed.");
 
         js::SetActivityCallback(mJSRuntime, ActivityCallback, this);
 
         NS_RegisterMemoryReporter(new NS_MEMORY_REPORTER_NAME(XPConnectJSGCHeap));
         NS_RegisterMemoryReporter(new NS_MEMORY_REPORTER_NAME(XPConnectJSSystemCompartmentCount));
         NS_RegisterMemoryReporter(new NS_MEMORY_REPORTER_NAME(XPConnectJSUserCompartmentCount));
         NS_RegisterMemoryMultiReporter(new JSMemoryMultiReporter);
+        NS_RegisterMemoryMultiReporter(new JSCompartmentsMultiReporter);
     }
 
     if (!JS_DHashTableInit(&mJSHolders, JS_DHashGetStubOps(), nsnull,
                            sizeof(ObjectHolder), 512))
         mJSHolders.ops = nsnull;
 
     mCompartmentMap.Init();
 
--- a/toolkit/components/aboutmemory/content/aboutMemory.css
+++ b/toolkit/components/aboutmemory/content/aboutMemory.css
@@ -30,27 +30,29 @@
  * 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. */
+
 body.verbose {
   /* override setting in about.css */
   max-width: 100% !important;
 }
 
-body.non-verbose pre.tree {
+body.non-verbose pre.entries {
   overflow-x: hidden;
   text-overflow: ellipsis;
 }
 
-.sectionHeader {
+h2 {
   background: #ddd;
   padding-left: .1em;
 }
 
 .accuracyWarning {
   color: #f00;
 }
 
--- a/toolkit/components/aboutmemory/content/aboutMemory.js
+++ b/toolkit/components/aboutmemory/content/aboutMemory.js
@@ -31,110 +31,203 @@
  * 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;
 
-let gAddedObserver = false;
-
 const KIND_NONHEAP           = Ci.nsIMemoryReporter.KIND_NONHEAP;
 const KIND_HEAP              = Ci.nsIMemoryReporter.KIND_HEAP;
 const KIND_OTHER             = Ci.nsIMemoryReporter.KIND_OTHER;
 const UNITS_BYTES            = Ci.nsIMemoryReporter.UNITS_BYTES;
 const UNITS_COUNT            = Ci.nsIMemoryReporter.UNITS_COUNT;
 const 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.
+const gVerbose = location.href === "about:memory?verbose" ||
+                 location.href === "about:compartments?verbose";
+
+let gChildMemoryListener = undefined;
+
+//---------------------------------------------------------------------------
+
 // Forward slashes in URLs in paths are represented with backslashes to avoid
 // being mistaken for path separators.  Paths/names/descriptions where this
 // hasn't been undone are prefixed with "unsafe"; the rest are prefixed with
 // "safe".
-function makeSafe(aUnsafeStr)
+function flipBackslashes(aUnsafeStr)
 {
   return aUnsafeStr.replace(/\\/g, '/');
 }
 
 function assert(aCond, aMsg)
 {
   if (!aCond) {
     throw("assertion failed: " + aMsg);
   }
 }
 
 function debug(x)
 {
-  let content = document.getElementById("content");
-  appendElementWithText(content, "div", "legend", JSON.stringify(x));
+  appendElementWithText(document.body, "div", "legend", JSON.stringify(x));
 }
 
 //---------------------------------------------------------------------------
 
-function onLoad()
+function addChildObserversAndUpdate(aUpdateFn)
 {
   let os = Cc["@mozilla.org/observer-service;1"].
       getService(Ci.nsIObserverService);
   os.notifyObservers(null, "child-memory-reporter-request", null);
 
-  os.addObserver(ChildMemoryListener, "child-memory-reporter-update", false);
-  gAddedObserver = true;
+  gChildMemoryListener = aUpdateFn;
+  os.addObserver(gChildMemoryListener, "child-memory-reporter-update", false);
+ 
+  gChildMemoryListener();
+}
 
-  update();
+function onLoad()
+{
+  if (location.href.indexOf("about:memory") === 0) {
+    document.title = "about:memory";
+    onLoadAboutMemory();
+  } else if (location.href.indexOf("about:compartment") === 0) {
+    document.title = "about:compartments";
+    onLoadAboutCompartments();
+  } else {
+    assert(false, "Unknown location");
+  }
 }
 
 function onUnload()
 {
   // We need to check if the observer has been added before removing; in some
   // circumstances (eg. reloading the page quickly) it might not have because
-  // onLoad might not fire.
-  if (gAddedObserver) {
+  // onLoadAbout{Memory,Compartments} might not fire.
+  if (gChildMemoryListener) {
     let os = Cc["@mozilla.org/observer-service;1"].
         getService(Ci.nsIObserverService);
-    os.removeObserver(ChildMemoryListener, "child-memory-reporter-update");
+    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 sendHeapMinNotifications()
+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 (++j < 3)
+    if (++i < 3)
       runSoon(sendHeapMinNotificationsInner);
     else
-      runSoon(update);
+      runSoon(fAfter);
   }
 
-  let j = 0;
   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.
+
+  let e = aMgr.enumerateReporters();
+  while (e.hasMoreElements()) {
+    let rOrig = e.getNext().QueryInterface(Ci.nsIMemoryReporter);
+    let unsafePath = rOrig.path;
+    try {
+      if (!aIgnoreSingle(unsafePath)) {
+        aHandleReport(rOrig.process, unsafePath, rOrig.kind, rOrig.units,
+                      rOrig.amount, rOrig.description);
+      }
+    }
+    catch (e) {
+      debug("Bad memory 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(aHandleReport, null);
+      }
+    }
+    catch (e) {
+      debug("Bad memory multi-reporter " + name + ": " + e);
+    }
+  }
+}
+
+//---------------------------------------------------------------------------
+
+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;
 }
 
@@ -151,18 +244,18 @@ function appendElement(aP, aTagName, aCl
 function appendElementWithText(aP, aTagName, aClassName, aText)
 {
   let e = appendElement(aP, aTagName, aClassName);
   appendTextNode(e, aText);
   return e;
 }
 
 //---------------------------------------------------------------------------
-
-const gVerbose = location.href === "about:memory?verbose";
+// Code specific to about:memory
+//---------------------------------------------------------------------------
 
 const kUnknown = -1;    // used for an unknown _amount
 
 const kTreeUnsafeDescriptions = {
   '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 \
@@ -220,120 +313,119 @@ const kTreeNames = {
   'other':    'Other Measurements'
 };
 
 const kMapTreePaths =
   ['smaps/resident', 'smaps/pss', 'smaps/vsize', 'smaps/swap'];
 
 //---------------------------------------------------------------------------
 
-function ChildMemoryListener(aSubject, aTopic, aData)
+function onLoadAboutMemory()
 {
-  update();
+  addChildObserversAndUpdate(updateAboutMemory);
 }
 
 function doGlobalGC()
 {
   Cu.forceGC();
   let os = Cc["@mozilla.org/observer-service;1"]
             .getService(Ci.nsIObserverService);
   os.notifyObservers(null, "child-gc-request", null);
-  update();
+  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);
-  update();
+  updateAboutMemory();
 }
 
 //---------------------------------------------------------------------------
 
 /**
  * Top-level function that does the work of generating the page.
  */
-function update()
+function updateAboutMemory()
 {
-  // First, clear the page contents.  Necessary because update() might be
-  // called more than once due to ChildMemoryListener.
-  let oldContent = document.getElementById("content");
-  let content = oldContent.cloneNode(false);
-  oldContent.parentNode.replaceChild(content, oldContent);
-  content.classList.add(gVerbose ? 'verbose' : 'non-verbose');
+  // 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;
-  appendProcessElements(content, "Main", reportsByProcess["Main"],
-                        hasMozMallocUsableSize);
+  appendProcessReportsElements(body, "Main", reportsByProcess["Main"],
+                               hasMozMallocUsableSize);
   for (let process in reportsByProcess) {
     if (process !== "Main") {
-      appendProcessElements(content, process, reportsByProcess[process],
-                            hasMozMallocUsableSize);
+      appendProcessReportsElements(body, process, reportsByProcess[process],
+                                   hasMozMallocUsableSize);
     }
   }
 
-  appendElement(content, "hr");
+  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(content, "button", "", aText);
+    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, update,                   "Update", "updateButton");
-  appendButton(GCDesc, doGlobalGC,               "GC");
-  appendButton(CCDesc, doCC,                     "CC");
-  appendButton(MPDesc, sendHeapMinNotifications, "Minimize memory usage");
+  appendButton(UpDesc, updateAboutMemory, "Update", "updateButton");
+  appendButton(GCDesc, doGlobalGC,        "GC");
+  appendButton(CCDesc, doCC,              "CC");
+  appendButton(MPDesc, function() { minimizeMemoryUsage3x(updateAboutMemory); },
+                                          "Minimize memory usage");
 
-  let div1 = appendElement(content, "div");
+  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(content, "div");
+  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(content, "div", "legend", legendText1);
-  appendElementWithText(content, "div", "legend", legendText2);
+  appendElementWithText(body, "div", "legend", legendText1);
+  appendElementWithText(body, "div", "legend", legendText2);
 }
 
 //---------------------------------------------------------------------------
 
 function Report(aUnsafePath, aKind, aUnits, aAmount, aUnsafeDesc)
 {
   this._unsafePath  = aUnsafePath;
   this._kind        = aKind;
@@ -363,26 +455,33 @@ Report.prototype = {
     // called "explicit" which is not part of the "explicit" tree.
     return this._unsafePath.indexOf(aTreeName) === 0 &&
            this._unsafePath.charAt(aTreeName.length) === '/';
   }
 };
 
 function getReportsByProcess(aMgr)
 {
-  // Process each memory reporter:
-  // - Put a copy of its report(s) into a sub-table indexed by its process.
-  //   Each copy is a Report object.  After this point we never use the
-  //   original memory reporter again.
-  //
-  // - Note that copying rOrig.amount (which calls a C++ function under the
-  //   IDL covers) to r._amount for every report 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.
+  // 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.indexOf("smaps/") === 0 && !gVerbose) ||
+           (aPath.indexOf("compartments/") === 0)
+  }
+
+  function ignoreMulti(aName)
+  {
+    return ((aName === "smaps" && !gVerbose) ||
+            (aName === "compartments"));
+  }
+
   let reportsByProcess = {};
 
   function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
                         aUnsafeDesc)
   {
     let process = aProcess === "" ? "Main" : aProcess;
     let r = new Report(aUnsafePath, aKind, aUnits, aAmount, aUnsafeDesc);
     if (!reportsByProcess[process]) {
@@ -394,44 +493,17 @@ function getReportsByProcess(aMgr)
       // Already an entry;  must be a duplicated report.  This can happen
       // legitimately.  Merge them.
       rOld.merge(r);
     } else {
       reports[r._unsafePath] = r;
     }
   }
 
-  // Process vanilla reporters first, then multi-reporters.
-  let e = aMgr.enumerateReporters();
-  while (e.hasMoreElements()) {
-    let rOrig = e.getNext().QueryInterface(Ci.nsIMemoryReporter);
-    try {
-      handleReport(rOrig.process, rOrig.path, rOrig.kind, rOrig.units,
-                   rOrig.amount, rOrig.description);
-    }
-    catch(e) {
-      debug("An error occurred when collecting results from the memory reporter " +
-            rOrig.path + ": " + e);
-    }
-  }
-  let e = aMgr.enumerateMultiReporters();
-  while (e.hasMoreElements()) {
-    let mrOrig = e.getNext().QueryInterface(Ci.nsIMemoryMultiReporter);
-    // Ignore the "smaps" reports in non-verbose mode.
-    if (!gVerbose && mrOrig.name === "smaps") {
-      continue;
-    }
-
-    try {
-      mrOrig.collectReports(handleReport, null);
-    }
-    catch(e) {
-      debug("An error occurred when collecting a multi-reporter's results: " + e);
-    }
-  }
+  processMemoryReporters(aMgr, ignoreSingle, ignoreMulti, handleReport);
 
   return reportsByProcess;
 }
 
 //---------------------------------------------------------------------------
 
 // There are two kinds of TreeNode.
 // - Leaf TreeNodes correspond to Reports and have more properties.
@@ -776,42 +848,43 @@ function appendWarningElements(aP, aHasK
 
     let ul = appendElement(div, "ul");
     for (let i = 0;
          i < gUnsafePathsWithInvalidValuesForThisProcess.length;
          i++)
     {
       appendTextNode(ul, " ");
       appendElementWithText(ul, "li", "", 
-        makeSafe(gUnsafePathsWithInvalidValuesForThisProcess[i]));
+        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.
+ * 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 appendProcessElements(aP, aProcess, aReports, aHasMozMallocUsableSize)
+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');
@@ -1012,18 +1085,18 @@ function appendMrNameSpan(aP, aKind, aKi
   } else if (aKidsState === kShowKids) {
     appendElementWithText(aP, "span", "mrSep hidden", " ++ ");
     appendElementWithText(aP, "span", "mrSep",        " -- ");
   } else {
     assert(false, "bad aKidsState");
   }
 
   let nameSpan = appendElementWithText(aP, "span", "mrName",
-                                       makeSafe(aUnsafeName));
-  nameSpan.title = kindToString(aKind) + makeSafe(aUnsafeDesc);
+                                       flipBackslashes(aUnsafeName));
+  nameSpan.title = kindToString(aKind) + flipBackslashes(aUnsafeDesc);
 
   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", " [?!]");
@@ -1097,20 +1170,20 @@ function expandPathToThisElement(aElemen
   } 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.tree
+    expandPathToThisElement(aElement.parentNode);       // kids or pre.entries
 
   } else {
-    assertClassListContains(aElement, "tree");
+    assertClassListContains(aElement, "entries");
   }
 }
 
 /**
  * Appends the elements for the tree, including its heading.
  *
  * @param aPOuter
  *        The parent DOM node.
@@ -1192,17 +1265,17 @@ function appendTreeElements(aPOuter, aT,
     // 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 = makeSafe(aProcess + ":" + unsafePath);
+      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;
@@ -1210,18 +1283,18 @@ function appendTreeElements(aPOuter, aT,
       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 'map/vsize', since the
-    // whole tree is non-heap.
+    // 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._unsafeDescription, 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) {
@@ -1254,17 +1327,17 @@ function appendTreeElements(aPOuter, aT,
                             baseIndentText, tString.length);
         aIndentGuide.pop();
       }
     }
   }
 
   appendSectionHeader(aPOuter, kTreeNames[aT._unsafeName]);
  
-  let pre = appendElement(aPOuter, "pre", "tree");
+  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, aUnsafeDesc, aNMerged)
 {
@@ -1323,28 +1396,28 @@ OtherReport.compare = function(a, b) {
  * @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", "tree");
+  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._kind === KIND_OTHER,
-             "_kind !== KIND_OTHER for " + makeSafe(r._unsafePath));
+             "_kind !== KIND_OTHER for " + flipBackslashes(r._unsafePath));
       assert(r._nMerged === undefined);  // we don't allow dup'd OTHER Reports
       let o = new OtherReport(r._unsafePath, r._units, r._amount,
                               r._unsafeDescription);
       otherReports.push(o);
       if (o._asString.length > maxStringLength) {
         maxStringLength = o._asString.length;
       }
     }
@@ -1365,12 +1438,202 @@ function appendOtherElements(aP, aReport
     appendTextNode(pre, "\n");
   }
 
   appendTextNode(aP, "\n");  // gives nice spacing when we cut and paste
 }
 
 function appendSectionHeader(aP, aText)
 {
-  appendElementWithText(aP, "h2", "sectionHeader", aText);
+  appendElementWithText(aP, "h2", "", aText);
   appendTextNode(aP, "\n");
 }
 
+//-----------------------------------------------------------------------------
+// Code specific to about:compartments
+//-----------------------------------------------------------------------------
+
+function onLoadAboutCompartments()
+{
+  // Minimize memory usage before generating the page in an attempt to collect
+  // any dead compartments.
+  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";
+  }
+
+  // Dispatch a "bodygenerated" event to indicate that the DOM has finished
+  // generating.  This is used by tests.
+  let e = document.createEvent("Event");
+  e.initEvent("bodygenerated", false, false);
+  document.dispatchEvent(e);
+}
+
+//---------------------------------------------------------------------------
+
+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.indexOf("compartments/") !== 0;
+  }
+
+  function ignoreMulti(aName)
+  {
+    return aName !== "compartments";
+  }
+
+  let compartmentsByProcess = {};
+
+  function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount, aDesc)
+  {
+    let process = aProcess === "" ? "Main" : aProcess;
+
+    assert(aKind   === KIND_OTHER, "bad kind");
+    assert(aUnits  === UNITS_COUNT, "bad units");
+    assert(aAmount === 1, "bad amount");
+    assert(aDesc   === "", "bad description");
+
+    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].indexOf("moz-nullprincipal:{") === 0) {
+        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 (var 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");
+}
+
--- a/toolkit/components/aboutmemory/content/aboutMemory.xhtml
+++ b/toolkit/components/aboutmemory/content/aboutMemory.xhtml
@@ -32,18 +32,20 @@
    - 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 LGPL or the GPL. 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. -->
+
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
-    <title>about:memory</title>
+    <!-- the <title> is filled in by aboutMemory.js -->
     <link rel="stylesheet" href="chrome://global/skin/aboutMemory.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"/>
     <script type="text/javascript;version=1.8" src="chrome://global/content/aboutMemory.js"/>
   </head>
 
-  <body id="content" onload="onLoad()" onunload="onUnload()"></body>
+  <body onload="onLoad()" onunload="onUnload()"></body>
 </html>
--- a/toolkit/components/aboutmemory/tests/Makefile.in
+++ b/toolkit/components/aboutmemory/tests/Makefile.in
@@ -40,16 +40,17 @@ topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir  = toolkit/components/aboutmemory/tests
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _CHROME_FILES	= \
+		test_aboutcompartments.xul \
 		test_aboutmemory.xul \
 		test_aboutmemory2.xul \
 		test_sqliteMultiReporter.xul \
 		$(NULL)
 
 libs:: $(_CHROME_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutcompartments.xul
@@ -0,0 +1,230 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:compartments"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+  <!-- test code goes here -->
+  <script type="application/javascript;version=1.8">
+  <![CDATA[
+  const Cc = Components.classes;
+  const Ci = Components.interfaces;
+  let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+            getService(Ci.nsIMemoryReporterManager);
+
+  // Remove all the real reporters and multi-reporters;  save them to
+  // restore at the end.
+  var e = mgr.enumerateReporters();
+  var realReporters = [];
+  while (e.hasMoreElements()) {
+    var r = e.getNext().QueryInterface(Ci.nsIMemoryReporter);
+    mgr.unregisterReporter(r);
+    realReporters.push(r);
+  }
+  e = mgr.enumerateMultiReporters();
+  var realMultiReporters = [];
+  while (e.hasMoreElements()) {
+    var r = e.getNext().QueryInterface(Ci.nsIMemoryMultiReporter);
+    mgr.unregisterMultiReporter(r);
+    realMultiReporters.push(r);
+  }
+
+  // Setup various fake-but-deterministic reporters.
+  const KB = 1024;
+  const MB = KB * KB;
+  const NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
+  const HEAP    = Ci.nsIMemoryReporter.KIND_HEAP;
+  const OTHER   = Ci.nsIMemoryReporter.KIND_OTHER;
+
+  const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+  const COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
+
+  function f(aProcess, aPath, aKind, aUnits, aAmount) {
+    return {
+      process:     aProcess,
+      path:        aPath,
+      kind:        aKind,
+      units:       aUnits,
+      description: "",
+      amount:      aAmount 
+    };
+  }
+
+  var fakeReporters = [
+    // 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",     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);
+        }
+        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, 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
+        f("compartments/system/[System Principal]");
+        f("compartments/system/[System Principal]");
+        f("compartments/system/[System Principal]");
+        f("compartments/system/atoms");
+      },
+      explicitNonHeap: 0
+    },
+    // These shouldn't show up.
+    { 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("smaps/vsize/a",     24);
+        f("smaps/swap/a",       1);
+      },
+      explicitNonHeap: 0
+    }
+  ];
+
+  for (var i = 0; i < fakeReporters.length; i++) {
+    mgr.registerReporter(fakeReporters[i]);
+  }
+  for (var i = 0; i < fakeMultiReporters.length; i++) {
+    mgr.registerMultiReporter(fakeMultiReporters[i]);
+  }
+  ]]>
+  </script>
+
+  <iframe id="acFrame"  height="400" src="about:compartments"></iframe>
+  <iframe id="acvFrame" height="400" src="about:compartments?verbose"></iframe>
+
+  <script type="application/javascript">
+  <![CDATA[
+  var acExpectedText =
+"\
+Main Process\n\
+\n\
+User Compartments\n\
+http://foo.com/\n\
+https://bar.com/bar?baz\n\
+https://very-long-url.com/very-long/oh-so-long/really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789\n\
+\n\
+System Compartments\n\
+[System Principal] [3]\n\
+atoms\n\
+moz-nullprincipal:{7ddefdaf-34f1-473f-9b03-50a4568ccb06}\n\
+\n\
+2nd Process\n\
+\n\
+User Compartments\n\
+child-user-compartment\n\
+\n\
+System Compartments\n\
+child-system-compartment\n\
+\n\
+";
+
+  // Verbose mode output is the same when you cut and paste.
+  var acvExpectedText = acExpectedText;
+
+  function finish()
+  {
+    // Unregister fake reporters and multi-reporters, re-register the real
+    // reporters and multi-reporters, just in case subsequent tests rely on
+    // them.
+    for (var i = 0; i < fakeReporters.length; i++) {
+      mgr.unregisterReporter(fakeReporters[i]);
+    }
+    for (var i = 0; i < fakeMultiReporters.length; i++) {
+      mgr.unregisterMultiReporter(fakeMultiReporters[i]);
+    }
+    for (var i = 0; i < realReporters.length; i++) {
+      mgr.registerReporter(realReporters[i]);
+    }
+    for (var i = 0; i < realMultiReporters.length; i++) {
+      mgr.registerMultiReporter(realMultiReporters[i]);
+    }
+    SimpleTest.finish();
+  }
+
+  // Cut+paste the entire page and check that the cut text matches what we
+  // expect.  This tests the output in general and also that the cutting and
+  // pasting works as expected.
+  function test(aFrameId, aExpected, aNext) {
+    let frame = document.getElementById(aFrameId);
+    let handler = function() {
+      frame.focus();
+      SimpleTest.waitForClipboard(
+        function(aActual) {
+          mostRecentActual = aActual;
+          return aActual === aExpected;
+        },
+        function() {
+          synthesizeKey("A", {accelKey: true});
+          synthesizeKey("C", {accelKey: true});
+        },
+        aNext,
+        function() {
+          ok(false, "pasted text doesn't match for " + aFrameId);
+          dump("******EXPECTED******\n");
+          dump(aExpected);
+          dump("*******ACTUAL*******\n");
+          dump(mostRecentActual);
+          dump("********************\n");
+          finish();
+        }
+      );
+    };
+    // about:compartment dispatches a "bodygenerated" event to the document
+    // when it's finished generating its DOM.  If we don't wait for that we end
+    // up copying an empty page.
+    frame.contentDocument.addEventListener("bodygenerated", handler, false);
+  }
+
+  SimpleTest.waitForFocus(function() {
+    test(
+      "acFrame",
+      acExpectedText,
+      function() {
+        test(
+          "acvFrame",
+          acvExpectedText,
+          function() {
+            finish()
+          }
+        )
+      }
+    );
+  });
+
+  SimpleTest.waitForExplicitFinish();
+  ]]>
+  </script>
+</window>
--- a/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
@@ -84,17 +84,20 @@
     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)
+    f2("", "other4",            OTHER,   COUNT_CUMULATIVE, 888),
+    // These compartments ones shouldn't be displayed.
+    f("", "compartments/user/foo",   OTHER, COUNT, 1),
+    f("", "compartments/system/foo", OTHER, COUNT, 1)
   ];
   let fakeMultiReporters = [
     { name: "fake1",
       collectReports: function(aCbObj, aClosure) {
         function f(aP, aK, aU, aA) {
           aCbObj.callback("", aP, aK, aU, aA, "(desc)", aClosure);
         }
         f("explicit/c/d",     NONHEAP, BYTES,  13 * MB),
@@ -130,16 +133,26 @@
         f("smaps/swap/a",       1);
         f("smaps/swap/a",       2);
         f("smaps/vsize/a",      19);
         f("smaps/swap/b/c",     10);
         f("smaps/resident/a",   42);
         f("smaps/pss/a",        43);
       },
       explicitNonHeap: 0
+    },
+    { name: "compartments",
+      collectReports: function(aCbObj, aClosure) {
+        function f(aP) {
+          aCbObj.callback("", aP, OTHER, COUNT, 1, "", aClosure);
+        }
+        f("compartments/user/bar");
+        f("compartments/system/bar");
+      },
+      explicitNonHeap: 0
     }
   ];
   for (let i = 0; i < fakeReporters.length; i++) {
     mgr.registerReporter(fakeReporters[i]);
   }
   for (let i = 0; i < fakeMultiReporters.length; i++) {
     mgr.registerMultiReporter(fakeMultiReporters[i]);
   }
@@ -160,16 +173,23 @@
     f("2nd", "explicit/flip\\the\\backslashes",
                                 HEAP,    200 * MB),
     f("2nd", "explicit/compartment(compartment-url)",
                                 HEAP,    200 * MB),
     // The escaping of compartment names must prevent this script from running.
     f("2nd", "danger<script>window.alert(1)</script>",
                                 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", "smaps/vsize/e",   NONHEAP, 24*4*KB),
+    f("2nd", "smaps/vsize/f",   NONHEAP, 24*4*KB),
 
     // kUnknown should be handled gracefully for "heap-allocated", non-leaf
     // reporters, leaf-reporters, "other" reporters, and duplicated reporters.
     f("3rd", "heap-allocated",  OTHER,   kUnknown),
     f("3rd", "explicit/a/b",    HEAP,    333 * MB),
     f("3rd", "explicit/a/c",    HEAP,    444 * MB),
     f("3rd", "explicit/a/c",    HEAP,    kUnknown), // dup: merge
     f("3rd", "explicit/a/d",    HEAP,    kUnknown),
@@ -206,40 +226,20 @@
     f("5th", "explicit/a/pos",   HEAP,     40 * KB),
     f("5th", "explicit/a/neg1",  NONHEAP, -20 * KB),
     f("5th", "explicit/a/neg2",  NONHEAP, -10 * KB),
     f("5th", "explicit/b/c/d/e", NONHEAP,  20 * KB),
     f("5th", "explicit/b/c/d/f", NONHEAP, -60 * KB),
     f("5th", "explicit/b/c/g/h", NONHEAP,  10 * KB),
     f("5th", "explicit/b/c/i/j", NONHEAP,   5 * KB)
   ];
-  let fakeMultiReporters2 = [
-    // Because this multi-reporter is in a child process, the fact that we
-    // skip the "smaps" multi-reporter in the parent process won't cause
-    // these to be skipped;  the fall-back skipping will be hit instead.
-    { name: "smaps",
-      collectReports: function(aCbObj, aClosure) {
-        // The amounts are given in pages, so multiply here by 4kb.
-        function f(aP, aA) {
-          aCbObj.callback("2nd", aP, NONHEAP, BYTES, aA * 4 * KB, "(desc)", aClosure);
-        }
-        f("smaps/vsize/a", 24);
-        f("smaps/vsize/b", 24);
-      },
-      explicitNonHeap: 0
-    }
-  ];
   for (let i = 0; i < fakeReporters2.length; i++) {
     mgr.registerReporter(fakeReporters2[i]);
   }
-  for (let i = 0; i < fakeMultiReporters2.length; i++) {
-    mgr.registerMultiReporter(fakeMultiReporters2[i]);
-  }
   fakeReporters = fakeReporters.concat(fakeReporters2);
-  fakeMultiReporters = fakeMultiReporters.concat(fakeMultiReporters2);
   ]]>
   </script>
 
   <iframe id="amFrame"  height="400" src="about:memory"></iframe>
   <iframe id="amvFrame" height="400" src="about:memory?verbose"></iframe>
 
   <script type="application/javascript">
   <![CDATA[
@@ -560,32 +560,32 @@ 104,857,600 B ── heap-allocated\n\
       mgr.registerMultiReporter(realMultiReporters[i]);
     }
     SimpleTest.finish();
   }
 
   // Cut+paste the entire page and check that the cut text matches what we
   // expect.  This tests the output in general and also that the cutting and
   // pasting works as expected.
-  function test(aFrame, aExpected, aNext) {
+  function test(aFrameId, aExpected, aNext) {
     SimpleTest.executeSoon(function() {
       let mostRecentActual;
-      document.getElementById(aFrame).focus();
+      document.getElementById(aFrameId).focus();
       SimpleTest.waitForClipboard(
         function(aActual) {
           mostRecentActual = aActual;
           return aActual === aExpected;
         },
         function() {
           synthesizeKey("A", {accelKey: true});
           synthesizeKey("C", {accelKey: true});
         },
         aNext,
         function() {
-          ok(false, "pasted text doesn't match for " + aFrame);
+          ok(false, "pasted text doesn't match for " + aFrameId);
           dump("******EXPECTED******\n");
           dump(aExpected);
           dump("*******ACTUAL*******\n");
           dump(mostRecentActual);
           dump("********************\n");
           finish();
         }
       );
--- a/xpcom/base/nsIMemoryReporter.idl
+++ b/xpcom/base/nsIMemoryReporter.idl
@@ -67,19 +67,25 @@ interface nsIMemoryReporter : nsISupport
    * (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".  There are three categories of paths.
+   * '/'-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.
    *
-   * - Paths starting with "explicit" represent regions of memory that have
+   * 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
    *   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:
@@ -94,46 +100,51 @@ interface nsIMemoryReporter : nsISupport
    *
    *   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.
    *
-   * - Paths starting with "map" represent regions of virtual memory that the
-   *   process has mapped.  The reporter immediately beneath "map" describes
-   *   the type of measurement; for instance, the reporter "map/rss/[stack]"
-   *   might report how much of the process's stack is currently in physical
-   *   memory.
+   * - 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 and units BYTES.
    *
+   * - Paths starting with "compartments/" represent the names of JS
+   *   compartments.  Reporters in this category must paths of the form
+   *   "compartments/user/<name>" or "compartments/system/<name>", amount 1,
+   *   kind OTHER, units COUNT, and an empty description.
+   *
    * - All other paths represent cross-cutting values and may overlap with any
-   *   other reporter.
+   *   other reporter.  Reporters in this category must have paths that do not
+   *   contain '/' separators, and kind OTHER.
    */
   readonly attribute AUTF8String path;
 
   /*
    * There are three categories 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".
+   *    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 "map".
+   *    and must have a path starting with "explicit/" or "smaps/".
    *
    *  - OTHER: reporters which don't fit into either of these categories. Such
-   *    reporters must have a path that does not start with "explicit" or "map"
-   *    and may have any units.
+   *    reporters must have a path that does not start with "explicit/" or
+   *    "smaps/" and may have any units.
    */
   const PRInt32 KIND_NONHEAP = 0;
   const PRInt32 KIND_HEAP    = 1;
   const PRInt32 KIND_OTHER   = 2;
 
   /*
    * 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