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 89146 5bbfd1e5ebf73bec45c0a2f0fed6b9310b12238b
parent 89145 b937df325e7184e2454896e15eabde5037c180aa
child 89147 290c4cdaa79636a22e80345b3f97aa19f6f023e1
push idunknown
push userunknown
push dateunknown
reviewersjlebar
bugs722969
milestone13.0a1
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>