Bug 719335 - Allow arbitrary collapsing/expanding of sub-trees in about:memory. r=jlebar.
authorNicholas Nethercote <nnethercote@mozilla.com>
Thu, 26 Jan 2012 14:02:49 -0800
changeset 86756 3116d5b836873bc0a20aed2e9a4c389f0242390d
parent 86755 87a57afa0226686468473127d9768ab167f1054b
child 86757 a02b6f797e023fc3dc8f7399c8d077b38934b4ac
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlebar
bugs719335
milestone12.0a1
Bug 719335 - Allow arbitrary collapsing/expanding of sub-trees in about:memory. r=jlebar.
toolkit/components/aboutmemory/content/aboutMemory.css
toolkit/components/aboutmemory/content/aboutMemory.js
toolkit/components/aboutmemory/tests/Makefile.in
toolkit/components/aboutmemory/tests/test_aboutmemory.xul
toolkit/components/aboutmemory/tests/test_aboutmemory2.xul
--- a/toolkit/components/aboutmemory/content/aboutMemory.css
+++ b/toolkit/components/aboutmemory/content/aboutMemory.css
@@ -30,65 +30,71 @@
  * 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 ***** */
 
+body.verbose {
+  /* override setting in about.css */
+  max-width: 100% !important;
+}
+
+body.non-verbose pre.tree {
+  overflow-x: hidden;
+  text-overflow: ellipsis;
+}
+
+.sectionHeader {
+  background: #ddd;
+  padding-left: .1em;
+}
+
 .accuracyWarning {
   color: #f00;
 }
 
+.treeLine {
+  color: #888;
+}
+
 .mrValue {
   font-weight: bold;
   color: #400;
 }
 
 .mrPerc {
 }
 
+.mrSep {
+}
+
 .mrName {
   color: #004;
 }
 
-.hasDesc:hover {
-  text-decoration: underline;
-}
-
 .mrStar {
   color: #604;
 }
 
-.treeLine {
-  color: #888;
+.hasKids {
+  cursor: pointer;
+}
+
+.hasKids:hover {
+  text-decoration: underline;
 }
 
 .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 */
 }
 
-body.verbose {
-  /* override setting in about.css */
-  max-width: 100% !important;
-}
-
-h2.tree {
-  cursor: pointer;
-  background: #ddd;
-  padding-left: .1em;
-}
-
-body.non-verbose pre.tree {
-  overflow-x: hidden;
-  text-overflow: ellipsis;
-}
-
-pre.collapsed {
+.hidden {
   display: none;
 }
--- a/toolkit/components/aboutmemory/content/aboutMemory.js
+++ b/toolkit/components/aboutmemory/content/aboutMemory.js
@@ -194,27 +194,16 @@ function sendHeapMinNotifications()
     else
       runSoon(update);
   }
 
   var j = 0;
   sendHeapMinNotificationsInner();
 }
 
-function toggleTreeVisibility(aEvent)
-{
-  var headerElem = aEvent.target;
-
-  // Replace "header-" with "pre-" in the header element's id to get the id of
-  // the corresponding pre element.
-  var treeElem = $(headerElem.id.replace(/^header-/, 'pre-'));
-
-  treeElem.classList.toggle('collapsed');
-}
-
 function Reporter(aPath, aKind, aUnits, aAmount, aDescription)
 {
   this._path        = aPath;
   this._kind        = aKind;
   this._units       = aUnits;
   this._amount      = aAmount;
   this._description = aDescription;
   // this._nMerged is only defined if > 1
@@ -332,25 +321,28 @@ function update()
   for (var process in reportersByProcess) {
     if (process !== "Main") {
       text += genProcessText(process, reportersByProcess[process],
                              hasMozMallocUsableSize);
     }
   }
 
   // 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.";
 
+  // The "Update" button has an id so it can be clicked in a test.
   text += "<div>" +
+    "<button title='" + UpDesc + "' onclick='update()' id='updateButton'>Update</button>" +
     "<button title='" + GCDesc + "' onclick='doGlobalGC()'>GC</button>" +
     "<button title='" + CCDesc + "' onclick='doCC()'>CC</button>" +
     "<button title='" + MPDesc + "' onclick='sendHeapMinNotifications()'>" + "Minimize memory usage</button>" +
     "</div>";
 
   // Generate verbosity option link at the bottom.
   text += "<div>";
   text += gVerbose
@@ -358,43 +350,48 @@ function update()
         : "<span class='option'><a href='about:memory?verbose'>More verbose</a></span>";
   text += "</div>";
 
   text += "<div>" +
           "<span class='option'><a href='about:support'>Troubleshooting information</a></span>" +
           "</div>";
 
   text += "<div>" +
+          "<span class='legend'>Click on a non-leaf node in a tree to expand ('++') " +
+          "or collapse ('--') its children.</span>" +
+          "</div>";
+  text += "<div>" +
           "<span class='legend'>Hover the pointer over the name of a memory " +
-          "reporter to see a detailed description of what it measures. Click a " +
-          "heading to expand or collapse its tree.</span>" +
-          "</div>";
+          "reporter to see a description of what it measures.</span>";
 
   var div = document.createElement("div");
   div.innerHTML = text;
   content.appendChild(div);
 }
 
 // There are two kinds of TreeNode.
-// - Leaf TreeNodes correspond to Reporters and have more properties.  
+// - Leaf TreeNodes correspond to Reporters and have more properties.
 // - Non-leaf TreeNodes are just scaffolding nodes for the tree;  their values
 //   are derived from their children.
 function TreeNode(aName)
 {
   // Nb: _units is not needed, it's always UNITS_BYTES.
   this._name = aName;
   this._kids = [];
   // All TreeNodes have these properties added later:
   // - _amount (which is never |kUnknown|)
   // - _description
   //
   // Leaf TreeNodes have these properties added later:
   // - _kind
   // - _nMerged (if > 1)
   // - _hasProblem (only defined if true)
+  //
+  // Non-leaf TreeNodes have these properties added later:
+  // - _hideKids (only defined if true)
 }
 
 TreeNode.prototype = {
   findKid: function(aName) {
     for (var i = 0; i < this._kids.length; i++) {
       if (this._kids[i]._name === aName) {
         return this._kids[i];
       }
@@ -520,16 +517,35 @@ function buildTree(aReporters, aTreeName
 
   // Set the description on the root node.
   t._description = kTreeDescriptions[t._name];
 
   return t;
 }
 
 /**
+ * Ignore all the memory reporters that belong to a tree;  this involves
+ * explicitly marking them as done.
+ *
+ * @param aReporters
+ *        The table of Reporters, indexed by path.
+ * @param aTreeName
+ *        The name of the tree being built.
+ */
+function ignoreTree(aReporters, aTreeName)
+{
+  for (var path in aReporters) {
+    var r = aReporters[path];
+    if (r.treeNameMatches(aTreeName)) {
+      var dummy = getBytes(aReporters, path);
+    }
+  }
+}
+
+/**
  * Do some work which only makes sense for the 'explicit' tree.
  *
  * @param aT
  *        The tree.
  * @param aReporters
  *        Table of Reporters for this process, indexed by _path.
  * @return A boolean indicating if "heap-allocated" is known for the process.
  */
@@ -576,68 +592,91 @@ function fixUpExplicitTree(aT, aReporter
 
   aT._kids.push(heapUnclassifiedT);
   aT._amount += heapUnclassifiedT._amount;
 
   return hasKnownHeapAllocated;
 }
 
 /**
- * Sort all kid nodes from largest to smallest and aggregate insignificant
- * nodes.
+ * Sort all kid nodes from largest to smallest, and insert aggregate nodes
+ * where appropriate.
  *
  * @param aTotalBytes
  *        The size of the tree's root node.
  * @param aT
  *        The tree.
  */
-function filterTree(aTotalBytes, aT)
+function sortTreeAndInsertAggregateNodes(aTotalBytes, aT)
 {
-  const omitThresholdPerc = 0.5; /* percent */
+  const kSignificanceThresholdPerc = 1;
 
-  function shouldOmit(aBytes)
+  function isInsignificant(aT)
   {
     return !gVerbose &&
            aTotalBytes !== kUnknown &&
-           (100 * aBytes / aTotalBytes) < omitThresholdPerc;
+           (100 * aT._amount / aTotalBytes) < kSignificanceThresholdPerc;
+  }
+
+  if (aT._kids.length === 0) {
+    return;
   }
 
   aT._kids.sort(TreeNode.compare);
 
-  for (var i = 0; i < aT._kids.length; i++) {
-    if (shouldOmit(aT._kids[i]._amount)) {
-      // This sub-tree is below the significance threshold
-      // Remove it and all remaining (smaller) sub-trees, and
-      // replace them with a single aggregate node.
+  // If the first child is insignificant, they all are, and there's no point
+  // creating an aggregate node that lacks siblings.  Just set the parent's
+  // _hideKids property and process all children.
+  if (isInsignificant(aT._kids[0])) {
+    aT._hideKids = true;
+    for (var i = 0; i < aT._kids.length; i++) {
+      sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
+    }
+    return;
+  }
+
+  // Look at all children except the last one.
+  for (var i = 0; i < aT._kids.length - 1; i++) {
+    if (isInsignificant(aT._kids[i])) {
+      // This child is below the significance threshold.  If there are other
+      // (smaller) children remaining, move them under an aggregate node.
       var i0 = i;
+      var nAgg = aT._kids.length - i0;
+      // Create an aggregate node.
+      var aggT = new TreeNode("(" + nAgg + " tiny)");
       var aggBytes = 0;
       for ( ; i < aT._kids.length; i++) {
         aggBytes += aT._kids[i]._amount;
+        aggT._kids.push(aT._kids[i]);
       }
-      aT._kids.splice(i0, aT._kids.length);
-      var n = i - i0;
-      var rSub = new TreeNode("(" + n + " omitted)");
-      rSub._amount = aggBytes;
-      rSub._description =
-        n + " sub-trees that were below the " + omitThresholdPerc +
-        "% significance threshold.  Click 'More verbose' at the bottom of " +
-        "this page to see them.";
+      aggT._hideKids = true;
+      aggT._amount = aggBytes;
+      aggT._description =
+        nAgg + " sub-trees that are below the " + kSignificanceThresholdPerc +
+        "% significance threshold.";
+      aT._kids.splice(i0, nAgg, aggT);
+      aT._kids.sort(TreeNode.compare);
 
-      // Add the "omitted" sub-tree at the end and then re-sort, because the
-      // sum of the omitted sub-trees may be larger than some of the shown
-      // sub-trees.
-      aT._kids[i0] = rSub;
-      aT._kids.sort(TreeNode.compare);
-      break;
+      // Process the moved children.
+      for (i = 0; i < aggT._kids.length; i++) {
+        sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]);
+      }
+      return;
     }
-    filterTree(aTotalBytes, aT._kids[i]);
+
+    sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
   }
+
+  // The first n-1 children were significant.  Don't consider if the last child
+  // is significant;  there's no point creating an aggregate node that only has
+  // one child.  Just process it.
+  sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
 }
 
-function genWarningText(aHasKnownHeapAllocated, aHasMozMallocUsableSize) 
+function genWarningText(aHasKnownHeapAllocated, aHasMozMallocUsableSize)
 {
   var warningText = "";
 
   if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) {
     warningText =
       "<p class='accuracyWarning'>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 " +
@@ -672,34 +711,40 @@ function genWarningText(aHasKnownHeapAll
  * @param aHasMozMallocUsableSize
  *        Boolean indicating if moz_malloc_usable_size works.
  * @return The generated text.
  */
 function genProcessText(aProcess, aReporters, aHasMozMallocUsableSize)
 {
   var explicitTree = buildTree(aReporters, 'explicit');
   var hasKnownHeapAllocated = fixUpExplicitTree(explicitTree, aReporters);
-  filterTree(explicitTree._amount, explicitTree);
+  sortTreeAndInsertAggregateNodes(explicitTree._amount, explicitTree);
   var explicitText = genTreeText(explicitTree, aProcess);
 
   // Generate any warnings about inaccuracies due to platform limitations.
   // The newlines give nice spacing if we cut+paste into a text buffer.
   var warningText = "";
   var accuracyTagText = "<p class='accuracyWarning'>";
   var warningText =
         genWarningText(hasKnownHeapAllocated, aHasMozMallocUsableSize);
 
-  var mapTreeText = '';
+  // We only show these breakdown trees in verbose mode.
+  var mapTreeText = "";
   kMapTreePaths.forEach(function(t) {
-    var tree = buildTree(aReporters, t);
+    if (gVerbose) {
+      var tree = buildTree(aReporters, t);
 
-    // |tree| will be null if we don't have any reporters for the given path.
-    if (tree) {
-      filterTree(tree._amount, tree);
-      mapTreeText += genTreeText(tree, aProcess);
+      // |tree| will be null if we don't have any reporters for the given path.
+      if (tree) {
+        sortTreeAndInsertAggregateNodes(tree._amount, tree);
+        tree._hideKids = true;   // map trees are always initially collapsed
+        mapTreeText += genTreeText(tree, aProcess);
+      }
+    } else {
+      ignoreTree(aReporters, t);
     }
   });
 
   // We have to call genOtherText after we process all the trees, because it
   // looks at all the reporters which aren't part of a tree.
   var otherText = genOtherText(aReporters, aProcess);
 
   // The newlines give nice spacing if we cut+paste into a text buffer.
@@ -830,19 +875,28 @@ function getBytes(aReporters, aPath, aDo
  */
 function getDescription(aReporters, aPath)
 {
   var r = aReporters[aPath];
   assert(r, "getDescription: no such Reporter: " + aPath);
   return r._description;
 }
 
+// There's a subset of the Unicode "light" box-drawing chars that are widely
+// implemented in terminals, and this code sticks to that subset to maximize
+// the chance that cutting and pasting about:memory output to a terminal will
+// work correctly:
+const kHorizontal       = "\u2500",
+      kVertical         = "\u2502",
+      kUpAndRight       = "\u2514",
+      kVerticalAndRight = "\u251c";
+
 function genMrValueText(aValue)
 {
-  return "<span class='mrValue'>" + aValue + "</span>";
+  return "<span class='mrValue'>" + aValue + " </span>";
 }
 
 function kindToString(aKind)
 {
   switch (aKind) {
    case KIND_NONHEAP: return "(Non-heap) ";
    case KIND_HEAP:    return "(Heap) ";
    case KIND_OTHER:
@@ -856,51 +910,112 @@ function escapeAll(aStr)
 {
   return aStr.replace(/\&/g, '&amp;').replace(/'/g, '&#39;').
               replace(/\</g, '&lt;').replace(/>/g, '&gt;').
               replace(/\"/g, '&quot;');
 }
 
 // Compartment reporter names are URLs and so can include forward slashes.  But
 // forward slash is the memory reporter path separator.  So the memory
-// reporters change them to backslashes.  Undo that here.  
+// reporters change them to backslashes.  Undo that here.
 function flipBackslashes(aStr)
 {
   return aStr.replace(/\\/g, '/');
 }
 
 function prepName(aStr)
 {
   return escapeAll(flipBackslashes(aStr));
 }
 
 function prepDesc(aStr)
 {
   return escapeAll(flipBackslashes(aStr));
 }
 
-function genMrNameText(aKind, aDesc, aName, aHasProblem, aNMerged)
+function genMrNameText(aKind, aShowSubtrees, aHasKids, aDesc, aName,
+                       aHasProblem, aNMerged)
 {
-  var text = "-- <span class='mrName hasDesc' title='" +
-             kindToString(aKind) + prepDesc(aDesc) +
-             "'>" + prepName(aName) + "</span>";
+  var text = "";
+  if (aHasKids) {
+    if (aShowSubtrees) {
+      text += "<span class='mrSep hidden'>++ </span>";
+      text += "<span class='mrSep'>-- </span>";
+    } else {
+      text += "<span class='mrSep'>++ </span>";
+      text += "<span class='mrSep hidden'>-- </span>";
+    }
+  } else {
+    text += "<span class='mrSep'>" + kHorizontal + kHorizontal + " </span>";
+  }
+  text += "<span class='mrName' title='" +
+          kindToString(aKind) + prepDesc(aDesc) + "'>" +
+          prepName(aName) + "</span>";
   if (aHasProblem) {
     const problemDesc =
       "Warning: this memory reporter was unable to compute a useful value. ";
-    text += " <span class='mrStar' title=\"" + problemDesc + "\">[*]</span>";
+    text += "<span class='mrStar' title=\"" + problemDesc + "\"> [*]</span>";
   }
   if (aNMerged) {
     const dupDesc = "This value is the sum of " + aNMerged +
                     " memory reporters that all have the same path.";
-    text += " <span class='mrStar' title=\"" + dupDesc + "\">[" + 
+    text += "<span class='mrStar' title=\"" + dupDesc + "\"> [" +
             aNMerged + "]</span>";
   }
   return text + '\n';
 }
 
+// This is used to record which sub-trees have been toggled, so the
+// collapsed/expanded state can be replicated when the page is regenerated.
+// It can end up holding IDs of nodes that no longer exist, e.g. for
+// compartments that have been closed.  This doesn't seem like a big deal,
+// because the number is limited by the number of entries the user has changed
+// from their original state.
+var gToggles = {};
+
+function toggle(aEvent)
+{
+  // This relies on each line being a span that contains at least five spans:
+  // mrValue, mrPerc, mrSep ('++'), mrSep ('--'), mrName, and then zero or more
+  // mrStars.  All whitespace must be within one of these spans for this
+  // function to find the right nodes.  And the span containing the children of
+  // this line must immediately follow.  Assertions check this.
+
+  function assertClassName(span, className) {
+    assert(span, "undefined " + className);
+    assert(span.nodeName === "span", "non-span " + className);
+    assert(span.classList.contains(className), "bad " + className);
+  }
+
+  // |aEvent.target| will be one of the five spans.  Get the outer span.
+  var outerSpan = aEvent.target.parentNode;
+  assertClassName(outerSpan, "hasKids");
+
+  // Toggle visibility of the '++' and '--' separators.
+  var plusSpan  = outerSpan.childNodes[2];
+  var minusSpan = outerSpan.childNodes[3];
+  assertClassName(plusSpan,  "mrSep");
+  assertClassName(minusSpan, "mrSep");
+  plusSpan .classList.toggle("hidden");
+  minusSpan.classList.toggle("hidden");
+
+  // Toggle visibility of the span containing this node's children.
+  var subTreeSpan = outerSpan.nextSibling;
+  assertClassName(subTreeSpan, "kids");
+  subTreeSpan.classList.toggle("hidden");
+
+  // Record/unrecord that this sub-tree was toggled.
+  var treeId = outerSpan.id;
+  if (gToggles[treeId]) {
+    delete gToggles[treeId];
+  } else {
+    gToggles[treeId] = true;
+  }
+}
+
 /**
  * Generates the text for the tree, including its heading.
  *
  * @param aT
  *        The tree.
  * @param aProcess
  *        The process the tree corresponds to.
  * @return The generated text.
@@ -909,103 +1024,122 @@ function genTreeText(aT, aProcess)
 {
   var treeBytes = aT._amount;
   var rootStringLength = aT.toString().length;
   var isExplicitTree = aT._name == 'explicit';
 
   /**
    * Generates the text for a particular tree, without a heading.
    *
+   * @param aPrePath
+   *        The partial path leading up to this node.
    * @param aT
    *        The tree.
    * @param aIndentGuide
    *        Records what indentation is required for this tree.  It has one
    *        entry per level of indentation.  For each entry, ._isLastKid
    *        records whether the node in question is the last child, and
    *        ._depth records how many chars of indentation are required.
    * @param aParentStringLength
    *        The length of the formatted byte count of the top node in the tree.
    * @return The generated text.
    */
-  function genTreeText2(aT, aIndentGuide, aParentStringLength)
+  function genTreeText2(aPrePath, aT, aIndentGuide, aParentStringLength)
   {
     function repeatStr(aC, aN)
     {
       var s = "";
       for (var i = 0; i < aN; i++) {
         s += aC;
       }
       return s;
     }
 
-    // Generate the indent.  There's a subset of the Unicode "light"
-    // box-drawing chars that are widely implemented in terminals, and
-    // this code sticks to that subset to maximize the chance that
-    // cutting and pasting about:memory output to a terminal will work
-    // correctly:
-    const kHorizontal       = "\u2500",
-          kVertical         = "\u2502",
-          kUpAndRight       = "\u2514",
-          kVerticalAndRight = "\u251c";
+    // Generate the indent.
     var indent = "<span class='treeLine'>";
     if (aIndentGuide.length > 0) {
       for (var i = 0; i < aIndentGuide.length - 1; i++) {
         indent += aIndentGuide[i]._isLastKid ? " " : kVertical;
         indent += repeatStr(" ", aIndentGuide[i]._depth - 1);
       }
       indent += aIndentGuide[i]._isLastKid ? kUpAndRight : kVerticalAndRight;
       indent += repeatStr(kHorizontal, aIndentGuide[i]._depth - 1);
     }
-
     // Indent more if this entry is narrower than its parent, and update
     // aIndentGuide accordingly.
     var tString = aT.toString();
     var extraIndentLength = Math.max(aParentStringLength - tString.length, 0);
     if (extraIndentLength > 0) {
       for (var i = 0; i < extraIndentLength; i++) {
         indent += kHorizontal;
       }
       aIndentGuide[aIndentGuide.length - 1]._depth += extraIndentLength;
     }
     indent += "</span>";
 
-    // Generate the percentage.
-    var perc = "";
+    // Generate the percentage, and determine if we should show subtrees.
+    var percText = "";
+    var showSubtrees = !aT._hideKids;
     if (aT._amount === treeBytes) {
-      perc = "100.0";
+      percText = "100.0";
     } else {
-      perc = (100 * aT._amount / treeBytes).toFixed(2);
-      perc = pad(perc, 5, '0');
+      var perc = (100 * aT._amount / treeBytes);
+      percText = (100 * aT._amount / treeBytes).toFixed(2);
+      percText = pad(percText, 5, '0');
     }
-    perc = "<span class='mrPerc'>(" + perc + "%)</span> ";
+    percText = "<span class='mrPerc'>(" + percText + "%) </span>";
+
+    // Reinstate any previous toggling of this sub-tree.
+    var path = aPrePath + aT._name;
+    var treeId = escapeAll(aProcess + ":" + path);
+    if (gToggles[treeId]) {
+      showSubtrees = !showSubtrees;
+    }
 
     // We don't want to show '(nonheap)' on a tree like 'map/vsize', since the
     // whole tree is non-heap.
     var kind = isExplicitTree ? aT._kind : undefined;
-    var text = indent + genMrValueText(tString) + " " + perc +
-               genMrNameText(kind, aT._description, aT._name,
-                             aT._hasProblem, aT._nMerged);
+
+    // For non-leaf nodes, the entire sub-tree is put within a span so it can
+    // be collapsed if the node is clicked on.
+    var hasKids = aT._kids.length > 0;
+    if (!hasKids) {
+      assert(!aT._hideKids, "leaf node with _hideKids set")
+    }
+    var text = indent;
+    if (hasKids) {
+      text +=
+        "<span onclick='toggle(event)' class='hasKids' id='" + treeId + "'>";
+    }
+    text += genMrValueText(tString) + percText;
+    text += genMrNameText(kind, showSubtrees, hasKids, aT._description,
+                          aT._name, aT._hasProblem, aT._nMerged);
+    if (hasKids) {
+      var hiddenText = showSubtrees ? "" : " hidden";
+      // The 'kids' class is just used for sanity checking in toggle().
+      text += "</span><span class='kids" + hiddenText + "'>";
+    }
 
     for (var i = 0; i < aT._kids.length; i++) {
       // 3 is the standard depth, the callee adjusts it if necessary.
       aIndentGuide.push({ _isLastKid: (i === aT._kids.length - 1), _depth: 3 });
-      text += genTreeText2(aT._kids[i], aIndentGuide, tString.length);
+      text += genTreeText2(path + "/", aT._kids[i], aIndentGuide,
+                           tString.length);
       aIndentGuide.pop();
     }
+    text += hasKids ? "</span>" : "";
     return text;
   }
 
-  var text = genTreeText2(aT, [], rootStringLength);
+  var text = genTreeText2(/* prePath = */"", aT, [], rootStringLength);
 
-  // The explicit tree is not collapsed, but all other trees are, so pass
-  // !isExplicitTree for genSectionMarkup's aCollapsed parameter.
-  return genSectionMarkup(aProcess, aT._name, text, !isExplicitTree);
+  return genSectionMarkup(aT._name, text);
 }
 
-function OtherReporter(aPath, aUnits, aAmount, aDescription, 
+function OtherReporter(aPath, aUnits, aAmount, aDescription,
                        aNMerged)
 {
   // Nb: _kind is not needed, it's always KIND_OTHER.
   this._path        = aPath;
   this._units       = aUnits;
   if (aAmount === kUnknown) {
     this._amount     = 0;
     this._hasProblem = true;
@@ -1050,17 +1184,17 @@ function genOtherText(aReportersByProces
   // Reporters that have already been handled.  Also find the width of the
   // widest element, so we can format things nicely.
   var maxStringLength = 0;
   var otherReporters = [];
   for (var path in aReportersByProcess) {
     var r = aReportersByProcess[path];
     if (!r._done) {
       assert(r._kind === KIND_OTHER, "_kind !== KIND_OTHER for " + r._path);
-      assert(r.nMerged === undefined);  // we don't allow dup'd OTHER reporters 
+      assert(r.nMerged === undefined);  // we don't allow dup'd OTHER reporters
       var hasProblem = false;
       if (r._amount === kUnknown) {
         hasProblem = true;
       }
       var o = new OtherReporter(r._path, r._units, r._amount, r._description);
       otherReporters.push(o);
       if (o.asString.length > maxStringLength) {
         maxStringLength = o.asString.length;
@@ -1068,39 +1202,29 @@ function genOtherText(aReportersByProces
     }
   }
   otherReporters.sort(OtherReporter.compare);
 
   // Generate text for the not-yet-printed values.
   var text = "";
   for (var i = 0; i < otherReporters.length; i++) {
     var o = otherReporters[i];
-    text += genMrValueText(pad(o.asString, maxStringLength, ' ')) + " ";
-    text += genMrNameText(KIND_OTHER, o._description, o._path, o._hasProblem);
+    text += genMrValueText(pad(o.asString, maxStringLength, ' '));
+    text += genMrNameText(KIND_OTHER, /* showSubtrees = */true,
+                          /* hasKids = */false, o._description, o._path,
+                          o._hasProblem);
   }
 
-  // Nb: the newlines give nice spacing if we cut+paste into a text buffer.
-  const desc = "This list contains other memory measurements that cross-cut " +
-               "the requested memory measurements above."
-
-  return genSectionMarkup(aProcess, 'other', text, false);
+  return genSectionMarkup('other', text);
 }
 
-function genSectionMarkup(aProcess, aName, aText, aCollapsed)
+function genSectionMarkup(aName, aText)
 {
-  var headerId = 'header-' + aProcess + '-' + aName;
-  var preId = 'pre-' + aProcess + '-' + aName;
-  var elemClass = (aCollapsed ? 'collapsed' : '') + ' tree';
-
-  // Ugh.
-  return '<h2 id="' + headerId + '" class="' + elemClass + '" ' +
-         'onclick="toggleTreeVisibility(event)">' +
-           kTreeNames[aName] +
-         '</h2>\n' +
-         '<pre id="' + preId + '" class="' + elemClass + '">' + aText + '</pre>\n';
+  return "<h2 class='sectionHeader'>" + kTreeNames[aName] + "</h2>\n" +
+         "<pre class='tree'>" + aText + "</pre>\n";
 }
 
 function assert(aCond, aMsg)
 {
   if (!aCond) {
     throw("assertion failed: " + aMsg);
   }
 }
--- a/toolkit/components/aboutmemory/tests/Makefile.in
+++ b/toolkit/components/aboutmemory/tests/Makefile.in
@@ -41,14 +41,15 @@ srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir  = toolkit/components/aboutmemory/tests
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _CHROME_FILES	= \
 		test_aboutmemory.xul \
+		test_aboutmemory2.xul \
 		test_sqliteMultiReporter.xul \
 		$(NULL)
 
 libs:: $(_CHROME_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
 
--- a/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
@@ -118,16 +118,17 @@
           // The amounts are given in pages, so multiply here by 4kb.
           function f(p, a) { cbObj.callback("", p, NONHEAP, BYTES, a * 4 * KB, "(desc)", closure); }
           f("map/vsize/a",     24);
           f("map/swap/a",       1);
           f("map/swap/a",       2);
           f("map/vsize/a",      19);
           f("map/swap/b/c",     10);
           f("map/resident/a",   42);
+          f("map/pss/a",        43);
        },
        explicitNonHeap: 0
      }
   ];
   for (var i = 0; i < fakeReporters.length; i++) {
     mgr.registerReporter(fakeReporters[i]);
   }
   for (var i = 0; i < fakeMultiReporters.length; i++) {
@@ -181,182 +182,163 @@
   <![CDATA[
   var amExpectedText =
 "\
 Main Process\n\
 \n\
 Explicit Allocations\n\
 623.58 MB (100.0%) -- explicit\n\
 ├──232.00 MB (37.20%) -- b\n\
-│  ├───85.00 MB (13.63%) -- a\n\
-│  ├───75.00 MB (12.03%) -- b\n\
+│  ├───85.00 MB (13.63%) ── a\n\
+│  ├───75.00 MB (12.03%) ── b\n\
 │  └───72.00 MB (11.55%) -- c\n\
-│      ├──70.00 MB (11.23%) -- a\n\
-│      └───2.00 MB (00.32%) -- (1 omitted)\n\
-├──222.00 MB (35.60%) -- a\n\
+│      ├──70.00 MB (11.23%) ── a\n\
+│      └───2.00 MB (00.32%) ── b\n\
+├──222.00 MB (35.60%) ── a\n\
 ├──100.00 MB (16.04%) -- c\n\
-│  ├───77.00 MB (12.35%) -- other\n\
-│  └───23.00 MB (03.69%) -- d [2]\n\
-├───23.00 MB (03.69%) -- cc [2]\n\
+│  ├───77.00 MB (12.35%) ── other\n\
+│  └───23.00 MB (03.69%) ── d [2]\n\
+├───23.00 MB (03.69%) ── cc [2]\n\
 ├───20.00 MB (03.21%) -- f\n\
 │   └──20.00 MB (03.21%) -- g\n\
 │      └──20.00 MB (03.21%) -- h\n\
-│         └──20.00 MB (03.21%) -- i\n\
-├───15.00 MB (02.41%) -- g\n\
-│   ├───6.00 MB (00.96%) -- a\n\
-│   ├───5.00 MB (00.80%) -- b\n\
-│   └───4.00 MB (00.64%) -- other\n\
-├───11.00 MB (01.76%) -- heap-unclassified\n\
-└────0.58 MB (00.09%) -- (2 omitted)\n\
-\n\
-Resident Set Size (RSS) Breakdown\n\
-0.16 MB (100.0%) -- resident\n\
-└──0.16 MB (100.0%) -- a\n\
-\n\
-Virtual Size Breakdown\n\
-0.17 MB (100.0%) -- vsize\n\
-└──0.17 MB (100.0%) -- a [2]\n\
-\n\
-Swap Usage Breakdown\n\
-0.05 MB (100.0%) -- swap\n\
-├──0.04 MB (76.92%) -- b\n\
-│  └──0.04 MB (76.92%) -- c\n\
-└──0.01 MB (23.08%) -- a [2]\n\
+│         └──20.00 MB (03.21%) ── i\n\
+├───15.00 MB (02.41%) ++ g\n\
+├───11.00 MB (01.76%) ── heap-unclassified\n\
+└────0.58 MB (00.09%) ++ (2 tiny)\n\
 \n\
 Other Measurements\n\
-500.00 MB -- heap-allocated\n\
-100.00 MB -- heap-unallocated\n\
-111.00 MB -- other1\n\
-222.00 MB -- other2\n\
-      777 -- other3\n\
-      888 -- other4\n\
-   45.67% -- perc1\n\
-  100.00% -- perc2\n\
+500.00 MB ── heap-allocated\n\
+100.00 MB ── heap-unallocated\n\
+111.00 MB ── other1\n\
+222.00 MB ── other2\n\
+      777 ── other3\n\
+      888 ── other4\n\
+   45.67% ── perc1\n\
+  100.00% ── perc2\n\
 \n\
 2nd Process\n\
 \n\
 Explicit Allocations\n\
 1,000.00 MB (100.0%) -- explicit\n\
 ├────499.00 MB (49.90%) -- a\n\
 │    └──499.00 MB (49.90%) -- b\n\
-│       └──499.00 MB (49.90%) -- c [3]\n\
-├────200.00 MB (20.00%) -- flip/the/backslashes\n\
-├────200.00 MB (20.00%) -- compartment(compartment-url)\n\
-└────101.00 MB (10.10%) -- heap-unclassified\n\
+│       └──499.00 MB (49.90%) ── c [3]\n\
+├────200.00 MB (20.00%) ── flip/the/backslashes\n\
+├────200.00 MB (20.00%) ── compartment(compartment-url)\n\
+└────101.00 MB (10.10%) ── heap-unclassified\n\
 \n\
 Other Measurements\n\
-  666.00 MB -- danger<script>window.alert(1)</script>\n\
-1,000.00 MB -- heap-allocated\n\
-  100.00 MB -- heap-unallocated\n\
-  111.00 MB -- other1\n\
+  666.00 MB ── danger<script>window.alert(1)</script>\n\
+1,000.00 MB ── heap-allocated\n\
+  100.00 MB ── heap-unallocated\n\
+  111.00 MB ── other1\n\
 \n\
 3rd Process\n\
 \n\
 WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is zero and the 'explicit' tree shows less memory than it should.\n\
 \n\
 Explicit Allocations\n\
 777.00 MB (100.0%) -- explicit\n\
 ├──777.00 MB (100.0%) -- a\n\
-│  ├──444.00 MB (57.14%) -- c [2]\n\
-│  ├──333.00 MB (42.86%) -- b\n\
-│  └────0.00 MB (00.00%) -- (1 omitted)\n\
-└────0.00 MB (00.00%) -- (2 omitted)\n\
+│  ├──444.00 MB (57.14%) ── c [2]\n\
+│  ├──333.00 MB (42.86%) ── b\n\
+│  └────0.00 MB (00.00%) ── d [*] [2]\n\
+└────0.00 MB (00.00%) ++ (2 tiny)\n\
 \n\
 Other Measurements\n\
-0.00 MB -- heap-allocated [*]\n\
-0.00 MB -- other1 [*]\n\
+0.00 MB ── heap-allocated [*]\n\
+0.00 MB ── other1 [*]\n\
 \n\
 ";
 
   var amvExpectedText =
 "\
 Main Process\n\
 \n\
 Explicit Allocations\n\
 653,876,224 B (100.0%) -- explicit\n\
 ├──243,269,632 B (37.20%) -- b\n\
-│  ├───89,128,960 B (13.63%) -- a\n\
-│  ├───78,643,200 B (12.03%) -- b\n\
+│  ├───89,128,960 B (13.63%) ── a\n\
+│  ├───78,643,200 B (12.03%) ── b\n\
 │  └───75,497,472 B (11.55%) -- c\n\
-│      ├──73,400,320 B (11.23%) -- a\n\
-│      └───2,097,152 B (00.32%) -- b\n\
-├──232,783,872 B (35.60%) -- a\n\
+│      ├──73,400,320 B (11.23%) ── a\n\
+│      └───2,097,152 B (00.32%) ── b\n\
+├──232,783,872 B (35.60%) ── a\n\
 ├──104,857,600 B (16.04%) -- c\n\
-│  ├───80,740,352 B (12.35%) -- other\n\
-│  └───24,117,248 B (03.69%) -- d [2]\n\
-├───24,117,248 B (03.69%) -- cc [2]\n\
+│  ├───80,740,352 B (12.35%) ── other\n\
+│  └───24,117,248 B (03.69%) ── d [2]\n\
+├───24,117,248 B (03.69%) ── cc [2]\n\
 ├───20,971,520 B (03.21%) -- f\n\
 │   └──20,971,520 B (03.21%) -- g\n\
 │      └──20,971,520 B (03.21%) -- h\n\
-│         └──20,971,520 B (03.21%) -- i\n\
+│         └──20,971,520 B (03.21%) ── i\n\
 ├───15,728,640 B (02.41%) -- g\n\
-│   ├───6,291,456 B (00.96%) -- a\n\
-│   ├───5,242,880 B (00.80%) -- b\n\
-│   └───4,194,304 B (00.64%) -- other\n\
-├───11,534,336 B (01.76%) -- heap-unclassified\n\
-├──────510,976 B (00.08%) -- d\n\
-└──────102,400 B (00.02%) -- e\n\
+│   ├───6,291,456 B (00.96%) ── a\n\
+│   ├───5,242,880 B (00.80%) ── b\n\
+│   └───4,194,304 B (00.64%) ── other\n\
+├───11,534,336 B (01.76%) ── heap-unclassified\n\
+├──────510,976 B (00.08%) ── d\n\
+└──────102,400 B (00.02%) ── e\n\
 \n\
 Resident Set Size (RSS) Breakdown\n\
-172,032 B (100.0%) -- resident\n\
-└──172,032 B (100.0%) -- a\n\
+172,032 B (100.0%) ++ resident\n\
+\n\
+Proportional Set Size (PSS) Breakdown\n\
+176,128 B (100.0%) ++ pss\n\
 \n\
 Virtual Size Breakdown\n\
-176,128 B (100.0%) -- vsize\n\
-└──176,128 B (100.0%) -- a [2]\n\
+176,128 B (100.0%) ++ vsize\n\
 \n\
 Swap Usage Breakdown\n\
-53,248 B (100.0%) -- swap\n\
-├──40,960 B (76.92%) -- b\n\
-│  └──40,960 B (76.92%) -- c\n\
-└──12,288 B (23.08%) -- a [2]\n\
+53,248 B (100.0%) ++ swap\n\
 \n\
 Other Measurements\n\
-524,288,000 B -- heap-allocated\n\
-104,857,600 B -- heap-unallocated\n\
-116,391,936 B -- other1\n\
-232,783,872 B -- other2\n\
-          777 -- other3\n\
-          888 -- other4\n\
-       45.67% -- perc1\n\
-      100.00% -- perc2\n\
+524,288,000 B ── heap-allocated\n\
+104,857,600 B ── heap-unallocated\n\
+116,391,936 B ── other1\n\
+232,783,872 B ── other2\n\
+          777 ── other3\n\
+          888 ── other4\n\
+       45.67% ── perc1\n\
+      100.00% ── perc2\n\
 \n\
 2nd Process\n\
 \n\
 Explicit Allocations\n\
 1,048,576,000 B (100.0%) -- explicit\n\
 ├────523,239,424 B (49.90%) -- a\n\
 │    └──523,239,424 B (49.90%) -- b\n\
-│       └──523,239,424 B (49.90%) -- c [3]\n\
-├────209,715,200 B (20.00%) -- flip/the/backslashes\n\
-├────209,715,200 B (20.00%) -- compartment(compartment-url)\n\
-└────105,906,176 B (10.10%) -- heap-unclassified\n\
+│       └──523,239,424 B (49.90%) ── c [3]\n\
+├────209,715,200 B (20.00%) ── flip/the/backslashes\n\
+├────209,715,200 B (20.00%) ── compartment(compartment-url)\n\
+└────105,906,176 B (10.10%) ── heap-unclassified\n\
 \n\
 Other Measurements\n\
-  698,351,616 B -- danger<script>window.alert(1)</script>\n\
-1,048,576,000 B -- heap-allocated\n\
-  104,857,600 B -- heap-unallocated\n\
-  116,391,936 B -- other1\n\
+  698,351,616 B ── danger<script>window.alert(1)</script>\n\
+1,048,576,000 B ── heap-allocated\n\
+  104,857,600 B ── heap-unallocated\n\
+  116,391,936 B ── other1\n\
 \n\
 3rd Process\n\
 \n\
 WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is zero and the 'explicit' tree shows less memory than it should.\n\
 \n\
 Explicit Allocations\n\
 814,743,552 B (100.0%) -- explicit\n\
 ├──814,743,552 B (100.0%) -- a\n\
-│  ├──465,567,744 B (57.14%) -- c [2]\n\
-│  ├──349,175,808 B (42.86%) -- b\n\
-│  └────────────0 B (00.00%) -- d [*] [2]\n\
-├────────────0 B (00.00%) -- b [*]\n\
-└────────────0 B (00.00%) -- heap-unclassified [*]\n\
+│  ├──465,567,744 B (57.14%) ── c [2]\n\
+│  ├──349,175,808 B (42.86%) ── b\n\
+│  └────────────0 B (00.00%) ── d [*] [2]\n\
+├────────────0 B (00.00%) ── b [*]\n\
+└────────────0 B (00.00%) ── heap-unclassified [*]\n\
 \n\
 Other Measurements\n\
-0 B -- heap-allocated [*]\n\
-0 B -- other1 [*]\n\
+0 B ── heap-allocated [*]\n\
+0 B ── other1 [*]\n\
 \n\
 "
 
   function finish()
   {
     // Unregister fake reporters and multi-reporters, re-register the real
     // reporters and multi-reporters, just in case subsequent tests rely on
     // them.
@@ -370,41 +352,39 @@ 0 B -- other1 [*]\n\
       mgr.registerReporter(realReporters[i]);
     }
     for (var i = 0; i < realMultiReporters.length; i++) {
       mgr.registerMultiReporter(realMultiReporters[i]);
     }
     SimpleTest.finish();
   }
 
+  var gHaveDumped = false;
+
   function checkClipboard(actual, expected) {
     if (actual != expected) {
-      dump("*******ACTUAL*******\n");
-      dump(actual);
-      dump("******EXPECTED******\n");
-      dump(expected);
-      dump("********************\n");
+      if (!gHaveDumped) {
+        dump("******EXPECTED******\n");
+        dump(expected);
+        dump("*******ACTUAL*******\n");
+        dump(actual);
+        dump("********************\n");
+        gHaveDumped = true;
+      }
       return false;
     }
     return true;
   }
 
   // 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, aExpectedText, aNext) {
-    // Click all h2.collapsed elements so they expand.
-    var win = document.querySelector("#" + aFrame).contentWindow;
-    var nodes = win.document.querySelectorAll("pre.collapsed");
-    for (var i = 0; i < nodes.length; i++) {
-      nodes[i].classList.toggle('collapsed');
-    }
-
     SimpleTest.executeSoon(function() {
-      document.querySelector("#" + aFrame).focus();
+      document.getElementById(aFrame).focus();
       SimpleTest.waitForClipboard(
         function(actual) { return checkClipboard(actual, aExpectedText) },
         function() {
           synthesizeKey("A", {accelKey: true});
           synthesizeKey("C", {accelKey: true});
         },
         aNext,
         function() {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul
@@ -0,0 +1,221 @@
+<?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 the collapsing and expanding of sub-trees 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[
+  const Cc = Components.classes;
+  const Ci = Components.interfaces;
+  var 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 HEAP  = Ci.nsIMemoryReporter.KIND_HEAP;
+  const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+  const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+
+  function f(aPath, aKind, aAmount) {
+    return {
+      process:     "",
+      path:        aPath,
+      kind:        aKind,
+      units:       BYTES,
+      description: "(description)",
+      amount:      aAmount
+    };
+  }
+
+  var fakeReporters = [
+    f("heap-allocated",     OTHER,   250 * MB),
+    f("explicit/a/b",       HEAP,     50 * MB),
+    f("explicit/a/c/d",     HEAP,     30 * MB),
+    f("explicit/a/c/e",     HEAP,     20 * MB),
+    f("explicit/a/f",       HEAP,     40 * MB),
+    f("explicit/g",         HEAP,    100 * MB)
+  ];
+
+  for (var i = 0; i < fakeReporters.length; i++) {
+    mgr.registerReporter(fakeReporters[i]);
+  }
+
+  ]]>
+  </script>
+
+  <iframe id="amFrame"  height="500" src="about:memory"></iframe>
+
+  <script type="application/javascript">
+  <![CDATA[
+  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 < realReporters.length; i++) {
+      mgr.registerReporter(realReporters[i]);
+    }
+    for (var i = 0; i < realMultiReporters.length; i++) {
+      mgr.registerMultiReporter(realMultiReporters[i]);
+    }
+    SimpleTest.finish();
+  }
+
+  var gHaveDumped = false;
+
+  function checkClipboard(actual, expected) {
+    if (actual != expected) {
+      if (!gHaveDumped) {
+        dump("******EXPECTED******\n");
+        dump(expected);
+        dump("*******ACTUAL*******\n");
+        dump(actual);
+        dump("********************\n");
+        gHaveDumped = true;
+      }
+      return false;
+    }
+    return true;
+  }
+
+  // Click on the identified element, then cut+paste the entire page and
+  // check that the cut text matches what we expect.
+  function test(aId, aExpectedText, aNext) {
+    var win = document.getElementById("amFrame").contentWindow;
+    var 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|.
+    if (node.nodeName === "button") {
+      node.click();
+    } else {
+      node.childNodes[0].click();
+    }
+
+    SimpleTest.executeSoon(function() {
+      document.getElementById("amFrame").focus();
+      SimpleTest.waitForClipboard(
+        function(actual) { return checkClipboard(actual, aExpectedText) },
+        function() {
+          synthesizeKey("A", {accelKey: true});
+          synthesizeKey("C", {accelKey: true});
+        },
+        aNext,
+        function() {
+          ok(false, "pasted text doesn't match");
+          finish();
+        }
+      );
+    });
+  }
+
+  // Returns a function that chains together one test() call per id.
+  function chain(ids) {
+    var x = ids.shift();
+    if (x) {
+      return function() { test(x.id, x.expected, chain(ids)); }
+    } else {
+      return function() { finish(); };
+    }
+  }
+
+  var openExpected =
+"\
+Main Process\n\
+\n\
+Explicit Allocations\n\
+250.00 MB (100.0%) -- explicit\n\
+├──140.00 MB (56.00%) -- a\n\
+│  ├───50.00 MB (20.00%) ── b\n\
+│  ├───50.00 MB (20.00%) -- c\n\
+│  │   ├──30.00 MB (12.00%) ── d\n\
+│  │   └──20.00 MB (08.00%) ── e\n\
+│  └───40.00 MB (16.00%) ── f\n\
+├──100.00 MB (40.00%) ── g\n\
+└───10.00 MB (04.00%) ── heap-unclassified\n\
+\n\
+Other Measurements\n\
+250.00 MB ── heap-allocated\n\
+\n\
+";
+
+  var cClosedExpected =
+"\
+Main Process\n\
+\n\
+Explicit Allocations\n\
+250.00 MB (100.0%) -- explicit\n\
+├──140.00 MB (56.00%) -- a\n\
+│  ├───50.00 MB (20.00%) ── b\n\
+│  ├───50.00 MB (20.00%) ++ c\n\
+│  └───40.00 MB (16.00%) ── f\n\
+├──100.00 MB (40.00%) ── g\n\
+└───10.00 MB (04.00%) ── heap-unclassified\n\
+\n\
+Other Measurements\n\
+250.00 MB ── heap-allocated\n\
+\n\
+";
+
+  var aClosedExpected =
+"\
+Main Process\n\
+\n\
+Explicit Allocations\n\
+250.00 MB (100.0%) -- explicit\n\
+├──140.00 MB (56.00%) ++ a\n\
+├──100.00 MB (40.00%) ── g\n\
+└───10.00 MB (04.00%) ── heap-unclassified\n\
+\n\
+Other Measurements\n\
+250.00 MB ── heap-allocated\n\
+\n\
+";
+
+  // We close two sub-trees, hit the "Update" button, then reopen the two
+  // sub-trees in reverse order.  After each step, we check the output.
+  var idsToClick = [
+    { id: "Main:explicit/a/c", expected: cClosedExpected },
+    { id: "Main:explicit/a",   expected: aClosedExpected },
+    { id: "updateButton",      expected: aClosedExpected },
+    { id: "Main:explicit/a",   expected: cClosedExpected },
+    { id: "Main:explicit/a/c", expected: openExpected }
+  ];
+
+  addLoadEvent(chain(idsToClick));
+
+  SimpleTest.waitForExplicitFinish();
+  ]]>
+  </script>
+</window>