Bug 722969 - Don't use innerHTML when generating about:memory. r=jlebar.
authorNicholas Nethercote <nnethercote@mozilla.com>
Tue, 31 Jan 2012 20:05:14 -0800
changeset 86267 5bbfd1e5ebf7
parent 86266 b937df325e71
child 86268 290c4cdaa796
push id22008
push userbmo@edmorley.co.uk
push dateTue, 07 Feb 2012 10:32:58 +0000
treeherdermozilla-central@2b61af9d18ee [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlebar
bugs722969
milestone13.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 722969 - Don't use innerHTML when generating about:memory. r=jlebar.
toolkit/components/aboutmemory/content/aboutMemory.js
toolkit/components/aboutmemory/content/aboutMemory.xhtml
--- a/toolkit/components/aboutmemory/content/aboutMemory.js
+++ b/toolkit/components/aboutmemory/content/aboutMemory.js
@@ -37,53 +37,37 @@
  * ***** END LICENSE BLOCK ***** */
 
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
-// Must use .href here instead of .search because "about:memory" is a
-// non-standard URL.
-var gVerbose = (location.href.split(/[\?,]/).indexOf("verbose") !== -1);
+const gVerbose = location.href === "about:memory?verbose";
 
 var gAddedObserver = false;
 
-const KIND_NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
-const KIND_HEAP    = Ci.nsIMemoryReporter.KIND_HEAP;
-const KIND_OTHER   = Ci.nsIMemoryReporter.KIND_OTHER;
-const UNITS_BYTES  = Ci.nsIMemoryReporter.UNITS_BYTES;
-const UNITS_COUNT  = Ci.nsIMemoryReporter.UNITS_COUNT;
+const KIND_NONHEAP           = Ci.nsIMemoryReporter.KIND_NONHEAP;
+const KIND_HEAP              = Ci.nsIMemoryReporter.KIND_HEAP;
+const KIND_OTHER             = Ci.nsIMemoryReporter.KIND_OTHER;
+const UNITS_BYTES            = Ci.nsIMemoryReporter.UNITS_BYTES;
+const UNITS_COUNT            = Ci.nsIMemoryReporter.UNITS_COUNT;
 const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
-const UNITS_PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
+const UNITS_PERCENTAGE       = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
 
 const kUnknown = -1;    // used for _amount if a memory reporter failed
 
-// Paths, names and descriptions all need to be sanitized before being
-// displayed, because the user has some control over them via names of
-// compartments, windows, etc.  Also, forward slashes in URLs in paths are
-// represented with backslashes to avoid being mistaken for path separators, so
-// we need to undo that as well.
-
-function escapeAll(aStr)
-{
-  return aStr.replace(/\&/g, '&amp;').replace(/'/g, '&#39;').
-              replace(/\</g, '&lt;').replace(/>/g, '&gt;').
-              replace(/\"/g, '&quot;');
-}
-
-function flipBackslashes(aStr)
-{
-  return aStr.replace(/\\/g, '/');
-}
-
+// Forward slashes in URLs in paths are represented with backslashes to avoid
+// being mistaken for path separators.  Paths/names/descriptions where this
+// hasn't been undone are prefixed with "unsafe"; the rest are prefixed with
+// "safe".
 function makeSafe(aUnsafeStr)
 {
-  return escapeAll(flipBackslashes(aUnsafeStr));
+  return aUnsafeStr.replace(/\\/g, '/');
 }
 
 const kTreeUnsafeDescriptions = {
   'explicit' :
     "This tree covers explicit memory allocations by the application, " +
     "both at the operating system level (via calls to functions such as " +
     "VirtualAlloc, vm_allocate, and mmap), and at the heap allocation level " +
     "(via functions such as malloc, calloc, realloc, memalign, operator " +
@@ -163,21 +147,16 @@ function onUnload()
   }
 }
 
 function ChildMemoryListener(aSubject, aTopic, aData)
 {
   update();
 }
 
-function $(n)
-{
-  return document.getElementById(n);
-}
-
 function doGlobalGC()
 {
   Cu.forceGC();
   var os = Cc["@mozilla.org/observer-service;1"]
             .getService(Ci.nsIObserverService);
   os.notifyObservers(null, "child-gc-request", null);
   update();
 }
@@ -310,90 +289,115 @@ function getReportersByProcess(aMgr)
     catch(e) {
       debug("An error occurred when collecting a multi-reporter's results: " + e);
     }
   }
 
   return reportersByProcess;
 }
 
+function appendTextNode(aP, aText)
+{
+  var e = document.createTextNode(aText);
+  aP.appendChild(e);
+  return e;
+}
+
+function appendElement(aP, aTagName, aClassName)
+{
+  var e = document.createElement(aTagName);
+  e.className = aClassName;
+  aP.appendChild(e);
+  return e;
+}
+
+function appendElementWithText(aP, aTagName, aClassName, aText)
+{
+  var e = appendElement(aP, aTagName, aClassName);
+  appendTextNode(e, aText);
+  return e;
+}
+
 /**
  * Top-level function that does the work of generating the page.
  */
 function update()
 {
   // First, clear the page contents.  Necessary because update() might be
   // called more than once due to ChildMemoryListener.
-  var content = $("content");
-  content.parentNode.replaceChild(content.cloneNode(false), content);
-  content = $("content");
-
-  if (gVerbose)
-    content.parentNode.classList.add('verbose');
-  else
-    content.parentNode.classList.add('non-verbose');
+  var oldContent = document.getElementById("content");
+  var content = oldContent.cloneNode(false);
+  oldContent.parentNode.replaceChild(content, oldContent);
+  content.classList.add(gVerbose ? 'verbose' : 'non-verbose');
 
   var mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
       getService(Ci.nsIMemoryReporterManager);
 
-  var text = "";
-
   // Generate output for one process at a time.  Always start with the
   // Main process.
   var reportersByProcess = getReportersByProcess(mgr);
   var hasMozMallocUsableSize = mgr.hasMozMallocUsableSize;
-  text += genProcessText("Main", reportersByProcess["Main"],
-                         hasMozMallocUsableSize);
+  appendProcessElements(content, "Main", reportersByProcess["Main"],
+                        hasMozMallocUsableSize);
   for (var process in reportersByProcess) {
     if (process !== "Main") {
-      text += genProcessText(process, reportersByProcess[process],
-                             hasMozMallocUsableSize);
+      appendProcessElements(content, process, reportersByProcess[process],
+                            hasMozMallocUsableSize);
     }
   }
 
+  appendElement(content, "hr");
+
   // Memory-related actions.
   const UpDesc = "Re-measure.";
   const GCDesc = "Do a global garbage collection.";
   const CCDesc = "Do a cycle collection.";
   const MPDesc = "Send three \"heap-minimize\" notifications in a " +
                  "row.  Each notification triggers a global garbage " +
                  "collection followed by a cycle collection, and causes the " +
                  "process to reduce memory usage in other ways, e.g. by " +
                  "flushing various caches.";
 
+  function appendButton(aTitle, aOnClick, aText, aId)
+  {
+    var b = appendElementWithText(content, "button", "", aText);
+    b.title = aTitle;
+    b.onclick = aOnClick
+    if (aId) {
+      b.id = aId;
+    }
+  }
+
   // The "Update" button has an id so it can be clicked in a test.
-  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
-        ? "<span class='option'><a href='about:memory'>Less verbose</a></span>"
-        : "<span class='option'><a href='about:memory?verbose'>More verbose</a></span>";
-  text += "</div>";
+  appendButton(UpDesc, update,                   "Update", "updateButton");
+  appendButton(GCDesc, doGlobalGC,               "GC");
+  appendButton(CCDesc, doCC,                     "CC");
+  appendButton(MPDesc, sendHeapMinNotifications, "Minimize memory usage");
 
-  text += "<div>" +
-          "<span class='option'><a href='about:support'>Troubleshooting information</a></span>" +
-          "</div>";
+  var div1 = appendElement(content, "div", "");
+  var a;
+  if (gVerbose) {
+    var a = appendElementWithText(div1, "a", "option", "Less verbose");
+    a.href = "about:memory";
+  } else {
+    var a = appendElementWithText(div1, "a", "option", "More verbose");
+    a.href = "about:memory?verbose";
+  }
 
-  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 description of what it measures.</span>";
+  var div2 = appendElement(content, "div", "");
+  a = appendElementWithText(div2, "a", "option", "Troubleshooting information");
+  a.href = "about:support";
 
-  var div = document.createElement("div");
-  div.innerHTML = text;
-  content.appendChild(div);
+  var legendText1 = "Click on a non-leaf node in a tree to expand ('++') " +
+                    "or collapse ('--') its children.";
+  var legendText2 = "Hover the pointer over the name of a memory reporter " +
+                    "to see a description of what it measures.";
+
+  appendElementWithText(content, "div", "legend", legendText1);
+  appendElementWithText(content, "div", "legend", legendText2);
 }
 
 // There are two kinds of TreeNode.
 // - 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(aUnsafeName)
 {
@@ -573,17 +577,16 @@ function ignoreTree(aReporters, aTreeNam
  *        The tree.
  * @param aReporters
  *        Table of Reporters for this process, indexed by _unsafePath.
  * @return A boolean indicating if "heap-allocated" is known for the process.
  */
 function fixUpExplicitTree(aT, aReporters)
 {
   // Determine how many bytes are reported by heap reporters.
-  var s = "";
   function getKnownHeapUsedBytes(aT)
   {
     var n = 0;
     if (aT._kids.length === 0) {
       // Leaf node.
       assert(aT._kind !== undefined, "aT._kind is undefined for leaf node");
       n = aT._kind === KIND_HEAP ? aT._amount : 0;
     } else {
@@ -696,122 +699,123 @@ function sortTreeAndInsertAggregateNodes
   sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
 }
 
 // Global variable indicating if we've seen any invalid values for this
 // process;  it holds the unsafePaths of any such reporters.  It is reset for
 // each new process.
 var gUnsafePathsWithInvalidValuesForThisProcess = [];
 
-function genWarningText(aHasKnownHeapAllocated, aHasMozMallocUsableSize)
+function appendWarningElements(aP, 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 " +
-      "'heap-unclassified' is zero and the 'explicit' tree shows " +
-      "much less memory than it should.</p>\n\n";
+    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 zero " +
+      "and the 'explicit' tree shows much less memory than it should.");
+    appendTextNode(aP, "\n\n");
 
   } else if (!aHasKnownHeapAllocated) {
-    warningText =
-      "<p class='accuracyWarning'>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.</p>\n\n";
+    appendElementWithText(aP, "p", "", 
+      "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.");
+    appendTextNode(aP, "\n\n");
 
   } else if (!aHasMozMallocUsableSize) {
-    warningText =
-      "<p class='accuracyWarning'>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'.</p>\n\n";
+    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'.");
+    appendTextNode(aP, "\n\n");
   }
 
   if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) {
-    warningText +=
-      "<div class='accuracyWarning'>" +
-      "<p>WARNING: the following values are negative or unreasonably " +
-      "large.</p>\n" +
-      "<ul>";
+    var div = appendElement(aP, "div", "");
+    appendElementWithText(div, "p", "", 
+      "WARNING: the following values are negative or unreasonably large.");
+    appendTextNode(div, "\n");  
+
+    var ul = appendElement(div, "ul", "");
     for (var i = 0;
          i < gUnsafePathsWithInvalidValuesForThisProcess.length;
          i++)
     {
-      warningText +=
-        " <li>" +
-        makeSafe(gUnsafePathsWithInvalidValuesForThisProcess[i]) +
-        "</li>\n";
+      appendTextNode(ul, " ");
+      appendElementWithText(ul, "li", "", 
+        makeSafe(gUnsafePathsWithInvalidValuesForThisProcess[i]));
+      appendTextNode(ul, "\n");
     }
-    warningText +=
-      "</ul>" +
-      "<p>This indicates a defect in one or more memory reporters.  The " +
+
+    appendElementWithText(div, "p", "",
+      "This indicates a defect in one or more memory reporters.  The " +
       "invalid values are highlighted, but you may need to expand one " +
-      "or more sub-trees to see them.</p>\n\n" +
-      "</div>";
+      "or more sub-trees to see them.");
+    appendTextNode(div, "\n\n");  
     gUnsafePathsWithInvalidValuesForThisProcess = [];  // reset for the next process
   }
-
-  return warningText;
 }
 
 /**
- * Generates the text for a single process.
+ * Appends the elements for a single process.
  *
+ * @param aP
+ *        The parent DOM node.
  * @param aProcess
  *        The name of the process.
  * @param aReporters
  *        Table of Reporters for this process, indexed by _unsafePath.
  * @param aHasMozMallocUsableSize
  *        Boolean indicating if moz_malloc_usable_size works.
  * @return The generated text.
  */
-function genProcessText(aProcess, aReporters, aHasMozMallocUsableSize)
+function appendProcessElements(aP, aProcess, aReporters,
+                               aHasMozMallocUsableSize)
 {
+  appendElementWithText(aP, "h1", "", aProcess + " Process");
+  appendTextNode(aP, "\n\n");   // gives nice spacing when we cut and paste
+
+  // We'll fill this in later.
+  var warningsDiv = appendElement(aP, "div", "accuracyWarning");
+
   var explicitTree = buildTree(aReporters, 'explicit');
   var hasKnownHeapAllocated = fixUpExplicitTree(explicitTree, aReporters);
   sortTreeAndInsertAggregateNodes(explicitTree._amount, explicitTree);
-  var explicitText = genTreeText(explicitTree, aProcess);
+  appendTreeElements(aP, explicitTree, aProcess);
 
   // We only show these breakdown trees in verbose mode.
-  var mapTreeText = "";
   kMapTreePaths.forEach(function(t) {
     if (gVerbose) {
       var tree = buildTree(aReporters, t);
 
       // |tree| will be null if we don't have any reporters for the given
       // unsafePath.
       if (tree) {
         sortTreeAndInsertAggregateNodes(tree._amount, tree);
         tree._hideKids = true;   // map trees are always initially collapsed
-        mapTreeText += genTreeText(tree, aProcess);
+        appendTreeElements(aP, 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);
+  // We have to call appendOtherElements after we process all the trees,
+  // because it looks at all the reporters which aren't part of a tree.
+  var otherText = appendOtherElements(aP, aReporters, aProcess);
 
-  // Generate any warnings about inaccuracies due to platform limitations.
-  // This must come after generating all the text.  The newlines give nice
-  // spacing if we cut+paste into a text buffer.
-  var warningText = "";
-  var warningText =
-        genWarningText(hasKnownHeapAllocated, aHasMozMallocUsableSize);
-
-  // The newlines give nice spacing if we cut+paste into a text buffer.
-  return "<h1>" + aProcess + " Process</h1>\n\n" +
-         warningText + explicitText + mapTreeText + otherText +
-         "<hr></hr>";
+  // Add any warnings about inaccuracies due to platform limitations.
+  // These must be computed after generating all the text.  The newlines give
+  // nice spacing if we cut+paste into a text buffer.
+  var warningElements =
+        appendWarningElements(warningsDiv, hasKnownHeapAllocated,
+                              aHasMozMallocUsableSize);
 }
 
 /**
  * Determines if a number has a negative sign when converted to a string.
  * Works even for -0.
  *
  * @param aN
  *        The number.
@@ -955,72 +959,74 @@ function getUnsafeDescription(aReporters
 
 // 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";
+      kVerticalAndRight = "\u251c",
+      kDoubleHorizontalSep = " \u2500\u2500 ";
 
-function genMrValueText(aValue, aIsInvalid)
+function appendMrValueSpan(aP, aValue, aIsInvalid)
 {
-  return aIsInvalid ?
-         "<span class='mrValue invalid'>" + aValue + "</span>" :
-         "<span class='mrValue'>"         + aValue + "</span>";
+  appendElementWithText(aP, "span", "mrValue" + (aIsInvalid ? " invalid" : ""),
+                        aValue);
 }
 
 function kindToString(aKind)
 {
   switch (aKind) {
    case KIND_NONHEAP: return "(Non-heap) ";
    case KIND_HEAP:    return "(Heap) ";
    case KIND_OTHER:
    case undefined:    return "";
    default:           assert(false, "bad kind in kindToString");
   }
 }
 
-function genMrNameText(aKind, aShowSubtrees, aHasKids, aUnsafeDesc,
-                       aUnsafeName, aIsUnknown, aIsInvalid, aNMerged)
+function appendMrNameSpan(aP, aKind, aShowSubtrees, aHasKids, aUnsafeDesc,
+                          aUnsafeName, aIsUnknown, aIsInvalid, aNMerged)
 {
   var text = "";
   if (aHasKids) {
     if (aShowSubtrees) {
-      text += "<span class='mrSep hidden'> ++ </span>";
-      text += "<span class='mrSep'> -- </span>";
+      appendElementWithText(aP, "span", "mrSep hidden", " ++ ");
+      appendElementWithText(aP, "span", "mrSep",        " -- ");
     } else {
-      text += "<span class='mrSep'> ++ </span>";
-      text += "<span class='mrSep hidden'> -- </span>";
+      appendElementWithText(aP, "span", "mrSep",        " ++ ");
+      appendElementWithText(aP, "span", "mrSep hidden", " -- ");
     }
   } else {
-    text += "<span class='mrSep'> " + kHorizontal + kHorizontal + " </span>";
+    appendElementWithText(aP, "span", "mrSep", kDoubleHorizontalSep);
   }
-  text += "<span class='mrName' title='" +
-          kindToString(aKind) + makeSafe(aUnsafeDesc) + "'>" +
-          makeSafe(aUnsafeName) + "</span>";
+
+  var nameSpan = appendElementWithText(aP, "span", "mrName",
+                                       makeSafe(aUnsafeName));
+  nameSpan.title = kindToString(aKind) + makeSafe(aUnsafeDesc);
+
   if (aIsUnknown) {
-    const problemDesc =
+    var noteSpan = appendElementWithText(aP, "span", "mrNote", " [*]");
+    noteSpan.title =
       "Warning: this memory reporter was unable to compute a useful value. ";
-    text += "<span class='mrNote' title=\"" + problemDesc + "\"> [*]</span>";
   }
   if (aIsInvalid) {
-    const invalidDesc =
+    var noteSpan = appendElementWithText(aP, "span", "mrNote", " [?!]");
+    noteSpan.title =
       "Warning: this value is invalid and indicates a bug in one or more " +
       "memory reporters. ";
-    text += "<span class='mrNote' title=\"" + invalidDesc + "\"> [?!]</span>";
   }
   if (aNMerged) {
-    const dupDesc = "This value is the sum of " + aNMerged +
-                    " memory reporters that all have the same path.";
-    text += "<span class='mrNote' title=\"" + dupDesc + "\"> [" +
-            aNMerged + "]</span>";
+    var noteSpan = appendElementWithText(aP, "span", "mrNote",
+                                         " [" + aNMerged + "]");
+    noteSpan.title =
+      "This value is the sum of " + aNMerged +
+      " memory reporters that all have the same path.";
   }
-  return text + '\n';
 }
 
 // This is used to record the (safe) IDs of 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.
@@ -1062,68 +1068,73 @@ function toggle(aEvent)
   if (gTogglesBySafeTreeId[safeTreeId]) {
     delete gTogglesBySafeTreeId[safeTreeId];
   } else {
     gTogglesBySafeTreeId[safeTreeId] = true;
   }
 }
 
 /**
- * Generates the text for the tree, including its heading.
+ * Appends the elements for the tree, including its heading.
  *
+ * @param aPOuter
+ *        The parent DOM node.
  * @param aT
  *        The tree.
  * @param aProcess
  *        The process the tree corresponds to.
  * @return The generated text.
  */
-function genTreeText(aT, aProcess)
+function appendTreeElements(aPOuter, aT, aProcess)
 {
   var treeBytes = aT._amount;
   var rootStringLength = aT.toString().length;
   var isExplicitTree = aT._unsafeName == 'explicit';
 
   /**
-   * Generates the text for a particular tree, without a heading.
+   * Appends the elements for a particular tree, without a heading.
    *
+   * @param aP
+   *        The parent DOM node.
    * @param aUnsafePrePath
    *        The partial unsafePath 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(aUnsafePrePath, aT, aIndentGuide, aParentStringLength)
+  function appendTreeElements2(aP, aUnsafePrePath, aT, aIndentGuide,
+                               aParentStringLength)
   {
     function repeatStr(aC, aN)
     {
       var s = "";
       for (var i = 0; i < aN; i++) {
         s += aC;
       }
       return s;
     }
 
     // Determine if we should show the sub-tree below this entry;  this
     // involves reinstating any previous toggling of the sub-tree.
     var unsafePath = aUnsafePrePath + aT._unsafeName;
-    var safeTreeId = escapeAll(aProcess + ":" + unsafePath);
+    var safeTreeId = makeSafe(aProcess + ":" + unsafePath);
     var showSubtrees = !aT._hideKids;
     if (gTogglesBySafeTreeId[safeTreeId]) {
       showSubtrees = !showSubtrees;
     }
 
     // Generate the indent.
-    var indent = "<span class='treeLine'>";
+    var indent = "";
     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);
     }
@@ -1132,76 +1143,83 @@ function genTreeText(aT, aProcess)
     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;  detect and record invalid values at the same
     // time.
     var percText = "";
     var tIsInvalid = false;
     if (aT._amount === treeBytes) {
       percText = "100.0";
     } else {
       var perc = (100 * aT._amount / treeBytes);
       if (!(0 <= perc && perc <= 100)) {
         tIsInvalid = true;
         gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath);
       }
       percText = (100 * aT._amount / treeBytes).toFixed(2);
       percText = pad(percText, 5, '0');
     }
-    percText = tIsInvalid ?
-               "<span class='mrPerc invalid'> (" + percText + "%)</span>" :
-               "<span class='mrPerc'> ("         + percText + "%)</span>";
-
-    // 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;
+    percText = " (" + percText + "%)";
 
     // 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;
+
+    appendElementWithText(aP, "span", "treeLine", indent);
+
+    var d;
     if (hasKids) {
-      text += "<span onclick='toggle(event)' class='hasKids' id='" +
-              safeTreeId + "'>";
+      d = appendElement(aP, "span", "hasKids");
+      d.id = safeTreeId;
+      d.onclick = toggle;
+    } else {
+      d = aP;
     }
-    text += genMrValueText(tString, tIsInvalid) + percText;
-    text += genMrNameText(kind, showSubtrees, hasKids, aT._unsafeDescription,
-                          aT._unsafeName, aT._isUnknown, tIsInvalid,
-                          aT._nMerged);
+
+    appendMrValueSpan(d, tString, tIsInvalid);
+    appendElementWithText(d, "span", "mrPerc", percText);
+
+    // We don't want to show '(nonheap)' on a tree like 'map/vsize', since the
+    // whole tree is non-heap.
+    var kind = isExplicitTree ? aT._kind : undefined;
+    appendMrNameSpan(d, kind, showSubtrees, hasKids, aT._unsafeDescription,
+                     aT._unsafeName, aT._isUnknown, tIsInvalid, aT._nMerged);
+    appendTextNode(d, "\n");
+
     if (hasKids) {
-      var hiddenText = showSubtrees ? "" : " hidden";
       // The 'kids' class is just used for sanity checking in toggle().
-      text += "</span><span class='kids" + hiddenText + "'>";
+      d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden");
+    } else {
+      d = aP;
     }
 
     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(unsafePath + "/", aT._kids[i], aIndentGuide,
-                           tString.length);
+      appendTreeElements2(d, unsafePath + "/", aT._kids[i], aIndentGuide,
+                          tString.length);
       aIndentGuide.pop();
     }
-    text += hasKids ? "</span>" : "";
-    return text;
   }
 
-  var text = genTreeText2(/* prePath = */"", aT, [], rootStringLength);
-
-  return genSectionMarkup(aT._unsafeName, text);
+  appendSectionHeader(aPOuter, kTreeNames[aT._unsafeName]);
+ 
+  var pre = appendElement(aPOuter, "pre", "tree");
+  appendTreeElements2(pre, /* prePath = */"", aT, [], rootStringLength);
+  appendTextNode(aPOuter, "\n");  // gives nice spacing when we cut and paste
 }
 
 function OtherReporter(aUnsafePath, aUnits, aAmount, aUnsafeDesc, aNMerged)
 {
   // Nb: _kind is not needed, it's always KIND_OTHER.
   this._unsafePath = aUnsafePath;
   this._units    = aUnits;
   if (aAmount === kUnknown) {
@@ -1242,26 +1260,32 @@ OtherReporter.prototype = {
 
 OtherReporter.compare = function(a, b) {
   return a._unsafePath < b._unsafePath ? -1 :
          a._unsafePath > b._unsafePath ?  1 :
          0;
 };
 
 /**
- * Generates the text for the "Other Measurements" section.
+ * Appends the elements for the "Other Measurements" section.
  *
+ * @param aP
+ *        The parent DOM node.
  * @param aReportersByProcess
  *        Table of Reporters for this process, indexed by _unsafePath.
  * @param aProcess
  *        The process these reporters correspond to.
  * @return The generated text.
  */
-function genOtherText(aReportersByProcess, aProcess)
+function appendOtherElements(aP, aReportersByProcess, aProcess)
 {
+  appendSectionHeader(aP, kTreeNames['other']);
+
+  var pre = appendElement(aP, "pre", "tree");
+
   // Generate an array of Reporter-like elements, stripping out all the
   // 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 unsafePath in aReportersByProcess) {
     var r = aReportersByProcess[unsafePath];
     if (!r._done) {
@@ -1281,37 +1305,36 @@ function genOtherText(aReportersByProces
   // Generate text for the not-yet-printed values.
   var text = "";
   for (var i = 0; i < otherReporters.length; i++) {
     var o = otherReporters[i];
     var oIsInvalid = o.isInvalid();
     if (oIsInvalid) {
       gUnsafePathsWithInvalidValuesForThisProcess.push(o._unsafePath);
     }
-    text += genMrValueText(pad(o._asString, maxStringLength, ' '), oIsInvalid);
-    text += genMrNameText(KIND_OTHER, /* showSubtrees = */true,
-                          /* hasKids = */false, o._unsafeDescription,
-                          o._unsafePath, o._isUnknown, oIsInvalid);
+    appendMrValueSpan(pre, pad(o._asString, maxStringLength, ' '), oIsInvalid);
+    appendMrNameSpan(pre, KIND_OTHER, /* showSubtrees = */true,
+                     /* hasKids = */false, o._unsafeDescription,
+                     o._unsafePath, o._isUnknown, oIsInvalid);
+    appendTextNode(pre, "\n");
   }
 
-  return genSectionMarkup('other', text);
+  appendTextNode(aP, "\n");  // gives nice spacing when we cut and paste
 }
 
-function genSectionMarkup(aName, aText)
+function appendSectionHeader(aP, aText)
 {
-  return "<h2 class='sectionHeader'>" + kTreeNames[aName] + "</h2>\n" +
-         "<pre class='tree'>" + aText + "</pre>\n";
+  appendElementWithText(aP, "h2", "sectionHeader", aText);
+  appendTextNode(aP, "\n");
 }
 
 function assert(aCond, aMsg)
 {
   if (!aCond) {
     throw("assertion failed: " + aMsg);
   }
 }
 
 function debug(x)
 {
-  var content = $("content");
-  var div = document.createElement("div");
-  div.innerHTML = JSON.stringify(x);
-  content.appendChild(div);
+  var content = document.getElementById("content");
+  appendElementWithText(content, "div", "legend", JSON.stringify(x));
 }
--- a/toolkit/components/aboutmemory/content/aboutMemory.xhtml
+++ b/toolkit/components/aboutmemory/content/aboutMemory.xhtml
@@ -40,12 +40,10 @@
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <title>about:memory</title>
     <link rel="stylesheet" href="chrome://global/skin/aboutMemory.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"/>
     <script type="text/javascript" src="chrome://global/content/aboutMemory.js"/>
   </head>
 
-  <!-- No newline before the div element!  This avoids extraneous spaces when
-       pasting the entire output after selecting it with Ctrl-a. -->
-  <body onload="onLoad()" onunload="onUnload()"><div id="content"></div></body>
+  <body id="content" onload="onLoad()" onunload="onUnload()"></body>
 </html>