Bug 856917 (part 1) - Improve about:memory's functional UI. r=kats.
authorNicholas Nethercote <nnethercote@mozilla.com>
Sun, 07 Apr 2013 21:37:19 -0700
changeset 140663 0b2c39985587efd2ea3f772c277942e3a4693aee
parent 140662 d13fc07e67afaf4320078f5954de3a3ea3c30596
child 140664 1c5977e8d52f485243a8d409138c2037f3572293
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs856917
milestone23.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 856917 (part 1) - Improve about:memory's functional UI. r=kats.
mobile/android/themes/core/aboutMemory.css
toolkit/components/aboutmemory/content/aboutMemory.css
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_aboutmemory2.xul
toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
toolkit/components/aboutmemory/tools/diff-memory-reports.js
--- a/mobile/android/themes/core/aboutMemory.css
+++ b/mobile/android/themes/core/aboutMemory.css
@@ -1,62 +1,83 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* This file is used for both about:memory and about:compartments. */
+/*
+ * This file is used for both about:memory and about:compartments.
+ *
+ * The version used for desktop is located at
+ * toolkit/components/aboutmemory/content/aboutMemory.css.
+ * Mobile-specific stuff is at the bottom of this file.
+ */
 
 html {
   background: -moz-Dialog;
   font: message-box;
 }
 
 body {
   padding: 0 2em;
   margin: 0;
   min-width: 45em;
   margin: auto;
 }
 
-div.section, div.footer {
-  margin: 2em 0;
-  box-sizing: border-box;
-  -moz-box-sizing: border-box;
+div.ancillary {
+  margin: 0.5em 0;
+  -moz-user-select: none;
 }
 
 div.section {
-  padding: 3em;
+  padding: 2em;
+  margin: 1em 0em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
 }
 
-body.non-verbose pre.entries {
+div.opsRow {
+  padding: 0.5em;
+  margin-right: 0.5em;
+  margin-top: 0.5em;
+  border: 1px solid ThreeDShadow;
+  border-radius: 10px;
+  background: -moz-Field;
+  display: inline-block;
+}
+
+div.opsRowLabel {
+  display: block;
+  margin-bottom: 0.2em;
+  font-weight: bold;
+}
+
+.opsRowLabel label {
+  margin-left: 1em;
+  font-weight: normal;
+}
+
+div.non-verbose pre.entries {
   overflow-x: hidden;
   text-overflow: ellipsis;
 }
 
 h1 {
   padding: 0;
   margin: 0;
 }
 
 h2 {
   background: #ddd;
   padding-left: .1em;
 }
 
-/* buttons are different sizes and overlapping without this */
-button {
-  margin: 1%;
-  padding: 2%;
-}
-
 .accuracyWarning {
-  color: #f00;
+  color: #d22;
 }
 
 .badInputWarning {
   color: #f00;
 }
 
 .treeline {
   color: #888;
@@ -80,35 +101,51 @@ button {
 .mrNote {
   color: #604;
 }
 
 .hasKids {
   cursor: pointer;
 }
 
+.hasKids:hover {
+  text-decoration: underline;
+}
+
+.noselect {
+  -moz-user-select: none;  /* no need to include this when cutting+pasting */
+}
+
 .option {
   font-size: 80%;
   -moz-user-select: none;  /* no need to include this when cutting+pasting */
 }
 
 .legend {
   font-size: 80%;
   -moz-user-select: none;  /* no need to include this when cutting+pasting */
 }
 
-.hiddenOnMobile {
-  display: none;
-}
-
 .debug {
   font-size: 80%;
 }
 
 .hidden {
   display: none;
 }
 
 .invalid {
   color: #fff;
   background-color: #f00;
 }
 
+/* Mobile-specific parts go here. */
+
+/* buttons are different sizes and overlapping without this */
+button {
+  margin: 1%;
+  padding: 2%;
+}
+
+.hiddenOnMobile {
+  display: none;
+}
+
--- a/toolkit/components/aboutmemory/content/aboutMemory.css
+++ b/toolkit/components/aboutmemory/content/aboutMemory.css
@@ -1,48 +1,67 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*
  * This file is used for both about:memory and about:compartments.
  *
- * Portions of this file are based on
- * toolkit/themes/windows/global/about.css.
- *
- * A version used for mobile is located at
+ * The version used for mobile is located at
  * mobile/android/themes/core/aboutMemory.css.
+ * Desktop-specific stuff is at the bottom of this file.
  */
 
 html {
   background: -moz-Dialog;
   font: message-box;
 }
 
 body {
   padding: 0 2em;
   margin: 0;
   min-width: 45em;
   margin: auto;
 }
 
-div.section, div.footer {
-  margin: 2em 0;
-  box-sizing: border-box;
-  -moz-box-sizing: border-box;
+div.ancillary {
+  margin: 0.5em 0;
+  -moz-user-select: none;
 }
 
 div.section {
-  padding: 3em;
+  padding: 2em;
+  margin: 1em 0em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
 }
 
-body.non-verbose pre.entries {
+div.opsRow {
+  padding: 0.5em;
+  margin-right: 0.5em;
+  margin-top: 0.5em;
+  border: 1px solid ThreeDShadow;
+  border-radius: 10px;
+  background: -moz-Field;
+  display: inline-block;
+}
+
+div.opsRowLabel {
+  display: block;
+  margin-bottom: 0.2em;
+  font-weight: bold;
+}
+
+.opsRowLabel label {
+  margin-left: 1em;
+  font-weight: normal;
+}
+
+div.non-verbose pre.entries {
   overflow-x: hidden;
   text-overflow: ellipsis;
 }
 
 h1 {
   padding: 0;
   margin: 0;
 }
@@ -86,16 +105,20 @@ h2 {
 .hasKids {
   cursor: pointer;
 }
 
 .hasKids:hover {
   text-decoration: underline;
 }
 
+.noselect {
+  -moz-user-select: none;  /* no need to include this when cutting+pasting */
+}
+
 .option {
   font-size: 80%;
   -moz-user-select: none;  /* no need to include this when cutting+pasting */
 }
 
 .legend {
   font-size: 80%;
   -moz-user-select: none;  /* no need to include this when cutting+pasting */
@@ -109,8 +132,14 @@ h2 {
   display: none;
 }
 
 .invalid {
   color: #fff;
   background-color: #f00;
 }
 
+/* Desktop-specific parts go here. */
+
+.hasKids:hover {
+  text-decoration: underline;
+}
+
--- a/toolkit/components/aboutmemory/content/aboutMemory.js
+++ b/toolkit/components/aboutmemory/content/aboutMemory.js
@@ -1,31 +1,28 @@
 /* -*- Mode: js2; tab-width: 8; indent-tabs-mode: nil; js2-basic-offset: 2 -*-*/
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // This file is used for both about:memory and about:compartments.
 
-// about:memory will by default show information about the browser's current
-// memory usage, but you can direct it to load information from a file by
-// providing a file= query string.  For example,
+// You can direct about:memory to immediately load memory reports from a file
+// by providing a file= query string.  For example,
 //
-//     about:memory?file=/foo/bar
-//     about:memory?verbose&file=/foo/bar%26baz
+//     about:memory?file=/home/username/reports.json.gz
 //
-// The order of "verbose" and "file=" isn't significant, and neither "verbose"
-// nor "file=" is case-sensitive.  We'll URI-unescape the contents of the
+// "file=" is not case-sensitive.  We'll URI-unescape the contents of the
 // "file=" argument, and obviously the filename is case-sensitive iff you're on
 // a case-sensitive filesystem.  If you specify more than one "file=" argument,
 // only the first one is used.
 //
-// about:compartments doesn't support the "verbose" or "file=" parameters and
-// will ignore them if they're provided.
+// about:compartments doesn't support "file=" parameters and will ignore them
+// if they're provided.
 
 "use strict";
 
 //---------------------------------------------------------------------------
 // Code shared by about:memory and about:compartments
 //---------------------------------------------------------------------------
 
 const Cc = Components.classes;
@@ -58,28 +55,24 @@ XPCOMUtils.defineLazyGetter(this, "nsGzi
 let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
              .getService(Ci.nsIMemoryReporterManager);
 
 let gUnnamedProcessStr = "Main Process";
 
 // Because about:memory and about:compartments are non-standard URLs,
 // location.search is undefined, so we have to use location.href here.
 // The toLowerCase() calls ensure that addresses like "ABOUT:MEMORY" work.
-let gVerbose = false;
 let gIsDiff = false;
 {
   let split = document.location.href.split('?');
   document.title = split[0].toLowerCase();
 
   if (split.length === 2) {
     let searchSplit = split[1].split('&');
     for (let i = 0; i < searchSplit.length; i++) {
-      if (searchSplit[i].toLowerCase() === 'verbose') {
-        gVerbose = true;
-      }
       if (searchSplit[i].toLowerCase() === 'diff') {
         gIsDiff = true;
       }
     }
   }
 }
 
 let gChildMemoryListener = undefined;
@@ -116,19 +109,21 @@ function assertInput(aCond, aMsg)
     throw "Invalid memory report(s): " + aMsg;
   }
 }
 
 function handleException(ex)
 {
   let str = ex.toString();
   if (str.startsWith(gAssertionFailureMsgPrefix)) {
-    throw ex;     // Argh, assertion failure within this file!  Give up.
+    // Argh, assertion failure within this file!  Give up.
+    throw ex;
   } else {
-    badInput(ex); // File or memory reporter problem.  Print a message.
+    // File or memory reporter problem.  Print a message.
+    updateMainAndFooter(ex.toString(), HIDE_FOOTER, "badInputWarning");
   }
 }
 
 function reportAssertionFailure(aMsg)
 {
   let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
   if (debug.isDebugBuild) {
     debug.assertion(aMsg, "false", "aboutMemory.js", 0);
@@ -136,22 +131,16 @@ function reportAssertionFailure(aMsg)
 }
 
 function debug(x)
 {
   let section = appendElement(document.body, 'div', 'section');
   appendElementWithText(section, "div", "debug", JSON.stringify(x));
 }
 
-function badInput(x)
-{
-  let section = appendElement(document.body, 'div', 'section');
-  appendElementWithText(section, "div", "badInputWarning", x);
-}
-
 //---------------------------------------------------------------------------
 
 function addChildObserversAndUpdate(aUpdateFn)
 {
   let os = Cc["@mozilla.org/observer-service;1"]
              .getService(Ci.nsIObserverService);
   os.notifyObservers(null, "child-memory-reporter-request", null);
 
@@ -250,23 +239,61 @@ function processMemoryReportsFromFile(aR
       aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
                     r.description);
     }
   }
 }
 
 //---------------------------------------------------------------------------
 
-function clearBody()
+// The <div> holding everything but the header and footer (if they're present).
+// It's what is updated each time the page changes.
+let gMain;
+
+// The <div> holding the footer.  Is undefined in about:compartments.
+let gFooter;
+
+// The "verbose" checkbox.
+let gVerbose;
+
+// Values for the second argument to updateMainAndFooter.
+let HIDE_FOOTER = 0;
+let SHOW_FOOTER = 1;
+let IGNORE_FOOTER = 2;
+
+function updateMainAndFooter(aMsg, aFooterAction, aClassName)
 {
-  let oldBody = document.body;
-  let body = oldBody.cloneNode(false);
-  oldBody.parentNode.replaceChild(body, oldBody);
-  body.classList.add(gVerbose ? 'verbose' : 'non-verbose');
-  return body
+  // Clear gMain by replacing it with an empty node.
+  let tmp = gMain.cloneNode(false);
+  gMain.parentNode.replaceChild(tmp, gMain);
+  gMain = tmp;
+
+  gMain.classList.remove('hidden');
+  gMain.classList.remove('verbose');
+  gMain.classList.remove('non-verbose');
+  if (gVerbose) {
+    gMain.classList.add(gVerbose.checked ? 'verbose' : 'non-verbose');
+  }
+
+  if (aMsg) {
+    let className = "section"
+    if (aClassName) {
+      className = className + " " + aClassName;
+    }
+    appendElementWithText(gMain, 'div', className, aMsg);
+  }
+
+  if (gFooter !== undefined) {
+    switch (aFooterAction) {
+     case HIDE_FOOTER:   gFooter.classList.add('hidden');    break;
+     case SHOW_FOOTER:   gFooter.classList.remove('hidden'); break;
+     case IGNORE_FOOTER:                                     break;
+     default: assertInput(false, "bad footer action in updateMainAndFooter");
+    }
+  }
 }
 
 function appendTextNode(aP, aText)
 {
   let e = document.createTextNode(aText);
   aP.appendChild(e);
   return e;
 }
@@ -370,147 +397,233 @@ function isSmapsPath(aUnsafePath)
       return true;
     }
   }
   return false;
 }
 
 //---------------------------------------------------------------------------
 
+function appendButton(aP, aTitle, aOnClick, aText, aId)
+{
+  let b = appendElementWithText(aP, "button", "", aText);
+  b.title = aTitle;
+  b.onclick = aOnClick;
+  if (aId) {
+    b.id = aId;
+  }
+  return b;
+}
+
 function onLoadAboutMemory()
 {
+  // Generate the header.
+
+  let header = appendElement(document.body, "div", "ancillary");
+
+  // A hidden file input element that can be invoked when necessary.
+  let filePickerInput = appendElementWithText(header, "input", "hidden", "");
+  filePickerInput.type = "file";
+  filePickerInput.id = "filePickerInput";   // used in testing
+  filePickerInput.addEventListener("change", function() {
+    let file = this.files[0];
+    let filename = file.mozFullPath;
+    updateAboutMemoryFromFile(filename);
+  });
+
+  const CuDesc = "Measure current memory reports and show.";
+  const LdDesc = "Load memory reports from file and show.";
+  const RdDesc = "Read memory reports from the clipboard and show.";
+
+  const SvDesc = "Save memory reports to file.";
+
+  const GCDesc = "Do a global garbage collection.";
+  const CCDesc = "Do a cycle collection.";
+  const MMDesc = "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.";
+
+  let ops = appendElement(header, "div", "");
+
+  let row1 = appendElement(ops, "div", "opsRow");
+
+  let labelDiv =
+   appendElementWithText(row1, "div", "opsRowLabel", "Show memory reports");
+  let label = appendElementWithText(labelDiv, "label", "");
+  gVerbose = appendElement(label, "input", "");
+  gVerbose.type = "checkbox";
+  gVerbose.id = "verbose";   // used for testing
+
+  appendTextNode(label, "verbose");
+
+  const kEllipsis = "\u2026";
+
+  // The "measureButton" id is used for testing.
+  appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton");
+  appendButton(row1, LdDesc, () => filePickerInput.click(), "Load" + kEllipsis);
+  appendButton(row1, RdDesc, updateAboutMemoryFromClipboard,
+               "Read from clipboard");
+
+  let row2 = appendElement(ops, "div", "opsRow");
+
+  appendElementWithText(row2, "div", "opsRowLabel", "Save memory reports");
+  appendButton(row2, SvDesc, saveReportsToFile, "Measure and save" + kEllipsis);
+
+  let row3 = appendElement(ops, "div", "opsRow");
+
+  appendElementWithText(row3, "div", "opsRowLabel", "Free memory");
+  appendButton(row3, GCDesc, doGC,  "GC");
+  appendButton(row3, CCDesc, doCC,  "CC");
+  appendButton(row3, MMDesc, doMMU, "Minimize memory usage");
+
+  // Generate the main div, where content ("section" divs) will go.  It's
+  // hidden at first.
+
+  gMain = appendElement(document.body, 'div', '');
+
+  // Generate the footer.  It's hidden at first.
+
+  gFooter = appendElement(document.body, 'div', 'ancillary hidden');
+
+  let a = appendElementWithText(gFooter, "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(gFooter, "div", "legend", legendText1);
+  appendElementWithText(gFooter, "div", "legend hiddenOnMobile", legendText2);
+
   // Check location.href to see if we're loading from a file.
   let search = location.href.split('?')[1];
   if (search) {
     let searchSplit = search.split('&');
     for (let i = 0; i < searchSplit.length; i++) {
       if (searchSplit[i].toLowerCase().startsWith('file=')) {
         let filename = searchSplit[i].substring('file='.length);
         updateAboutMemoryFromFile(decodeURIComponent(filename));
         return;
       }
     }
   }
-
-  addChildObserversAndUpdate(updateAboutMemory);
 }
 
-function doGlobalGC()
+function doGC()
 {
   Cu.forceGC();
   let os = Cc["@mozilla.org/observer-service;1"]
              .getService(Ci.nsIObserverService);
   os.notifyObservers(null, "child-gc-request", null);
-  updateAboutMemory();
+  updateMainAndFooter("Garbage collection completed", HIDE_FOOTER);
 }
 
 function doCC()
 {
   window.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIDOMWindowUtils)
         .cycleCollect();
   let os = Cc["@mozilla.org/observer-service;1"]
              .getService(Ci.nsIObserverService);
   os.notifyObservers(null, "child-cc-request", null);
-  updateAboutMemory();
+  updateMainAndFooter("Cycle collection completed", HIDE_FOOTER);
+}
+
+function doMMU()
+{
+  gMgr.minimizeMemoryUsage(
+    () => updateMainAndFooter("Memory minimization completed", HIDE_FOOTER));
+}
+
+function doMeasure()
+{
+  addChildObserversAndUpdate(updateAboutMemoryFromReporters);
 }
 
 //---------------------------------------------------------------------------
 
 /**
  * Top-level function that does the work of generating the page from the memory
  * reporters.
  */
-function updateAboutMemory()
+function updateAboutMemoryFromReporters()
 {
-  // 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();
+  // First, clear the contents of main.  Necessary because
+  // updateAboutMemoryFromReporters() might be called more than once due to the
+  // "child-memory-reporter-update" observer.
+  updateMainAndFooter("", SHOW_FOOTER);
 
   try {
     // Process the reports from the memory reporters.
     let process = function(aIgnoreSingle, aIgnoreMulti, aHandleReport) {
       processMemoryReporters(aIgnoreSingle, aIgnoreMulti, aHandleReport);
     }
-    appendAboutMemoryMain(body, process, gMgr.hasMozMallocUsableSize,
+    appendAboutMemoryMain(process, gMgr.hasMozMallocUsableSize,
                           /* forceShowSmaps = */ false);
 
   } catch (ex) {
     handleException(ex);
-
-  } finally {
-    appendAboutMemoryFooter(body);
   }
 }
 
 // Increment this if the JSON format changes.
 var gCurrentFileFormatVersion = 1;
 
 /**
- * Handle an update exception that occurs while updating the page.
- *
- * @param aEx
- *        The exception.
- */
-function clearBodyAndHandleException(aEx) {
-  let body = clearBody();
-  handleException(aEx);
-  appendAboutMemoryFooter(body);
-}
-
-/**
  * Populate about:memory using the data in the given JSON string.
  *
  * @param aJSONString
  *        A string containing JSON data conforming to the schema used by
  *        nsIMemoryReporterManager::dumpReports.
  */
 function updateAboutMemoryFromJSONString(aJSONString)
 {
-  let body = clearBody();
-
   try {
     let json = JSON.parse(aJSONString);
     assertInput(json.version === gCurrentFileFormatVersion,
                 "data version number missing or doesn't match");
     assertInput(json.hasMozMallocUsableSize !== undefined,
                 "missing 'hasMozMallocUsableSize' property");
     assertInput(json.reports && json.reports instanceof Array,
                 "missing or non-array 'reports' property");
     let process = function(aIgnoreSingle, aIgnoreMulti, aHandleReport) {
       processMemoryReportsFromFile(json.reports, aIgnoreSingle,
                                    aHandleReport);
     }
-    appendAboutMemoryMain(body, process, json.hasMozMallocUsableSize,
+    appendAboutMemoryMain(process, json.hasMozMallocUsableSize,
                           /* forceShowSmaps = */ true);
   } catch (ex) {
     handleException(ex);
-  } finally {
-    appendAboutMemoryFooter(body);
   }
 }
 
 /**
- * Like updateAboutMemory(), but gets its data from a file instead of the
- * memory reporters.
+ * Like updateAboutMemoryFromReporters(), but gets its data from a file instead
+ * of the memory reporters.
  *
  * @param aFilename
  *        The name of the file being read from.
  *
  *        The expected format of the file's contents is described in the
  *        comment describing nsIMemoryReporterManager::dumpReports.
  */
 function updateAboutMemoryFromFile(aFilename)
 {
+  updateMainAndFooter("Loading...", HIDE_FOOTER);
+
   try {
     let reader = new FileReader();
     reader.onerror = () => { throw "FileReader.onerror"; };
     reader.onabort = () => { throw "FileReader.onabort"; };
     reader.onload = (aEvent) => {
+      updateMainAndFooter("", SHOW_FOOTER);  // Clear "Loading..." from above.
       updateAboutMemoryFromJSONString(aEvent.target.result);
     };
 
     // If it doesn't have a .gz suffix, read it as a (legacy) ungzipped file.
     if (!aFilename.endsWith(".gz")) {
       reader.readAsText(new File(aFilename));
       return;
     }
@@ -526,27 +639,27 @@ function updateAboutMemoryFromFile(aFile
       },
       onStopRequest: function(aR, aC, aStatusCode) {
         try {
           if (!Components.isSuccessCode(aStatusCode)) {
             throw aStatusCode;
           }
           reader.readAsText(new Blob(this.data));
         } catch (ex) {
-          clearBodyAndHandleException(ex);
+          handleException(ex);
         }
       }
     }, null);
 
     let file = new nsFile(aFilename);
     let fileChan = Services.io.newChannelFromURI(Services.io.newFileURI(file));
     fileChan.asyncOpen(converter, null);
 
   } catch (ex) {
-    clearBodyAndHandleException(ex);
+    handleException(ex);
   }
 }
 
 /**
  * Like updateAboutMemoryFromFile(), but gets its data from the clipboard
  * instead of a file.
  */
 function updateAboutMemoryFromClipboard()
@@ -568,36 +681,34 @@ function updateAboutMemoryFromClipboard(
     transferable.getTransferData('text/unicode', cbData,
                                  /* out dataLen (ignored) */ {});
     let cbString = cbData.value.QueryInterface(Ci.nsISupportsString).data;
 
     // Success!  Now use the string to generate about:memory.
     updateAboutMemoryFromJSONString(cbString);
 
   } catch (ex) {
-    clearBodyAndHandleException(ex);
+    handleException(ex);
   }
 }
 
 /**
  * Processes reports (whether from reporters or from a file) and append the
  * main part of the page.
  *
- * @param aBody
- *        The DOM body element.
  * @param aProcess
  *        Function that extracts the memory reports from the reporters or from
  *        file.
  * @param aHasMozMallocUsableSize
  *        Boolean indicating if moz_malloc_usable_size works.
  * @param aForceShowSmaps
  *        True if we should show the smaps memory reporters even if we're not
  *        in verbose mode.
  */
-function appendAboutMemoryMain(aBody, aProcess, aHasMozMallocUsableSize,
+function appendAboutMemoryMain(aProcess, aHasMozMallocUsableSize,
                                aForceShowSmaps)
 {
   let treesByProcess = {}, degeneratesByProcess = {}, heapTotalByProcess = {};
   getTreesByProcess(aProcess, treesByProcess, degeneratesByProcess,
                     heapTotalByProcess, aForceShowSmaps);
 
   // Sort our list of processes.
   let processes = Object.keys(treesByProcess);
@@ -636,110 +747,26 @@ function appendAboutMemoryMain(aBody, aP
     }
 
     return 0;
   });
 
   // Generate output for each process.
   for (let i = 0; i < processes.length; i++) {
     let process = processes[i];
-    let section = appendElement(aBody, 'div', 'section');
+    let section = appendElement(gMain, 'div', 'section');
 
     appendProcessAboutMemoryElements(section, process,
                                      treesByProcess[process],
                                      degeneratesByProcess[process],
                                      heapTotalByProcess[process],
                                      aHasMozMallocUsableSize);
   }
 }
 
-/**
- * Appends the page footer.
- *
- * @param aBody
- *        The DOM body element.
- */
-function appendAboutMemoryFooter(aBody)
-{
-  let section = appendElement(aBody, 'div', 'footer');
-
-  // 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.";
-  const RdDesc = "Read memory report data from a file.";
-  const CbDesc = "Read memory report data from the clipboard.";
-  const WrDesc = "Write memory report data to a file.";
-
-  function appendButton(aP, aTitle, aOnClick, aText, aId)
-  {
-    let b = appendElementWithText(aP, "button", "", aText);
-    b.title = aTitle;
-    b.onclick = aOnClick
-    if (aId) {
-      b.id = aId;
-    }
-  }
-
-  let div1 = appendElement(section, "div");
-
-  // The "Update" button has an id so it can be clicked in a test.
-  appendButton(div1, UpDesc, updateAboutMemory, "Update", "updateButton");
-  appendButton(div1, GCDesc, doGlobalGC,        "GC");
-  appendButton(div1, CCDesc, doCC,              "CC");
-  appendButton(div1, MPDesc,
-               function() { gMgr.minimizeMemoryUsage(updateAboutMemory); },
-               "Minimize memory usage");
-
-  // The standard file input element is ugly.  So we hide it, and add a button
-  // that when clicked invokes the input element.
-  let input = appendElementWithText(div1, "input", "hidden", "input text");
-  input.type = "file";
-  input.id = "fileInput";   // has an id so it can be invoked by a test
-  input.addEventListener("change", function() {
-    let file = this.files[0];
-    updateAboutMemoryFromFile(file.mozFullPath);
-  }); 
-  appendButton(div1, RdDesc, function() { input.click() },
-               "Read reports from a file", "readReportsFromFileButton");
-
-  appendButton(div1, CbDesc, updateAboutMemoryFromClipboard,
-               "Read reports from clipboard", "readReportsFromClipboardButton");
-
-  appendButton(div1, WrDesc, writeReportsToFile,
-               "Write reports to a file", "writeReportsToAFileButton");
-
-  let div2 = appendElement(section, "div");
-  if (gVerbose) {
-    let a = appendElementWithText(div2, "a", "option", "Less verbose");
-    a.href = "about:memory";
-  } else {
-    let a = appendElementWithText(div2, "a", "option", "More verbose");
-    a.href = "about:memory?verbose";
-  }
-
-  let div3 = appendElement(section, "div");
-  let a = appendElementWithText(div3, "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(section, "div", "legend", legendText1);
-  appendElementWithText(section, "div", "legend hiddenOnMobile", 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;
 
 /**
@@ -779,34 +806,35 @@ function getTreesByProcess(aProcessMemor
   //
   // We don't show both resident and resident-fast because running the resident
   // reporter can purge pages on MacOS, which affects the results of the
   // resident-fast reporter.  We don't want about:memory's results to be
   // affected by the order of memory reporter execution.
 
   function ignoreSingle(aUnsafePath)
   {
-    return (isSmapsPath(aUnsafePath) && !gVerbose && !aForceShowSmaps) ||
+    return (isSmapsPath(aUnsafePath) && !gVerbose.checked && !aForceShowSmaps) ||
            aUnsafePath.startsWith("compartments/") ||
            aUnsafePath.startsWith("ghost-windows/") ||
            aUnsafePath == "resident-fast";
   }
 
   function ignoreMulti(aMRName)
   {
-    return (aMRName === "smaps" && !gVerbose && !aForceShowSmaps) ||
+    return (aMRName === "smaps" && !gVerbose.checked && !aForceShowSmaps) ||
             aMRName === "compartments" ||
             aMRName === "ghost-windows";
   }
 
   function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
                         aDescription)
   {
     if (isExplicitPath(aUnsafePath)) {
-      assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP, "bad explicit kind");
+      assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP,
+                  "bad explicit kind");
       assertInput(aUnits === UNITS_BYTES, "bad explicit units");
       assertInput(gSentenceRegExp.test(aDescription),
                   "non-sentence explicit description");
 
     } else if (isSmapsPath(aUnsafePath)) {
       assertInput(aKind === KIND_NONHEAP, "bad smaps kind");
       assertInput(aUnits === UNITS_BYTES, "bad smaps units");
       assertInput(aDescription !== "", "empty smaps description");
@@ -1048,17 +1076,17 @@ function addHeapUnclassifiedNode(aT, aHe
  *        The tree.
  */
 function sortTreeAndInsertAggregateNodes(aTotalBytes, aT)
 {
   const kSignificanceThresholdPerc = 1;
 
   function isInsignificant(aT)
   {
-    return !gVerbose &&
+    return !gVerbose.checked &&
            (100 * aT._amount / aTotalBytes) < kSignificanceThresholdPerc;
   }
 
   if (!aT._kids) {
     return;
   }
 
   aT._kids.sort(TreeNode.compareAmounts);
@@ -1246,17 +1274,17 @@ function appendProcessAboutMemoryElement
     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. 
+  // 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];
@@ -1338,20 +1366,20 @@ function formatInt(aN, aExtra)
  * Converts a byte count to an appropriate string representation.
  *
  * @param aBytes
  *        The byte count.
  * @return The string representation.
  */
 function formatBytes(aBytes)
 {
-  let unit = gVerbose ? " B" : " MB";
+  let unit = gVerbose.checked ? " B" : " MB";
 
   let s;
-  if (gVerbose) {
+  if (gVerbose.checked) {
     s = formatInt(aBytes, unit);
   } else {
     let mbytes = (aBytes / (1024 * 1024)).toFixed(2);
     let a = String(mbytes).split(".");
     // If the argument to formatInt() is -0, it will print the negative sign.
     s = formatInt(Number(a[0])) + "." + a[1] + unit;
   }
   return s;
@@ -1626,17 +1654,17 @@ function appendTreeElements(aP, aRoot, a
     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) {
+    if (!gVerbose.checked && 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");
 
@@ -1669,17 +1697,17 @@ function appendTreeElements(aP, aRoot, a
 function appendSectionHeader(aP, aText)
 {
   appendElementWithText(aP, "h2", "", aText + "\n");
   return appendElement(aP, "pre", "entries");
 }
 
 //---------------------------------------------------------------------------
 
-function writeReportsToFile()
+function saveReportsToFile()
 {
   let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave);
   fp.appendFilter("Zipped JSON files", "*.json.gz");
   fp.appendFilters(Ci.nsIFilePicker.filterAll);
   fp.filterIndex = 0;
   fp.addToRecentDocs = true;
   fp.defaultString = "memory-report.json.gz";
@@ -1687,73 +1715,86 @@ function writeReportsToFile()
   let fpCallback = function(aResult) {
     if (aResult == Ci.nsIFilePicker.returnOK ||
         aResult == Ci.nsIFilePicker.returnReplace) {
 
       let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
                      .getService(Ci.nsIMemoryInfoDumper);
 
       dumper.dumpMemoryReportsToNamedFile(fp.file.path);
+
+      updateMainAndFooter("Saved reports to " + fp.file.path, HIDE_FOOTER);
     }
   };
   fp.open(fpCallback);
 }
 
 //-----------------------------------------------------------------------------
 // Code specific to about:compartments
 //-----------------------------------------------------------------------------
 
 function onLoadAboutCompartments()
 {
+  // Generate the main div, where content will go.  about:compartments doesn't
+  // have a header or footer.
+  gMain = appendElement(document.body, 'div', 'section');
+
   // First generate the page, then minimize memory usage to collect any dead
   // compartments, then update the page.  The first generation step may sound
   // unnecessary, but it avoids a short delay in showing content when the page
   // is loaded, which makes test_aboutcompartments.xul more reliable (see bug
   // 729018 for details).
   updateAboutCompartments();
   gMgr.minimizeMemoryUsage(
     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
+  // First, clear the contents of main.  Necessary because
+  // updateAboutMemoryFromReporters() might be called more than once due to the
   // "child-memory-reporter-update" observer.
-  let body = clearBody();
+  updateMainAndFooter("", IGNORE_FOOTER);
 
-  let compartmentsByProcess = getCompartmentsByProcess();
-  let ghostWindowsByProcess = getGhostWindowsByProcess();
+  try {
+    let compartmentsByProcess = getCompartmentsByProcess();
+    let ghostWindowsByProcess = getGhostWindowsByProcess();
 
-  function handleProcess(aProcess) {
-    let section = appendElement(body, 'div', 'section');
-    appendProcessAboutCompartmentsElements(section, aProcess,
-                                           compartmentsByProcess[aProcess],
-                                           ghostWindowsByProcess[aProcess]);
-  }
+    // Sort our list of processes.
+    let processes = Object.keys(compartmentsByProcess);
+    processes.sort(function(aProcessA, aProcessB) {
+      assert(aProcessA != aProcessB,
+             "Elements of Object.keys() should be unique, but " +
+             "saw duplicate '" + aProcessA + "' elem.");
 
-  // Generate output for one process at a time.  Always start with the
-  // Main process.
-  handleProcess(gUnnamedProcessStr);
-  for (let process in compartmentsByProcess) {
-    if (process !== gUnnamedProcessStr) {
-      handleProcess(process);
+      // Always put the main process first.
+      if (aProcessA == gUnnamedProcessStr) {
+        return -1;
+      }
+      if (aProcessB == gUnnamedProcessStr) {
+        return 1;
+      }
+
+      // Otherwise the order doesn't matter.
+      return 0;
+    });
+
+    // Generate output for each process.
+    for (let i = 0; i < processes.length; i++) {
+      let process = processes[i];
+      appendProcessAboutCompartmentsElements(gMain, process,
+                                             compartmentsByProcess[process],
+                                             ghostWindowsByProcess[process]);
     }
-  }
 
-  let section = appendElement(body, 'div', 'footer');
-  if (gVerbose) {
-    let a = appendElementWithText(section, "a", "option", "Less verbose");
-    a.href = "about:compartments";
-  } else {
-    let a = appendElementWithText(section, "a", "option", "More verbose");
-    a.href = "about:compartments?verbose";
+  } catch (ex) {
+    handleException(ex);
   }
 }
 
 //---------------------------------------------------------------------------
 
 function Compartment(aUnsafeName, aIsSystemCompartment)
 {
   this._unsafeName          = aUnsafeName;
--- a/toolkit/components/aboutmemory/tests/test_aboutcompartments.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutcompartments.xul
@@ -135,19 +135,18 @@
     mgr.registerReporterEvenIfBlocked(fakeReporters[i]);
   }
   for (var i = 0; i < fakeMultiReporters.length; i++) {
     mgr.registerMultiReporterEvenIfBlocked(fakeMultiReporters[i]);
   }
   ]]>
   </script>
 
-  <iframe id="acFrame"  height="400" src="about:compartments"></iframe>
   <!-- vary the capitalization to make sure that works -->
-  <iframe id="acvFrame" height="400" src="abouT:compartMENTS?veRBOse"></iframe>
+  <iframe id="acFrame"  height="400" src="abouT:compartMENTS"></iframe>
 
   <script type="application/javascript">
   <![CDATA[
   var acExpectedText =
 "\
 Main Process\n\
 User Compartments\n\
 \n\
@@ -174,19 +173,16 @@ child-user-compartment\n\
 System Compartments\n\
 \n\
 child-system-compartment\n\
 \n\
 Ghost Windows\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]);
     }
@@ -235,23 +231,17 @@ Ghost Windows\n\
     });
   }
 
   SimpleTest.waitForFocus(function() {
     test(
       "acFrame",
       acExpectedText,
       function() {
-        test(
-          "acvFrame",
-          acvExpectedText,
-          function() {
-            finish()
-          }
-        )
+        finish()
       }
     );
   });
 
   SimpleTest.waitForExplicitFinish();
   ]]>
   </script>
 </window>
--- a/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
@@ -245,19 +245,19 @@
   ];
   for (let i = 0; i < fakeReporters2.length; i++) {
     mgr.registerReporterEvenIfBlocked(fakeReporters2[i]);
   }
   fakeReporters = fakeReporters.concat(fakeReporters2);
   ]]>
   </script>
 
-  <iframe id="amFrame"  height="400" src="about:memory"></iframe>
+  <iframe id="amFrame"  height="300" src="about:memory"></iframe>
   <!-- vary the capitalization to make sure that works -->
-  <iframe id="amvFrame" height="400" src="About:Memory?verbose"></iframe>
+  <iframe id="amvFrame" height="300" src="About:Memory"></iframe>
 
   <script type="application/javascript">
   <![CDATA[
   let amExpectedText =
 "\
 Main Process\n\
 Explicit Allocations\n\
 \n\
@@ -604,21 +604,30 @@ 104,857,600 B ── heap-allocated\n\
     mgr.unblockRegistration();
 
     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) {
+  function test(aFrameId, aVerbose, aExpected, aNext) {
     SimpleTest.executeSoon(function() {
       ok(document.title === "about:memory", "document.title is correct");
       let mostRecentActual;
-      document.getElementById(aFrameId).focus();
+      let frame = document.getElementById(aFrameId);
+      frame.focus();
+        
+      // Set the verbose checkbox value and click the go button.
+      let doc = frame.contentWindow.document;
+      let measureButton = doc.getElementById("measureButton");
+      let verbose = doc.getElementById("verbose");
+      verbose.checked = aVerbose;
+      measureButton.click();
+
       SimpleTest.waitForClipboard(
         function(aActual) {
           mostRecentActual = aActual;
           return aActual === aExpected;
         },
         function() {
           synthesizeKey("A", {accelKey: true});
           synthesizeKey("C", {accelKey: true});
@@ -635,20 +644,22 @@ 104,857,600 B ── heap-allocated\n\
         }
       );
     });
   }
 
   SimpleTest.waitForFocus(function() {
     test(
       "amFrame",
+      /* verbose = */ false,
       amExpectedText,
       function() {
         test(
           "amvFrame",
+          /* verbose = */ true,
           amvExpectedText,
           function() {
             finish()
           }
         )
       }
     );
   });
--- a/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul
@@ -105,31 +105,33 @@
     }
     mgr.unblockRegistration();
 
     SimpleTest.finish();
   }
 
   // Click on the identified element, then cut+paste the entire page and
   // check that the cut text matches what we expect.
-  function test(aId, aExpected, aNext) {
+  function test(aId, aSwap, aExpected, aNext) {
     let win = document.getElementById("amFrame").contentWindow;
     if (aId) {
       let node = win.document.getElementById(aId);
 
       // Yuk:  clicking a button is easy;  but for tree entries we need to
       // click on a child of the span identified via |id|.
-      // Also, we swap the paths of hiReport/hi2Report and
-      // jkReport/jk2Report just before updating, to test what happens when
-      // significant nodes become insignificant and vice versa.
       if (node.nodeName === "button") {
-        hiReport.path  = "explicit/j/k";
-        hi2Report.path = "explicit/j/k2";
-        jkReport.path  = "explicit/h/i";
-        jk2Report.path = "explicit/h/i2";
+        if (aSwap) {
+          // We swap the paths of hiReport/hi2Report and jkReport/jk2Report
+          // just before updating, to test what happens when significant nodes
+          // become insignificant and vice versa.
+          hiReport.path  = "explicit/j/k";
+          hi2Report.path = "explicit/j/k2";
+          jkReport.path  = "explicit/h/i";
+          jk2Report.path = "explicit/h/i2";
+        }
         node.click();
       } else {
         node.childNodes[0].click();
       }
     }
 
     SimpleTest.executeSoon(function() {
       let mostRecentActual;
@@ -156,17 +158,17 @@
       );
     });
   }
 
   // Returns a function that chains together one test() call per id.
   function chain(aIds) {
     let x = aIds.shift();
     if (x) {
-      return function() { test(x.id, x.expected, chain(aIds)); }
+      return function() { test(x.id, x.swap, x.expected, chain(aIds)); }
     } else {
       return function() { finish(); };
     }
   }
 
   let startExpected =
 "\
 Main Process\n\
@@ -412,26 +414,26 @@ 250.00 MB ── heap-allocated\n\
   // - explicit/a is significant, we collapse it (which hides its
   //   sub-trees), it's unchanged upon update, we re-expand it
   // - explicit/h is significant, we collapse it, it becomes insignificant
   //   upon update (and should remain collapsed)
   // - explicit/j is insignificant, we expand it, it becomes significant
   //   upon update (and should remain expanded)
   //
   let idsToClick = [
-    { id: "",                  expected: startExpected },
-    { id: "Main Process:explicit/a/c", expected: acCollapsedExpected },
-    { id: "Main Process:explicit/a/l", expected: alExpandedExpected },
-    { id: "Main Process:explicit/a",   expected: aCollapsedExpected },
-    { id: "Main Process:explicit/h",   expected: hCollapsedExpected },
-    { id: "Main Process:explicit/j",   expected: jExpandedExpected },
-    { id: "updateButton",      expected: updatedExpected },
-    { id: "Main Process:explicit/a",   expected: aExpandedExpected },
-    { id: "Main Process:explicit/a/c", expected: acExpandedExpected },
-    { id: "Main Process:explicit/a/l", expected: alCollapsedExpected }
+    { id: "measureButton",             swap: 0, expected: startExpected },
+    { id: "Main Process:explicit/a/c", swap: 0, expected: acCollapsedExpected },
+    { id: "Main Process:explicit/a/l", swap: 0, expected: alExpandedExpected },
+    { id: "Main Process:explicit/a",   swap: 0, expected: aCollapsedExpected },
+    { id: "Main Process:explicit/h",   swap: 0, expected: hCollapsedExpected },
+    { id: "Main Process:explicit/j",   swap: 0, expected: jExpandedExpected },
+    { id: "measureButton",             swap: 1, expected: updatedExpected },
+    { id: "Main Process:explicit/a",   swap: 0, expected: aExpandedExpected },
+    { id: "Main Process:explicit/a/c", swap: 0, expected: acExpandedExpected },
+    { id: "Main Process:explicit/a/l", swap: 0, expected: alCollapsedExpected }
   ];
 
   SimpleTest.waitForFocus(chain(idsToClick));
 
   SimpleTest.waitForExplicitFinish();
   ]]>
   </script>
 </window>
--- a/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
@@ -117,17 +117,17 @@
         let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
                         getService(Ci.nsIMemoryInfoDumper);
 
         dumper.dumpMemoryReportsToNamedFile(file.path,
                                             /* minimizeMemoryUsage = */ false,
                                             /* dumpChildProcesses = */ false);
     }
 
-    let input = frame.contentWindow.document.getElementById("fileInput");
+    let input = frame.contentWindow.document.getElementById("filePickerInput");
     input.value = file.path;    // this works because it's a chrome test
 
     var e = document.createEvent('Event');
     e.initEvent('change', true, true);
     input.dispatchEvent(e);
 
     // Initialize the clipboard contents.
     SpecialPowers.clipboardCopyString("initial clipboard value");
@@ -194,18 +194,17 @@ 0.30 MB (100.0%) -- other\n\
 \n\
 250.00 MB ── heap-allocated\n\
 \n\
 ";
 
   // This is the output for a malformed data file.
   let expectedBad =
 "\
-Invalid memory report(s): missing 'hasMozMallocUsableSize' property\n\
-";
+Invalid memory report(s): missing 'hasMozMallocUsableSize' property";
 
   let frames = [
     // This loads a pre-existing file that is valid.
     { frameId: "amGoodFrame", filename: "memory-reports-good.json", expected: expectedGood, dumpFirst: false },
 
     // This dumps to a file and then reads it back in.  The output is the same as the first test.
     { frameId: "amGoodFrame2", filename: "memory-reports-dumped.json.gz", expected: expectedGood, dumpFirst: true },
 
--- a/toolkit/components/aboutmemory/tools/diff-memory-reports.js
+++ b/toolkit/components/aboutmemory/tools/diff-memory-reports.js
@@ -2,17 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //---------------------------------------------------------------------------
 // This script diffs two memory report dumps produced by about:memory.  Run it
 // using a JS shell.  View the result in about:memory using its diff mode, e.g.
-// visit "about:memory?diff" or "about:memory?verbose&diff".
+// visit "about:memory?diff" or "about:memory?diff".
 //
 // A simple test can be performed by running:
 //
 //   js diff-memory-reports.js --test
 //
 //---------------------------------------------------------------------------
 
 //---------------------------------------------------------------------------