Bug 1517175 - Part 2: Add about:memory filter r=njn
authorCameron McCormack <cam@mcc.id.au>
Mon, 07 Jan 2019 03:04:46 +0000
changeset 509781 eb6cf8333a9aac70bddfa87b670dbce922fbe058
parent 509780 79dfd716ddeb583bf14d27281e4531d976db3d3f
child 509782 7cf2438bcd343bcd530e8e9f8e8e9c4cc92942e4
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnjn
bugs1517175
milestone66.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 1517175 - Part 2: Add about:memory filter r=njn Depends on D15628 Differential Revision: https://phabricator.services.mozilla.com/D15629
mobile/android/themes/core/aboutMemory.css
toolkit/components/aboutmemory/content/aboutMemory.css
toolkit/components/aboutmemory/content/aboutMemory.js
toolkit/components/aboutmemory/tests/chrome.ini
toolkit/components/aboutmemory/tests/test_aboutmemory7.xul
--- a/mobile/android/themes/core/aboutMemory.css
+++ b/mobile/android/themes/core/aboutMemory.css
@@ -49,48 +49,52 @@ div.sidebar {
   margin-left: 1em;
 }
 
 div.sidebarContents {
   position: sticky;
   top: 0.5em;
 }
 
-div.index {
+div.sidebarItem {
   padding: 0.5em;
   margin: 1em 0em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
   color: -moz-FieldText;
   -moz-user-select: none;  /* no need to include this when cutting+pasting */
 }
 
-ul.indexList {
+input.filterInput {
+  width: calc(100% - 1em);
+}
+
+ul.index {
   list-style-position: inside;
   margin: 0;
   padding: 0;
 }
 
-ul.indexList > li {
+ul.index > li {
   padding-left: 0.5em;
 }
 
 div.opsRow {
   padding: 0.5em;
   margin-right: 0.5em;
   margin-top: 0.5em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
   color: -moz-FieldText;
   display: inline-block;
 }
 
-div.opsRowLabel, div.indexLabel {
+div.opsRowLabel, div.sidebarLabel {
   display: block;
   margin-bottom: 0.2em;
   font-weight: bold;
 }
 
 .opsRowLabel label {
   margin-left: 1em;
   font-weight: normal;
--- a/toolkit/components/aboutmemory/content/aboutMemory.css
+++ b/toolkit/components/aboutmemory/content/aboutMemory.css
@@ -49,48 +49,52 @@ div.sidebar {
   margin-left: 1em;
 }
 
 div.sidebarContents {
   position: sticky;
   top: 0.5em;
 }
 
-div.index {
+div.sidebarItem {
   padding: 0.5em;
   margin: 1em 0em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
   color: -moz-FieldText;
   -moz-user-select: none;  /* no need to include this when cutting+pasting */
 }
 
-ul.indexList {
+input.filterInput {
+  width: calc(100% - 1em);
+}
+
+ul.index {
   list-style-position: inside;
   margin: 0;
   padding: 0;
 }
 
-ul.indexList > li {
+ul.index > li {
   padding-left: 0.5em;
 }
 
 div.opsRow {
   padding: 0.5em;
   margin-right: 0.5em;
   margin-top: 0.5em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
   color: -moz-FieldText;
   display: inline-block;
 }
 
-div.opsRowLabel, div.indexLabel {
+div.opsRowLabel, div.sidebarLabel {
   display: block;
   margin-bottom: 0.2em;
   font-weight: bold;
 }
 
 .opsRowLabel label {
   margin-left: 1em;
   font-weight: normal;
--- a/toolkit/components/aboutmemory/content/aboutMemory.js
+++ b/toolkit/components/aboutmemory/content/aboutMemory.js
@@ -51,18 +51,26 @@ XPCOMUtils.defineLazyGetter(this, "nsGzi
 let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
              .getService(Ci.nsIMemoryReporterManager);
 
 const gPageName = "about:memory";
 document.title = gPageName;
 
 const gUnnamedProcessStr = "Main Process";
 
+const gFilterUpdateDelayMS = 300;
+
 let gIsDiff = false;
 
+let gCurrentReports = [];
+let gCurrentHasMozMallocUsableSize = false;
+let gCurrentIsDiff = false;
+
+let gFilter = "";
+
 // ---------------------------------------------------------------------------
 
 // Forward slashes in URLs in paths are represented with backslashes to avoid
 // being mistaken for path separators.  Paths/names where this hasn't been
 // undone are prefixed with "unsafe"; the rest are prefixed with "safe".
 function flipBackslashes(aUnsafeStr) {
   // Save memory by only doing the replacement if it's necessary.
   return (!aUnsafeStr.includes("\\"))
@@ -106,16 +114,24 @@ function reportAssertionFailure(aMsg) {
   }
 }
 
 function debug(aVal) {
   let section = appendElement(document.body, "div", "section");
   appendElementWithText(section, "div", "debug", JSON.stringify(aVal));
 }
 
+function stringMatchesFilter(aString, aFilter) {
+  assert(typeof aFilter == "string" || aFilter instanceof RegExp,
+         "unexpected aFilter type");
+
+  return typeof aFilter == "string" ? aString.includes(aFilter)
+                                    : aFilter.test(aString);
+}
+
 // ---------------------------------------------------------------------------
 
 function onUnload() {
 }
 
 // ---------------------------------------------------------------------------
 
 // The <div> holding everything but the header and footer (if they're present).
@@ -188,33 +204,38 @@ function updateMainAndFooter(aMsg, aShow
 
 function appendTextNode(aP, aText) {
   let e = document.createTextNode(aText);
   aP.appendChild(e);
   return e;
 }
 
 function appendElement(aP, aTagName, aClassName) {
-  let e = document.createElement(aTagName);
-  if (aClassName) {
-    e.className = aClassName;
-  }
+  let e = newElement(aTagName, aClassName);
   aP.appendChild(e);
   return e;
 }
 
 function appendElementWithText(aP, aTagName, aClassName, aText) {
   let e = appendElement(aP, aTagName, aClassName);
   // Setting textContent clobbers existing children, but there are none.  More
   // importantly, it avoids creating a JS-land object for the node, saving
   // memory.
   e.textContent = aText;
   return e;
 }
 
+function newElement(aTagName, aClassName) {
+  let e = document.createElement(aTagName);
+  if (aClassName) {
+    e.className = aClassName;
+  }
+  return e;
+}
+
 // ---------------------------------------------------------------------------
 
 const explicitTreeDescription =
 "This tree covers explicit memory allocations by the application.  It includes \
 \n\n\
 * allocations made at the operating system level (via calls to functions such as \
 VirtualAlloc, vm_allocate, and mmap), \
 \n\n\
@@ -492,37 +513,42 @@ function dumpGCLogAndCCLog(aVerbose) {
 /**
  * Top-level function that does the work of generating the page from the memory
  * reporters.
  */
 function updateAboutMemoryFromReporters() {
   updateMainAndFooter("Measuring...", NO_TIMESTAMP, HIDE_FOOTER);
 
   try {
-    let processLiveMemoryReports =
-        function(aHandleReport, aDisplayReports) {
-      let handleReport = function(aProcess, aUnsafePath, aKind, aUnits,
-                                  aAmount, aDescription) {
-        aHandleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
-                      aDescription, /* presence = */ undefined);
-      };
+    gCurrentReports = [];
+    gCurrentHasMozMallocUsableSize = gMgr.hasMozMallocUsableSize;
+    gCurrentIsDiff = false;
+    gFilter = "";
 
-      let displayReportsAndFooter = function() {
-        updateTitleMainAndFooter("live measurement", "", NO_TIMESTAMP,
-                                 SHOW_FOOTER);
-        aDisplayReports();
-      };
-
-      gMgr.getReports(handleReport, null, displayReportsAndFooter, null,
-                      gAnonymize.checked);
+    // Record the reports from the live memory reporters then process them.
+    let handleReport = function(aProcess, aUnsafePath, aKind, aUnits,
+                                aAmount, aDescription) {
+      gCurrentReports.push({
+        process: aProcess,
+        path: aUnsafePath,
+        kind: aKind,
+        units: aUnits,
+        amount: aAmount,
+        description: aDescription,
+      });
     };
 
-    // Process the reports from the live memory reporters.
-    appendAboutMemoryMain(processLiveMemoryReports,
-                          gMgr.hasMozMallocUsableSize);
+    let displayReports = function() {
+      updateTitleMainAndFooter("live measurement", "", NO_TIMESTAMP,
+                               SHOW_FOOTER);
+      updateAboutMemoryFromCurrentData();
+    };
+
+    gMgr.getReports(handleReport, null, displayReports, null,
+                    gAnonymize.checked);
 
   } catch (ex) {
     handleException(ex);
   }
 }
 
 // Increment this if the JSON format changes.
 //
@@ -543,41 +569,56 @@ function parseAndUnwrapIfCrashDump(aStr)
     // It looks like a crash dump. The memory reports should be in the
     // |memory_report| property.
     obj = obj.memory_report;
   }
   return obj;
 }
 
 /**
+ * Populate about:memory using the data stored in gCurrentReports and
+ * gCurrentHasMozMallocUsableSize.
+ */
+function updateAboutMemoryFromCurrentData() {
+  function processCurrentMemoryReports(aHandleReport, aDisplayReports) {
+    for (let r of gCurrentReports) {
+      aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
+                    r.description, r._presence);
+    }
+    aDisplayReports();
+  }
+
+  gIsDiff = gCurrentIsDiff;
+  appendAboutMemoryMain(processCurrentMemoryReports, gFilter,
+                        gCurrentHasMozMallocUsableSize);
+  gIsDiff = false;
+}
+
+/**
  * Populate about:memory using the data in the given JSON object.
  *
  * @param aObj
  *        An object that (hopefully!) conforms to the JSON schema used by
  *        nsIMemoryInfoDumper.
  */
 function updateAboutMemoryFromJSONObject(aObj) {
   try {
     assertInput(aObj.version === gCurrentFileFormatVersion,
                 "data version number missing or doesn't match");
     assertInput(aObj.hasMozMallocUsableSize !== undefined,
                 "missing 'hasMozMallocUsableSize' property");
     assertInput(aObj.reports && aObj.reports instanceof Array,
                 "missing or non-array 'reports' property");
 
-    let processMemoryReportsFromFile =
-        function(aHandleReport, aDisplayReports) {
-      for (let r of aObj.reports) {
-        aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
-                      r.description, r._presence);
-      }
-      aDisplayReports();
-    };
-    appendAboutMemoryMain(processMemoryReportsFromFile,
-                          aObj.hasMozMallocUsableSize);
+    gCurrentReports = aObj.reports.concat();
+    gCurrentHasMozMallocUsableSize = aObj.hasMozMallocUsableSize;
+    gCurrentIsDiff = gIsDiff;
+    gFilter = "";
+
+    updateAboutMemoryFromCurrentData();
   } catch (ex) {
     handleException(ex);
   }
 }
 
 /**
  * Populate about:memory using the data in the given JSON string.
  *
@@ -924,36 +965,57 @@ function PColl() {
 
 /**
  * Processes reports (whether from reporters or from a file) and append the
  * main part of the page.
  *
  * @param aProcessReports
  *        Function that extracts the memory reports from the reporters or from
  *        file.
+ * @param aFilter
+ *        String or RegExp used to filter reports by their path.
  * @param aHasMozMallocUsableSize
  *        Boolean indicating if moz_malloc_usable_size works.
  */
-function appendAboutMemoryMain(aProcessReports, aHasMozMallocUsableSize) {
+function appendAboutMemoryMain(aProcessReports, aFilter,
+                               aHasMozMallocUsableSize) {
   let pcollsByProcess = {};
+  let infoByProcess = {};
 
   function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
                         aDescription, aPresence) {
     if (aUnsafePath.startsWith("explicit/")) {
       assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP,
                   "bad explicit kind");
       assertInput(aUnits === UNITS_BYTES, "bad explicit units");
     }
 
     assert(aPresence === undefined ||
            aPresence == DReport.PRESENT_IN_FIRST_ONLY ||
            aPresence == DReport.PRESENT_IN_SECOND_ONLY,
            "bad presence");
 
     let process = aProcess === "" ? gUnnamedProcessStr : aProcess;
+
+    // Store the "resident" value for each process, so that if we filter it
+    // out, we can still use it to correctly sort processes and generate the
+    // process index.
+    let info = infoByProcess[process];
+    if (!info) {
+      info = infoByProcess[process] = {};
+    }
+    if (aUnsafePath == "resident") {
+      infoByProcess[process].resident = aAmount;
+    }
+
+    // Ignore reports that don't match the current filter.
+    if (!stringMatchesFilter(aUnsafePath, aFilter)) {
+      return;
+    }
+
     let unsafeNames = aUnsafePath.split("/");
     let unsafeName0 = unsafeNames[0];
     let isDegenerate = unsafeNames.length === 1;
 
     // Get the PColl table for the process, creating it if necessary.
     let pcoll = pcollsByProcess[process];
     if (!pcollsByProcess[process]) {
       pcoll = pcollsByProcess[process] = new PColl();
@@ -1001,36 +1063,33 @@ function appendAboutMemoryMain(aProcessR
       if (aPresence !== undefined) {
         t._presence = aPresence;
       }
     }
   }
 
   function displayReports() {
     // Sort the processes.
-    let processes = Object.keys(pcollsByProcess);
+    let processes = Object.keys(infoByProcess);
     processes.sort(function(aProcessA, aProcessB) {
       assert(aProcessA != aProcessB,
              `Elements of Object.keys() should be unique, but ` +
              `saw duplicate '${aProcessA}' elem.`);
 
       // Always put the main process first.
       if (aProcessA == gUnnamedProcessStr) {
         return -1;
       }
       if (aProcessB == gUnnamedProcessStr) {
         return 1;
       }
 
       // Then sort by resident size.
-      let nodeA = pcollsByProcess[aProcessA]._degenerates.resident;
-      let nodeB = pcollsByProcess[aProcessB]._degenerates.resident;
-      let residentA = nodeA ? nodeA._amount : -1;
-      let residentB = nodeB ? nodeB._amount : -1;
-
+      let residentA = infoByProcess[aProcessA].resident || -1;
+      let residentB = infoByProcess[aProcessB].resident || -1;
       if (residentA > residentB) {
         return -1;
       }
       if (residentA < residentB) {
         return 1;
       }
 
       // Then sort by process name.
@@ -1039,41 +1098,129 @@ function appendAboutMemoryMain(aProcessR
       }
       if (aProcessA > aProcessB) {
         return 1;
       }
 
       return 0;
     });
 
-    // Set up a layout with a left (main) column to contain the process sections
-    // and a right (sidebar) column to contain the process index.
-    let outputContainer = appendElement(gMain, "div", "outputContainer");
+    // We set up this general layout inside gMain:
+    //
+    //   <div class="outputContainer">
+    //     <div class="sections"></div>
+    //     <div class="sidebar">
+    //       <div class="sidebarContents">
+    //         <div class="sidebarItem filterItem"></div>
+    //         <div class="sidebarItem indexItem"></div>
+    //       </div>
+    //     </div>
+    //   </div>
+    //
+    // If we detect that outputContainer already exists, then this is an update
+    // (due to typing in a filter string) to an already-displayed memory report.
+    // In this case we preserve the structure of the layout and only replace
+    // div.sections and #indexItem. Preserving the filter sidebar item means we
+    // preserve any editing state in its <input>.
+
+    // Generate the main process sections.
+    let sections = newElement("div", "sections");
+
+    for (let [i, process] of processes.entries()) {
+      let pcolls = pcollsByProcess[process];
+      if (!pcolls) {
+        continue;
+      }
+
+      let section = appendElement(sections, "div", "section");
+      appendProcessAboutMemoryElements(section, i, process,
+                                       pcolls._trees,
+                                       pcolls._degenerates,
+                                       pcolls._heapTotal,
+                                       aHasMozMallocUsableSize,
+                                       aFilter != "");
+    }
 
-    let sections = appendElement(outputContainer, "div", "sections");
+    if (!sections.firstChild) {
+      appendElementWithText(sections, "div", "section", "No results found.");
+    }
+
+    // Generate the process index.
+    let indexItem = newElement("div", "sidebarItem");
+    indexItem.classList.add("indexItem");
+    appendElementWithText(indexItem, "div", "sidebarLabel", "Process index");
+    let indexList = appendElement(indexItem, "ul", "index");
+
+    for (let [i, process] of processes.entries()) {
+      let indexListItem = appendElement(indexList, "li");
+      let pcolls = pcollsByProcess[process];
+      if (pcolls) {
+        let indexLink = appendElementWithText(indexListItem, "a", "", process);
+        indexLink.href = "#start" + i;
+      } else {
+        // We've filtered out all reports from this process. Generate a non-link
+        // entry in the process index, and skip creating a process report
+        // section.
+        indexListItem.textContent = process;
+      }
+    }
+
+    // If we are updating, just swap in the new process output.
+    let outputContainer = gMain.querySelector(".outputContainer");
+    if (outputContainer) {
+      outputContainer.querySelector(".sections").replaceWith(sections);
+      outputContainer.querySelector(".indexItem").replaceWith(indexItem);
+      return;
+    }
+
+    // Otherwise, generate the rest of the layout.
+    outputContainer = appendElement(gMain, "div", "outputContainer");
+    outputContainer.appendChild(sections);
+
     let sidebar = appendElement(outputContainer, "div", "sidebar");
     let sidebarContents = appendElement(sidebar, "div", "sidebarContents");
 
-    let index = appendElement(sidebarContents, "div", "index");
-    appendElementWithText(index, "div", "indexLabel", "Process index");
-    let indexList = appendElement(index, "ul", "indexList");
+    // Generate the filter input and checkbox.
+    let filterItem = appendElement(sidebarContents, "div", "sidebarItem");
+    filterItem.classList.add("filterItem");
+    appendElementWithText(filterItem, "div", "sidebarLabel", "Filter");
+
+    let filterInput = appendElement(filterItem, "input", "filterInput");
+    filterInput.placeholder = "Memory report path filter";
+
+    let filterOptions = appendElement(filterItem, "div");
+    let filterRegExLabel = appendElement(filterOptions, "label");
+    let filterRegExCheckbox = appendElement(filterRegExLabel, "input");
+    filterRegExCheckbox.type = "checkbox";
+    filterRegExLabel.append(" Regular expression");
 
-    // Generate output and an index link for each process.
-    for (let [i, process] of processes.entries()) {
-      let section = appendElement(sections, "div", "section");
-      appendProcessAboutMemoryElements(section, i, process,
-                                       pcollsByProcess[process]._trees,
-                                       pcollsByProcess[process]._degenerates,
-                                       pcollsByProcess[process]._heapTotal,
-                                       aHasMozMallocUsableSize);
+    // Set up event handlers to update the display if the filter input or
+    // checkbox changes.
+    let filterUpdateTimeout;
+    let filterUpdate = function() {
+      if (filterUpdateTimeout) {
+        window.clearTimeout(filterUpdateTimeout);
+      }
+      filterUpdateTimeout = window.setTimeout(function() {
+        try {
+          gFilter = filterRegExCheckbox.checked && filterInput.value != ""
+                      ? new RegExp(filterInput.value)
+                      : filterInput.value;
+        } catch (ex) {
+          // Match nothing if the regex was invalid.
+          gFilter = new RegExp("^$");
+        }
+        updateAboutMemoryFromCurrentData();
+      }, gFilterUpdateDelayMS);
+    };
+    filterInput.oninput = filterUpdate;
+    filterRegExCheckbox.onchange = filterUpdate;
 
-      let indexListItem = appendElement(indexList, "li");
-      let indexLink = appendElementWithText(indexListItem, "a", "", process);
-      indexLink.href = "#start" + i;
-    }
+    // Append the process list item after the filter item.
+    sidebarContents.appendChild(indexItem);
   }
 
   aProcessReports(handleReport, displayReports);
 }
 
 // ---------------------------------------------------------------------------
 
 // There are two kinds of TreeNode.
@@ -1362,31 +1509,34 @@ function sortTreeAndInsertAggregateNodes
 }
 
 // Global variable indicating if we've seen any invalid values for this
 // process;  it holds the unsafePaths of any such reports.  It is reset for
 // each new process.
 let gUnsafePathsWithInvalidValuesForThisProcess = [];
 
 function appendWarningElements(aP, aHasKnownHeapAllocated,
-                               aHasMozMallocUsableSize) {
-  if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) {
+                               aHasMozMallocUsableSize,
+                               aFiltered) {
+  // These warnings may not make sense if the reporters they reference have been
+  // filtered out, so just skip them if we have a filter applied.
+  if (!aFiltered && !aHasKnownHeapAllocated && !aHasMozMallocUsableSize) {
     appendElementWithText(aP, "p", "",
       "WARNING: the 'heap-allocated' memory reporter and the " +
       "moz_malloc_usable_size() function do not work for this platform " +
       "and/or configuration.  This means that 'heap-unclassified' is not " +
       "shown and the 'explicit' tree shows much less memory than it should.\n\n");
 
-  } else if (!aHasKnownHeapAllocated) {
+  } else if (!aFiltered && !aHasKnownHeapAllocated) {
     appendElementWithText(aP, "p", "",
       "WARNING: the 'heap-allocated' memory reporter does not work for this " +
       "platform and/or configuration. This means that 'heap-unclassified' " +
       "is not shown and the 'explicit' tree shows less memory than it should.\n\n");
 
-  } else if (!aHasMozMallocUsableSize) {
+  } else if (!aFiltered && !aHasMozMallocUsableSize) {
     appendElementWithText(aP, "p", "",
       "WARNING: the moz_malloc_usable_size() function does not work for " +
       "this platform and/or configuration.  This means that much of the " +
       "heap-allocated memory is not measured by individual memory reporters " +
       "and so will fall under 'heap-unclassified'.\n\n");
   }
 
   if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) {
@@ -1420,21 +1570,24 @@ function appendWarningElements(aP, aHasK
  * @param aProcess
  *        The name of the process.
  * @param aTrees
  *        The table of non-degenerate trees for this process.
  * @param aDegenerates
  *        The table of degenerate trees for this process.
  * @param aHasMozMallocUsableSize
  *        Boolean indicating if moz_malloc_usable_size works.
+ * @param aFiltered
+ *        Boolean indicating whether the reports were filtered.
  * @return The generated text.
  */
 function appendProcessAboutMemoryElements(aP, aN, aProcess, aTrees,
                                           aDegenerates, aHeapTotal,
-                                          aHasMozMallocUsableSize) {
+                                          aHasMozMallocUsableSize,
+                                          aFiltered) {
   let appendLink = function(aHere, aThere, aArrow) {
     let link = appendElementWithText(aP, "a", "upDownArrow", aArrow);
     link.href = "#" + aThere + aN;
     link.id = aHere + aN;
     link.title = `Go to the ${aThere} of ${aProcess}`;
     link.style = "text-decoration: none";
 
     // This gives nice spacing when we copy and paste.
@@ -1513,17 +1666,17 @@ function appendProcessAboutMemoryElement
     appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
   }
 
   // Add any warnings about inaccuracies in the "explicit" tree due to platform
   // limitations.  These must be computed after generating all the text.  The
   // newlines give nice spacing if we copy+paste into a text buffer.
   if (hasExplicitTree) {
     appendWarningElements(warningsDiv, hasKnownHeapAllocated,
-                          aHasMozMallocUsableSize);
+                          aHasMozMallocUsableSize, aFiltered);
   }
 
   appendElementWithText(aP, "h3", "", "End of " + aProcess);
   appendLink("end", "start", "↑");
 }
 
 // Used for UNITS_BYTES values that are printed as MiB.
 const kMBStyle = {
--- a/toolkit/components/aboutmemory/tests/chrome.ini
+++ b/toolkit/components/aboutmemory/tests/chrome.ini
@@ -17,13 +17,15 @@ subsuite = clipboard
 [test_aboutmemory3.xul]
 subsuite = clipboard
 [test_aboutmemory4.xul]
 subsuite = clipboard
 [test_aboutmemory5.xul]
 subsuite = clipboard
 skip-if = asan # Bug 1116230
 [test_aboutmemory6.xul]
+[test_aboutmemory7.xul]
+subsuite = clipboard
 [test_memoryReporters.xul]
 [test_memoryReporters2.xul]
 [test_sqliteMultiReporter.xul]
 [test_dumpGCAndCCLogsToFile.xul]
 skip-if = (verify && debug && (os == 'mac'))
new file mode 100644
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory7.xul
@@ -0,0 +1,217 @@
+<?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:memory"
+        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>
+
+  <!-- This file tests filtering in about:memory. -->
+
+  <!-- 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">
+  <![CDATA[
+  "use strict";
+
+  let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+            getService(Ci.nsIMemoryReporterManager);
+
+  // Hide all the real reporters;  we'll restore them at the end.
+  mgr.blockRegistrationAndHideExistingReporters();
+
+  // Setup various fake-but-deterministic reporters.
+  const KB = 1024;
+  const MB = KB * KB;
+  const HEAP  = Ci.nsIMemoryReporter.KIND_HEAP;
+  const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+  const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+
+  let fakeReporters = [
+    { collectReports: function(aCbObj, aClosure, aAnonymize) {
+        function f(aP, aK, aA) {
+          aCbObj.callback("", aP, aK, BYTES, aA, "Desc.", aClosure);
+        }
+        f("heap-allocated",     OTHER,   250 * MB);
+        f("explicit/a/b",       HEAP,     50 * MB);
+        f("explicit/a/c/d",     HEAP,     25 * MB);
+        f("explicit/a/c/e",     HEAP,     15 * MB);
+        f("explicit/a/f",       HEAP,     30 * MB);
+        f("explicit/g",         HEAP,    100 * MB);
+        f("explicit/h/i",       HEAP,     10 * MB);
+        f("explicit/h/i2",      HEAP,      9 * MB);
+        f("explicit/j/k",       HEAP,    0.5 * MB);
+        f("explicit/j/k2",      HEAP,    0.3 * MB);
+        f("explicit/a/l/m",     HEAP,    0.1 * MB);
+        f("explicit/a/l/n",     HEAP,    0.1 * MB);
+      }
+    }
+  ];
+
+  for (let i = 0; i < fakeReporters.length; i++) {
+    mgr.registerStrongReporterEvenIfBlocked(fakeReporters[i]);
+  }
+
+  ]]>
+  </script>
+
+  <iframe id="amFrame"  height="500" src="about:memory"></iframe>
+
+  <script type="application/javascript">
+  <![CDATA[
+  function finish()
+  {
+    mgr.unblockRegistrationAndRestoreOriginalReporters();
+    SimpleTest.finish();
+  }
+
+  // Click on the identified element, then cut+paste the entire page and
+  // check that the cut text matches what we expect.
+  function testClick(aId, aExpected, aNext) {
+    let win = document.getElementById("amFrame").contentWindow;
+
+    win.document.getElementById(aId).click();
+
+    testClipboard(aExpected, aNext, 0);
+  }
+
+  // Apply the specified filter, then cut+paste the entire page and
+  // check that the cut text matches what we expect.
+  function testFilter(aFilterString, aRegEx, aExpected, aNext) {
+    let win = document.getElementById("amFrame").contentWindow;
+
+    let filterInput = win.document.querySelector(".filterInput");
+    let filterRegExCheckbox =
+      win.document.querySelector(".filterInput + * input[type=checkbox]");
+
+    filterInput.value = aFilterString;
+    filterRegExCheckbox.checked = aRegEx;
+
+    // Dispatch a synthetic input event, since assigning to .value above
+    // doesn't trigger this.
+    filterInput.dispatchEvent(new win.Event("input"));
+
+    // about:memory delays 300 ms before applying the filter, so we wait a
+    // a bit longer than that before checking the clipboard.
+    testClipboard(aExpected, aNext, /* delay */ 600);
+  }
+
+  function testClipboard(aExpected, aNext, aDelay) {
+    setTimeout(function() {
+      let mostRecentActual;
+      document.getElementById("amFrame").focus();
+      SimpleTest.waitForClipboard(
+        function(aActual) {
+          mostRecentActual = aActual;
+          let rslt = aActual.trim() === aExpected.trim();
+          if (!rslt) {
+            // Try copying again.
+            synthesizeKey("A", {accelKey: true});
+            synthesizeKey("C", {accelKey: true});
+          }
+
+          return rslt;
+        },
+        function() {
+          synthesizeKey("A", {accelKey: true});
+          synthesizeKey("C", {accelKey: true});
+        },
+        aNext,
+        function() {
+          ok(false, "pasted text doesn't match");
+          dump("******EXPECTED******\n");
+          dump(aExpected);
+          dump("*******ACTUAL*******\n");
+          dump(mostRecentActual);
+          dump("********************\n");
+          finish();
+        }
+      );
+    }, aDelay);
+  }
+
+  // Returns a function that chains together one test() call per id.
+  function chain(aIds) {
+    let x = aIds.shift();
+    if (x) {
+      if (x.click) {
+        return function() { testClick(x.click, x.expected, chain(aIds)); }
+      } else {
+        return function() { testFilter(x.filter, x.regex, x.expected, chain(aIds)); }
+      }
+    } else {
+      return function() { finish(); };
+    }
+  }
+
+  let startExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│  ├───50.00 MB (20.00%) ── b\n\
+│  ├───40.00 MB (16.00%) -- c\n\
+│  │   ├──25.00 MB (10.00%) ── d\n\
+│  │   └──15.00 MB (06.00%) ── e\n\
+│  ├───30.00 MB (12.00%) ── f\n\
+│  └────0.20 MB (00.08%) ++ l\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- h\n\
+│   ├──10.00 MB (04.00%) ── i\n\
+│   └───9.00 MB (03.60%) ── i2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+  let acFilterExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+40.00 MB (100.0%) -- explicit\n\
+└──40.00 MB (100.0%) -- a/c\n\
+   ├──25.00 MB (62.50%) ── d\n\
+   └──15.00 MB (37.50%) ── e\n\
+\n\
+End of Main Process\n\
+";
+
+  let hjFilterExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+19.80 MB (100.0%) -- explicit\n\
+├──19.00 MB (95.96%) -- h\n\
+│  ├──10.00 MB (50.51%) ── i\n\
+│  └───9.00 MB (45.45%) ── i2\n\
+└───0.80 MB (04.04%) -- j\n\
+    ├──0.50 MB (02.53%) ── k\n\
+    └──0.30 MB (01.52%) ── k2\n\
+\n\
+End of Main Process\n\
+";
+
+  let filtersToApplyOrIdsToClick = [
+    { click: "measureButton",               expected: startExpected },
+    { filter: "a/c",          regex: false, expected: acFilterExpected },
+    { filter: "/[hj]",        regex: false, expected: "No results found." },
+    { filter: "/[hj]",        regex: true,  expected: hjFilterExpected },
+  ];
+
+  SimpleTest.waitForFocus(chain(filtersToApplyOrIdsToClick));
+
+  SimpleTest.waitForExplicitFinish();
+  ]]>
+  </script>
+</window>