Backed out 8 changesets (bug 1499906) for ES Lint failure in builds/worker/checkouts/gecko/toolkit/components/aboutmemory/content/aboutMemory.js
authorNoemi Erli <nerli@mozilla.com>
Fri, 19 Oct 2018 06:00:48 +0300
changeset 490368 be3ea7eb7ff484be2e8e840a64f8d86c6fdd0d65
parent 490367 81ed2232fb09403667483fad07a9ac4dc1d6f2f4
child 490388 a5db777b44d0cd1ceb747fb3bdd8401bffd5a6f5
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
bugs1499906
milestone64.0a1
backs out81ed2232fb09403667483fad07a9ac4dc1d6f2f4
a4e7eb7be03c2d1b51fd3b3ee355720d999fb2a9
eeea7ba794c30def2083f1a7555228b84d7b4626
9819dbed2d88c0aa7f4832a0cee383e68481ebd8
97348d7aa09eace30cab2b769754bfc334f750f7
075198910378e7bd8f3a84969017f178c501dccd
14cef5ef3dc4c9fb2e32b8b43778395ee6471c7d
9d2fa6237585ae36ca4922d8064944c5168883b2
Backed out 8 changesets (bug 1499906) for ES Lint failure in builds/worker/checkouts/gecko/toolkit/components/aboutmemory/content/aboutMemory.js Backed out changeset 81ed2232fb09 (bug 1499906) Backed out changeset a4e7eb7be03c (bug 1499906) Backed out changeset eeea7ba794c3 (bug 1499906) Backed out changeset 9819dbed2d88 (bug 1499906) Backed out changeset 97348d7aa09e (bug 1499906) Backed out changeset 075198910378 (bug 1499906) Backed out changeset 14cef5ef3dc4 (bug 1499906) Backed out changeset 9d2fa6237585 (bug 1499906)
toolkit/components/aboutmemory/content/aboutMemory.js
toolkit/components/aboutmemory/tests/test_aboutmemory.xul
toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
--- a/toolkit/components/aboutmemory/content/aboutMemory.js
+++ b/toolkit/components/aboutmemory/content/aboutMemory.js
@@ -13,17 +13,17 @@
 // "file=" argument, and obviously the filename is case-sensitive iff you're on
 // a case-sensitive filesystem.  If you specify more than one "file=" argument,
 // only the first one is used.
 
 "use strict";
 
 // ---------------------------------------------------------------------------
 
-let CC = Components.Constructor;
+var CC = Components.Constructor;
 
 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;
@@ -43,25 +43,25 @@ XPCOMUtils.defineLazyGetter(this, "nsBin
                                      "setInputStream"));
 XPCOMUtils.defineLazyGetter(this, "nsFile",
                             () => CC("@mozilla.org/file/local;1",
                                      "nsIFile", "initWithPath"));
 XPCOMUtils.defineLazyGetter(this, "nsGzipConverter",
                             () => CC("@mozilla.org/streamconv;1?from=gzip&to=uncompressed",
                                      "nsIStreamConverter"));
 
-let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
+var gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
              .getService(Ci.nsIMemoryReporterManager);
 
 const gPageName = "about:memory";
 document.title = gPageName;
 
 const gUnnamedProcessStr = "Main Process";
 
-let gIsDiff = false;
+var gIsDiff = false;
 
 // ---------------------------------------------------------------------------
 
 // Forward slashes in URLs in paths are represented with backslashes to avoid
 // being mistaken for path separators.  Paths/names where this hasn't been
 // undone are prefixed with "unsafe"; the rest are prefixed with "safe".
 function flipBackslashes(aUnsafeStr) {
   // Save memory by only doing the replacement if it's necessary.
@@ -79,76 +79,76 @@ function assert(aCond, aMsg) {
     reportAssertionFailure(aMsg);
     throw new Error(gAssertionFailureMsgPrefix + aMsg);
   }
 }
 
 // This is used for malformed input from memory reporters.
 function assertInput(aCond, aMsg) {
   if (!aCond) {
-    throw new Error(`Invalid memory report(s): ${aMsg}`);
+    throw new Error("Invalid memory report(s): " + aMsg);
   }
 }
 
-function handleException(aEx) {
-  let str = "" + aEx;
+function handleException(ex) {
+  let str = "" + ex;
   if (str.startsWith(gAssertionFailureMsgPrefix)) {
     // Argh, assertion failure within this file!  Give up.
-    throw aEx;
+    throw ex;
   } else {
     // File or memory reporter problem.  Print a message.
     updateMainAndFooter(str, NO_TIMESTAMP, HIDE_FOOTER, "badInputWarning");
   }
 }
 
 function reportAssertionFailure(aMsg) {
   let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
   if (debug.isDebugBuild) {
     debug.assertion(aMsg, "false", "aboutMemory.js", 0);
   }
 }
 
-function debug(aVal) {
+function debug(x) {
   let section = appendElement(document.body, "div", "section");
-  appendElementWithText(section, "div", "debug", JSON.stringify(aVal));
+  appendElementWithText(section, "div", "debug", JSON.stringify(x));
 }
 
 // ---------------------------------------------------------------------------
 
 function onUnload() {
 }
 
 // ---------------------------------------------------------------------------
 
 // The <div> holding everything but the header and footer (if they're present).
 // It's what is updated each time the page changes.
-let gMain;
+var gMain;
 
 // The <div> holding the footer.
-let gFooter;
+var gFooter;
 
 // The "verbose" checkbox.
-let gVerbose;
+var gVerbose;
 
 // The "anonymize" checkbox.
-let gAnonymize;
+var gAnonymize;
 
 // Values for the |aFooterAction| argument to updateTitleMainAndFooter.
 const HIDE_FOOTER = 0;
 const SHOW_FOOTER = 1;
 
 // Values for the |aShowTimestamp| argument to updateTitleMainAndFooter.
 const NO_TIMESTAMP = 0;
 const SHOW_TIMESTAMP = 1;
 
 function updateTitleMainAndFooter(aTitleNote, aMsg, aShowTimestamp,
                                   aFooterAction, aClassName) {
   document.title = gPageName;
   if (aTitleNote) {
-    document.title += ` (${aTitleNote})`;
+    document.title += " (" + aTitleNote + ")";
   }
 
   // Clear gMain by replacing it with an empty node.
   let tmp = gMain.cloneNode(false);
   gMain.parentNode.replaceChild(tmp, gMain);
   gMain = tmp;
 
   gMain.classList.remove("hidden");
@@ -163,17 +163,17 @@ function updateTitleMainAndFooter(aTitle
     let className = "section";
     if (aClassName) {
       className = className + " " + aClassName;
     }
     if (aShowTimestamp == SHOW_TIMESTAMP) {
       // JS has many options for pretty-printing timestamps. We use
       // toISOString() because it has sub-second granularity, which is useful
       // if you quickly and repeatedly click one of the buttons.
-      aMsg += ` (${(new Date()).toISOString()})`;
+      aMsg += " (" + (new Date()).toISOString() + ")";
     }
     msgElement = appendElementWithText(gMain, "div", className, aMsg);
   }
 
   switch (aFooterAction) {
    case HIDE_FOOTER: gFooter.classList.add("hidden"); break;
    case SHOW_FOOTER: gFooter.classList.remove("hidden"); break;
    default: assert(false, "bad footer action in updateTitleMainAndFooter");
@@ -259,27 +259,28 @@ function onLoad() {
   // A hidden file input element that can be invoked when necessary.
   let fileInput1 = appendHiddenFileInput(header, "fileInput1", function() {
     let file = this.files[0];
     let filename = file.mozFullPath;
     updateAboutMemoryFromFile(filename);
   });
 
   // Ditto.
-  let fileInput2 = appendHiddenFileInput(header, "fileInput2", function(aElem) {
+  let fileInput2 =
+      appendHiddenFileInput(header, "fileInput2", function(e) {
     let file = this.files[0];
     // First time around, we stash a copy of the filename and reinvoke.  Second
     // time around we do the diff and display.
     if (!this.filename1) {
       this.filename1 = file.mozFullPath;
 
-      // aElem.skipClick is only true when testing -- it allows fileInput2's
+      // e.skipClick is only true when testing -- it allows fileInput2's
       // onchange handler to be re-called without having to go via the file
       // picker.
-      if (!aElem.skipClick) {
+      if (!e.skipClick) {
         this.click();
       }
     } else {
       let filename1 = this.filename1;
       delete this.filename1;
       updateAboutMemoryFromTwoFiles(filename1, file.mozFullPath);
     }
   });
@@ -319,26 +320,29 @@ function onLoad() {
   let labelDiv1 =
    appendElementWithText(row1, "div", "opsRowLabel", "Show memory reports");
   let label1 = appendElementWithText(labelDiv1, "label", "");
   gVerbose = appendElement(label1, "input", "");
   gVerbose.type = "checkbox";
   gVerbose.id = "verbose"; // used for testing
   appendTextNode(label1, "verbose");
 
+  const kEllipsis = "\u2026";
+
   // The "measureButton" id is used for testing.
   appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton");
-  appendButton(row1, LdDesc, () => fileInput1.click(), "Load…");
-  appendButton(row1, DfDesc, () => fileInput2.click(), "Load and diff…");
+  appendButton(row1, LdDesc, () => fileInput1.click(), "Load" + kEllipsis);
+  appendButton(row1, DfDesc, () => fileInput2.click(),
+               "Load and diff" + kEllipsis);
 
   let row2 = appendElement(ops, "div", "opsRow");
 
   let labelDiv2 =
     appendElementWithText(row2, "div", "opsRowLabel", "Save memory reports");
-  appendButton(row2, SvDesc, saveReportsToFile, "Measure and save…");
+  appendButton(row2, SvDesc, saveReportsToFile, "Measure and save" + kEllipsis);
 
   // XXX: this isn't a great place for this checkbox, but I can't think of
   // anywhere better.
   let label2 = appendElementWithText(labelDiv2, "label", "");
   gAnonymize = appendElement(label2, "input", "");
   gAnonymize.type = "checkbox";
   appendTextNode(label2, "anonymize");
 
@@ -396,19 +400,19 @@ function onLoad() {
   appendElementWithText(gFooter, "div", "legend hiddenOnMobile", legendText2);
 
   // See if we're loading from a file.  (Because about:memory is a non-standard
   // URL, location.search is undefined, so we have to use location.href
   // instead.)
   let search = location.href.split("?")[1];
   if (search) {
     let searchSplit = search.split("&");
-    for (let s of searchSplit) {
-      if (s.toLowerCase().startsWith("file=")) {
-        let filename = s.substring("file=".length);
+    for (let i = 0; i < searchSplit.length; i++) {
+      if (searchSplit[i].toLowerCase().startsWith("file=")) {
+        let filename = searchSplit[i].substring("file=".length);
         updateAboutMemoryFromFile(decodeURIComponent(filename));
         return;
       }
     }
   }
 }
 
 // ---------------------------------------------------------------------------
@@ -467,23 +471,23 @@ function doDMD() {
 function dumpGCLogAndCCLog(aVerbose) {
   let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
                 .getService(Ci.nsIMemoryInfoDumper);
 
   let inProgress = updateMainAndFooter("Saving logs...",
                                        NO_TIMESTAMP, HIDE_FOOTER);
   let section = appendElement(gMain, "div", "section");
 
-  function displayInfo(aGCLog, aCCLog, aIsParent) {
+  function displayInfo(gcLog, ccLog, isParent) {
     appendElementWithText(section, "div", "",
-                          "Saved GC log to " + aGCLog.path);
+                          "Saved GC log to " + gcLog.path);
 
     let ccLogType = aVerbose ? "verbose" : "concise";
     appendElementWithText(section, "div", "",
-                          "Saved " + ccLogType + " CC log to " + aCCLog.path);
+                          "Saved " + ccLogType + " CC log to " + ccLog.path);
   }
 
   dumper.dumpGCAndCCLogsToFile("", aVerbose, /* dumpChildProcesses = */ true,
                                { onDump: displayInfo,
                                  onFinish() {
                                    inProgress.remove();
                                  },
                                });
@@ -521,17 +525,17 @@ function updateAboutMemoryFromReporters(
 
   } catch (ex) {
     handleException(ex);
   }
 }
 
 // Increment this if the JSON format changes.
 //
-let gCurrentFileFormatVersion = 1;
+var gCurrentFileFormatVersion = 1;
 
 
 /**
  * Parse a string as JSON and extract the |memory_report| property if it has
  * one, which indicates the string is from a crash dump.
  *
  * @param aStr
  *        The string.
@@ -560,17 +564,18 @@ function updateAboutMemoryFromJSONObject
                 "data version number missing or doesn't match");
     assertInput(aObj.hasMozMallocUsableSize !== undefined,
                 "missing 'hasMozMallocUsableSize' property");
     assertInput(aObj.reports && aObj.reports instanceof Array,
                 "missing or non-array 'reports' property");
 
     let processMemoryReportsFromFile =
         function(aHandleReport, aDisplayReports) {
-      for (let r of aObj.reports) {
+      for (let i = 0; i < aObj.reports.length; i++) {
+        let r = aObj.reports[i];
         aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
                       r.description, r._presence);
       }
       aDisplayReports();
     };
     appendAboutMemoryMain(processMemoryReportsFromFile,
                           aObj.hasMozMallocUsableSize);
   } catch (ex) {
@@ -676,17 +681,17 @@ function updateAboutMemoryFromFile(aFile
  * diffs them.
  *
  * @param aFilename1
  *        The name of the first file being read from.
  * @param aFilename2
  *        The name of the first file being read from.
  */
 function updateAboutMemoryFromTwoFiles(aFilename1, aFilename2) {
-  let titleNote = `diff of ${aFilename1} and ${aFilename2}`;
+  let titleNote = "diff of " + aFilename1 + " and " + aFilename2;
   loadMemoryReportsFromFile(aFilename1, titleNote, function(aStr1) {
     loadMemoryReportsFromFile(aFilename2, titleNote, function(aStr2) {
       try {
         let obj1 = parseAndUnwrapIfCrashDump(aStr1);
         let obj2 = parseAndUnwrapIfCrashDump(aStr2);
         gIsDiff = true;
         updateAboutMemoryFromJSONObject(diffJSONObjects(obj1, obj2));
         gIsDiff = false;
@@ -695,17 +700,17 @@ function updateAboutMemoryFromTwoFiles(a
       }
     });
   });
 }
 
 // ---------------------------------------------------------------------------
 
 // Something unlikely to appear in a process name.
-let kProcessPathSep = "^:^:^";
+var kProcessPathSep = "^:^:^";
 
 // Short for "diff report".
 function DReport(aKind, aUnits, aAmount, aDescription, aNMerged, aPresence) {
   this._kind = aKind;
   this._units = aUnits;
   this._amount = aAmount;
   this._description = aDescription;
   this._nMerged = aNMerged;
@@ -765,17 +770,19 @@ DReport.ADDED_FOR_BALANCE = 3;
  * DReport objects for values.
  *
  * @param aJSONReports
  *        The |reports| field of a JSON object.
  * @return The constructed report map.
  */
 function makeDReportMap(aJSONReports) {
   let dreportMap = {};
-  for (let jr of aJSONReports) {
+  for (let i = 0; i < aJSONReports.length; i++) {
+    let jr = aJSONReports[i];
+
     assert(jr.process !== undefined, "Missing process");
     assert(jr.path !== undefined, "Missing path");
     assert(jr.kind !== undefined, "Missing kind");
     assert(jr.units !== undefined, "Missing units");
     assert(jr.amount !== undefined, "Missing amount");
     assert(jr.description !== undefined, "Missing description");
 
     // Strip out some non-deterministic stuff that prevents clean diffs.
@@ -1000,18 +1007,18 @@ function appendAboutMemoryMain(aProcessR
     }
   }
 
   function displayReports() {
     // Sort the processes.
     let processes = Object.keys(pcollsByProcess);
     processes.sort(function(aProcessA, aProcessB) {
       assert(aProcessA != aProcessB,
-             `Elements of Object.keys() should be unique, but ` +
-             `saw duplicate '${aProcessA}' elem.`);
+             "Elements of Object.keys() should be unique, but " +
+             "saw duplicate '" + aProcessA + "' elem.");
 
       // Always put the main process first.
       if (aProcessA == gUnnamedProcessStr) {
         return -1;
       }
       if (aProcessB == gUnnamedProcessStr) {
         return 1;
       }
@@ -1036,17 +1043,18 @@ function appendAboutMemoryMain(aProcessR
       if (aProcessA > aProcessB) {
         return 1;
       }
 
       return 0;
     });
 
     // Generate output for each process.
-    for (let [i, process] of processes.entries()) {
+    for (let i = 0; i < processes.length; i++) {
+      let process = processes[i];
       let section = appendElement(gMain, "div", "section");
 
       appendProcessAboutMemoryElements(section, i, process,
                                        pcollsByProcess[process]._trees,
                                        pcollsByProcess[process]._degenerates,
                                        pcollsByProcess[process]._heapTotal,
                                        aHasMozMallocUsableSize);
     }
@@ -1082,19 +1090,19 @@ function TreeNode(aUnsafeName, aUnits, a
   // - _description
   // - _hideKids (only defined if true)
   // - _maxAbsDescendant (on-demand, only when gIsDiff is set)
 }
 
 TreeNode.prototype = {
   findKid(aUnsafeName) {
     if (this._kids) {
-      for (let kid of this._kids) {
-        if (kid._unsafeName === aUnsafeName) {
-          return kid;
+      for (let i = 0; i < this._kids.length; i++) {
+        if (this._kids[i]._unsafeName === aUnsafeName) {
+          return this._kids[i];
         }
       }
     }
     return undefined;
   },
 
   // When gIsDiff is false, tree operations -- sorting and determining if a
   // sub-tree is significant -- are straightforward. But when gIsDiff is true,
@@ -1110,28 +1118,28 @@ TreeNode.prototype = {
 
     if ("_maxAbsDescendant" in this) {
       // We've computed this before? Return the saved value.
       return this._maxAbsDescendant;
     }
 
     // Compute the maximum absolute value of all descendants.
     let max = Math.abs(this._amount);
-    for (let kid of this._kids) {
-      max = Math.max(max, kid.maxAbsDescendant());
+    for (let i = 0; i < this._kids.length; i++) {
+      max = Math.max(max, this._kids[i].maxAbsDescendant());
     }
     this._maxAbsDescendant = max;
     return max;
   },
 
   toString() {
     switch (this._units) {
       case UNITS_BYTES: return formatBytes(this._amount);
       case UNITS_COUNT:
-      case UNITS_COUNT_CUMULATIVE: return formatNum(this._amount);
+      case UNITS_COUNT_CUMULATIVE: return formatInt(this._amount);
       case UNITS_PERCENTAGE: return formatPercentage(this._amount);
       default:
         throw "Invalid memory report(s): bad units in TreeNode.toString";
     }
   },
 };
 
 // Sort TreeNodes first by size, then by name.  The latter is important for the
@@ -1190,18 +1198,18 @@ function fillInTree(aRoot) {
         aT._nMerged = kid._nMerged;
       }
       assert(!aT._hideKids && !kid._hideKids, "_hideKids set when merging");
 
     } else {
       // Non-leaf node with multiple children.  Derive its _amount and
       // _description entirely from its children...
       let kidsBytes = 0;
-      for (let kid of aT._kids) {
-        kidsBytes += fillInNonLeafNodes(kid);
+      for (let i = 0; i < aT._kids.length; i++) {
+        kidsBytes += fillInNonLeafNodes(aT._kids[i]);
       }
 
       // ... except in one special case. When diffing two memory report sets,
       // if one set has a node with children and the other has the same node
       // but without children -- e.g. the first has "a/b/c" and "a/b/d", but
       // the second only has "a/b" -- we need to add a fake node "a/b/(fake)"
       // to the second to make the trees comparable. It's ugly, but it works.
       if (aT._amount !== undefined &&
@@ -1290,67 +1298,67 @@ function sortTreeAndInsertAggregateNodes
 
   aT._kids.sort(TreeNode.compareAmounts);
 
   // 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 (let kid of aT._kids) {
-      sortTreeAndInsertAggregateNodes(aTotalBytes, kid);
+    for (let i = 0; i < aT._kids.length; i++) {
+      sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
     }
     return;
   }
 
   // Look at all children except the last one.
   let i;
   for (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.
       let i0 = i;
       let nAgg = aT._kids.length - i0;
       // Create an aggregate node.  Inherit units from the parent;  everything
       // in the tree should have the same units anyway (we test this later).
-      let aggT = new TreeNode(`(${nAgg} tiny)`, aT._units);
+      let aggT = new TreeNode("(" + nAgg + " tiny)", aT._units);
       aggT._kids = [];
       let aggBytes = 0;
       for ( ; i < aT._kids.length; i++) {
         aggBytes += aT._kids[i]._amount;
         aggT._kids.push(aT._kids[i]);
       }
       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.compareAmounts);
 
       // Process the moved children.
-      for (let kid of aggT._kids) {
-        sortTreeAndInsertAggregateNodes(aTotalBytes, kid);
+      for (i = 0; i < aggT._kids.length; i++) {
+        sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]);
       }
       return;
     }
 
     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]);
 }
 
 // Global variable indicating if we've seen any invalid values for this
 // process;  it holds the unsafePaths of any such reports.  It is reset for
 // each new process.
-let gUnsafePathsWithInvalidValuesForThisProcess = [];
+var gUnsafePathsWithInvalidValuesForThisProcess = [];
 
 function appendWarningElements(aP, aHasKnownHeapAllocated,
                                aHasMozMallocUsableSize) {
   if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) {
     appendElementWithText(aP, "p", "",
       "WARNING: the 'heap-allocated' memory reporter and the " +
       "moz_malloc_usable_size() function do not work for this platform " +
       "and/or configuration.  This means that 'heap-unclassified' is not " +
@@ -1406,29 +1414,32 @@ function appendWarningElements(aP, aHasK
  *        The table of degenerate trees for this process.
  * @param aHasMozMallocUsableSize
  *        Boolean indicating if moz_malloc_usable_size works.
  * @return The generated text.
  */
 function appendProcessAboutMemoryElements(aP, aN, aProcess, aTrees,
                                           aDegenerates, aHeapTotal,
                                           aHasMozMallocUsableSize) {
+  const kUpwardsArrow   = "\u2191",
+        kDownwardsArrow = "\u2193";
+
   let appendLink = function(aHere, aThere, aArrow) {
     let link = appendElementWithText(aP, "a", "upDownArrow", aArrow);
     link.href = "#" + aThere + aN;
     link.id = aHere + aN;
-    link.title = `Go to the ${aThere} of ${aProcess}`;
+    link.title = "Go to the " + aThere + " of " + aProcess;
     link.style = "text-decoration: none";
 
     // This gives nice spacing when we copy and paste.
     appendElementWithText(aP, "span", "", "\n");
   };
 
   appendElementWithText(aP, "h1", "", aProcess);
-  appendLink("start", "end", "↓");
+  appendLink("start", "end", kDownwardsArrow);
 
   // We'll fill this in later.
   let warningsDiv = appendElement(aP, "div", "accuracyWarning");
 
   // The explicit tree.
   let hasExplicitTree;
   let hasKnownHeapAllocated;
   {
@@ -1477,130 +1488,168 @@ function appendProcessAboutMemoryElement
       maxStringLength = length;
     }
     otherDegenerates.push(t);
   }
   otherDegenerates.sort(TreeNode.compareUnsafeNames);
 
   // Now generate the elements, putting non-degenerate trees first.
   let pre = appendSectionHeader(aP, "Other Measurements");
-  for (let t of otherTrees) {
+  for (let i = 0; i < otherTrees.length; i++) {
+    let t = otherTrees[i];
     appendTreeElements(pre, t, aProcess, "");
     appendTextNode(pre, "\n"); // blank lines after non-degenerate trees
   }
-  for (let t of otherDegenerates) {
-    let padText = "".padStart(maxStringLength - t.toString().length, " ");
+  for (let i = 0; i < otherDegenerates.length; i++) {
+    let t = otherDegenerates[i];
+    let padText = pad("", maxStringLength - t.toString().length, " ");
     appendTreeElements(pre, t, aProcess, padText);
   }
   appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
 
   // Add any warnings about inaccuracies in the "explicit" tree due to platform
   // limitations.  These must be computed after generating all the text.  The
   // newlines give nice spacing if we copy+paste into a text buffer.
   if (hasExplicitTree) {
     appendWarningElements(warningsDiv, hasKnownHeapAllocated,
                           aHasMozMallocUsableSize);
   }
 
   appendElementWithText(aP, "h3", "", "End of " + aProcess);
-  appendLink("end", "start", "↑");
+  appendLink("end", "start", kUpwardsArrow);
 }
 
-// Used for UNITS_BYTES values that are printed as MiB.
-const kMBStyle = {
-  minimumFractionDigits: 2,
-  maximumFractionDigits: 2
-};
-
-// Used for UNITS_PERCENTAGE values.
-const kPercStyle = {
-  style: "percent",
-  minimumFractionDigits: 2,
-  maximumFractionDigits: 2
-};
-
-// Used for fractions within the tree.
-const kFracStyle = {
-  style: "percent",
-  minimumIntegerDigits: 2,
-  minimumFractionDigits: 2,
-  maximumFractionDigits: 2
-};
-
-// Used for special-casing 100% fractions within the tree.
-const kFrac1Style = {
-  style: "percent",
-  minimumIntegerDigits: 3,
-  minimumFractionDigits: 1,
-  maximumFractionDigits: 1
-};
+/**
+ * Determines if a number has a negative sign when converted to a string.
+ * Works even for -0.
+ *
+ * @param aN
+ *        The number.
+ * @return A boolean.
+ */
+function hasNegativeSign(aN) {
+  if (aN === 0) { // this succeeds for 0 and -0
+    return 1 / aN === -Infinity; // this succeeds for -0
+  }
+  return aN < 0;
+}
 
 /**
  * Formats an int as a human-readable string.
  *
  * @param aN
  *        The integer to format.
- * @param aOptions
- *        Optional options object.
+ * @param aExtra
+ *        An extra string to tack onto the end.
  * @return A human-readable string representing the int.
+ *
+ * Note: building an array of chars and converting that to a string with
+ * Array.join at the end is more memory efficient than using string
+ * concatenation.  See bug 722972 for details.
  */
-function formatNum(aN, aOptions) {
-  return aN.toLocaleString('en-US', aOptions);
+function formatInt(aN, aExtra) {
+  let neg = false;
+  if (hasNegativeSign(aN)) {
+    neg = true;
+    aN = -aN;
+  }
+  let s = [];
+  while (true) {
+    let k = aN % 1000;
+    aN = Math.floor(aN / 1000);
+    if (aN > 0) {
+      if (k < 10) {
+        s.unshift(",00", k);
+      } else if (k < 100) {
+        s.unshift(",0", k);
+      } else {
+        s.unshift(",", k);
+      }
+    } else {
+      s.unshift(k);
+      break;
+    }
+  }
+  if (neg) {
+    s.unshift("-");
+  }
+  if (aExtra) {
+    s.push(aExtra);
+  }
+  return s.join("");
 }
 
 /**
  * Converts a byte count to an appropriate string representation.
  *
  * @param aBytes
  *        The byte count.
  * @return The string representation.
  */
 function formatBytes(aBytes) {
-  return gVerbose.checked
-       ? `${formatNum(aBytes)} B`
-       : `${formatNum(aBytes / (1024 * 1024), kMBStyle)} MB`;
+  let unit = gVerbose.checked ? " B" : " MB";
+
+  let s;
+  if (gVerbose.checked) {
+    s = formatInt(aBytes, unit);
+  } else {
+    let mbytes = (aBytes / (1024 * 1024)).toFixed(2);
+    let a = String(mbytes).split(".");
+    // If the argument to formatInt() is -0, it will print the negative sign.
+    s = formatInt(Number(a[0])) + "." + a[1] + unit;
+  }
+  return s;
 }
 
 /**
- * Converts a UNITS_PERCENTAGE value to an appropriate string representation.
+ * Converts a percentage to an appropriate string representation.
  *
  * @param aPerc100x
  *        The percentage, multiplied by 100 (see nsIMemoryReporter).
  * @return The string representation
  */
 function formatPercentage(aPerc100x) {
-  // A percentage like 12.34% will have an aPerc100x value of 1234, and we need
-  // to divide that by 10,000 to get the 0.1234 that toLocaleString() wants.
-  return formatNum(aPerc100x / 10000, kPercStyle);
+  return (aPerc100x / 100).toFixed(2) + "%";
 }
 
-/*
- * Converts a tree fraction to an appropriate string representation.
+/**
+ * Right-justifies a string in a field of a given width, padding as necessary.
  *
- * @param aNum
- *        The numerator.
- * @param aDenom
- *        The denominator.
- * @return The string representation
+ * @param aS
+ *        The string.
+ * @param aN
+ *        The field width.
+ * @param aC
+ *        The char used to pad.
+ * @return The string representation.
  */
-function formatTreeFrac(aNum, aDenom) {
-  // Two special behaviours here:
-  // - We treat 0 / 0 as 100%.
-  // - We want 4 digits, as much as possible, because it gives good vertical
-  //   alignment. For positive numbers, 00.00%--99.99% works straighforwardly,
-  //   but 100.0% needs special handling.
-  let num = aDenom === 0 ? 1 : (aNum / aDenom);
-  return (0.99995 <= num && num <= 1)
-         ? formatNum(1, kFrac1Style)
-         : formatNum(num, kFracStyle);
+function pad(aS, aN, aC) {
+  let padding = "";
+  let n2 = aN - aS.length;
+  for (let i = 0; i < n2; i++) {
+    padding += aC;
+  }
+  return padding + aS;
 }
 
-const kNoKidsSep   = " ── ",
-      kHideKidsSep = " ++ ",
-      kShowKidsSep = " -- ";
+// There's a subset of the Unicode "light" box-drawing chars that is widely
+// implemented in terminals, and this code sticks to that subset to maximize
+// the chance that copying and pasting about:memory output to a terminal will
+// work correctly.
+const kHorizontal                   = "\u2500",
+      kVertical                     = "\u2502",
+      kUpAndRight                   = "\u2514",
+      kUpAndRight_Right_Right       = "\u2514\u2500\u2500",
+      kVerticalAndRight             = "\u251c",
+      kVerticalAndRight_Right_Right = "\u251c\u2500\u2500",
+      kVertical_Space_Space         = "\u2502  ";
+
+const kNoKidsSep                    = " \u2500\u2500 ",
+      kHideKidsSep                  = " ++ ",
+      kShowKidsSep                  = " -- ";
 
 function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged,
                           aPresence) {
   let safeName = flipBackslashes(aUnsafeName);
   if (!aIsInvalid && !aNMerged && !aPresence) {
     safeName += "\n";
   }
   let nameSpan = appendElementWithText(aP, "span", "mrName", safeName);
@@ -1613,17 +1662,17 @@ function appendMrNameSpan(aP, aDescripti
     }
     let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
     noteSpan.title =
       "Warning: this value is invalid and indicates a bug in one or more " +
       "memory reporters. ";
   }
 
   if (aNMerged) {
-    let noteText = ` [${aNMerged}]`;
+    let noteText = " [" + aNMerged + "]";
     if (!aPresence) {
       noteText += "\n";
     }
     let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
     noteSpan.title =
       "This value is the sum of " + aNMerged +
       " memory reports that all have the same path.";
   }
@@ -1643,33 +1692,34 @@ function appendMrNameSpan(aP, aDescripti
       c = "!";
       title = "One of the sets of memory reports lacked children for this " +
               "node's parent. This is a fake child node added to make the " +
               "two memory sets comparable.";
       break;
      default: assert(false, "bad presence");
       break;
     }
-    let noteSpan = appendElementWithText(aP, "span", "mrNote", ` [${c}]\n`);
+    let noteSpan = appendElementWithText(aP, "span", "mrNote",
+                                         " [" + c + "]\n");
     noteSpan.title = title;
   }
 }
 
 // This is used to record the (safe) IDs of which sub-trees have been manually
 // expanded (marked as true) and collapsed (marked as false).  It's used to
 // replicate the collapsed/expanded state when the page is updated.  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.
-let gShowSubtreesBySafeTreeId = {};
+var gShowSubtreesBySafeTreeId = {};
 
-function assertClassListContains(aElem, aClassName) {
-  assert(aElem, "undefined " + aClassName);
-  assert(aElem.classList.contains(aClassName), "classname isn't " + aClassName);
+function assertClassListContains(e, className) {
+  assert(e, "undefined " + className);
+  assert(e.classList.contains(className), "classname isn't " + className);
 }
 
 function toggle(aEvent) {
   // This relies on each line being a span that contains at least four spans:
   // mrValue, mrPerc, mrSep, mrName, and then zero or more mrNotes.  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.
@@ -1733,82 +1783,85 @@ function expandPathToThisElement(aElemen
  *        The tree root.
  * @param aProcess
  *        The process the tree corresponds to.
  * @param aPadText
  *        A string to pad the start of each entry.
  */
 function appendTreeElements(aP, aRoot, aProcess, aPadText) {
   /**
-   * Appends the elements for a particular tree, without a heading. There's a
-   * subset of the Unicode "light" box-drawing chars that is widely implemented
-   * in terminals, and this code sticks to that subset to maximize the chance
-   * that copying and pasting about:memory output to a terminal will work
-   * correctly.
+   * Appends the elements for a particular tree, without a heading.
    *
    * @param aP
    *        The parent DOM node.
    * @param aProcess
    *        The process the tree corresponds to.
    * @param aUnsafeNames
    *        An array of the names forming the path to aT.
    * @param aRoot
    *        The root of the tree this sub-tree belongs to.
    * @param aT
    *        The tree.
-   * @param aTlThis
-   *        The treeline for this entry.
-   * @param aTlKids
-   *        The treeline for this entry's children.
+   * @param aTreelineText1
+   *        The first part of the treeline for this entry and this entry's
+   *        children.
+   * @param aTreelineText2a
+   *        The second part of the treeline for this entry.
+   * @param aTreelineText2b
+   *        The second part of the treeline for this entry's children.
    * @param aParentStringLength
    *        The length of the formatted byte count of the top node in the tree.
    */
   function appendTreeElements2(aP, aProcess, aUnsafeNames, aRoot, aT,
-                               aTlThis, aTlKids, aParentStringLength) {
+                               aTreelineText1, aTreelineText2a,
+                               aTreelineText2b, aParentStringLength) {
     function appendN(aS, aC, aN) {
       for (let i = 0; i < aN; i++) {
         aS += aC;
       }
       return aS;
     }
 
     // The tree line.  Indent more if this entry is narrower than its parent.
     let valueText = aT.toString();
-    let extraTlLength =
+    let extraTreelineLength =
       Math.max(aParentStringLength - valueText.length, 0);
-    if (extraTlLength > 0) {
-      aTlThis = appendN(aTlThis, "─", extraTlLength);
-      aTlKids = appendN(aTlKids, " ", extraTlLength);
+    if (extraTreelineLength > 0) {
+      aTreelineText2a =
+        appendN(aTreelineText2a, kHorizontal, extraTreelineLength);
+      aTreelineText2b =
+        appendN(aTreelineText2b, " ", extraTreelineLength);
     }
-    appendElementWithText(aP, "span", "treeline", aTlThis);
+    let treelineText = aTreelineText1 + aTreelineText2a;
+    appendElementWithText(aP, "span", "treeline", treelineText);
 
     // Detect and record invalid values.  But not if gIsDiff is true, because
     // we expect negative values in that case.
     assertInput(aRoot._units === aT._units,
                 "units within a tree are inconsistent");
     let tIsInvalid = false;
     if (!gIsDiff && !(0 <= aT._amount && aT._amount <= aRoot._amount)) {
       tIsInvalid = true;
       let unsafePath = aUnsafeNames.join("/");
       gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath);
-      reportAssertionFailure(
-        `Invalid value (${aT._amount} / ${aRoot._amount}) for ` +
-        flipBackslashes(unsafePath));
+      reportAssertionFailure("Invalid value (" + aT._amount + " / " +
+                             aRoot._amount + ") for " +
+                             flipBackslashes(unsafePath));
     }
 
     // For non-leaf nodes, the entire sub-tree is put within a span so it can
     // be collapsed if the node is clicked on.
     let d;
     let sep;
     let showSubtrees;
     if (aT._kids) {
       // Determine if we should show the sub-tree below this entry;  this
       // involves reinstating any previous toggling of the sub-tree.
       let unsafePath = aUnsafeNames.join("/");
-      let safeTreeId = `${aProcess}:${flipBackslashes(unsafePath)}`;
+      let safeTreeId = aProcess + ":" + flipBackslashes(unsafePath);
       showSubtrees = !aT._hideKids;
       if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
         showSubtrees = gShowSubtreesBySafeTreeId[safeTreeId];
       }
       d = appendElement(aP, "span", "hasKids");
       d.id = safeTreeId;
       d.onclick = toggle;
       sep = showSubtrees ? kShowKidsSep : kHideKidsSep;
@@ -1818,19 +1871,25 @@ function appendTreeElements(aP, aRoot, a
       d = aP;
     }
 
     // The value.
     appendElementWithText(d, "span", "mrValue" + (tIsInvalid ? " invalid" : ""),
                           valueText);
 
     // The percentage (omitted for single entries).
+    let percText;
     if (!aT._isDegenerate) {
-      let percText = formatTreeFrac(aT._amount, aRoot._amount);
-      appendElementWithText(d, "span", "mrPerc", ` (${percText})`);
+      // Treat 0 / 0 as 100%.
+      let num = aRoot._amount === 0 ? 100 : (100 * aT._amount / aRoot._amount);
+      let numText = num.toFixed(2);
+      percText = numText === "100.00"
+               ? " (100.0%)"
+               : (0 <= num && num < 10 ? " (0" : " (") + numText + "%)";
+      appendElementWithText(d, "span", "mrPerc", percText);
     }
 
     // The separator.
     appendElementWithText(d, "span", "mrSep", sep);
 
     // The entry's name.
     appendMrNameSpan(d, aT._description, aT._unsafeName,
                      tIsInvalid, aT._nMerged, aT._presence);
@@ -1841,39 +1900,38 @@ function appendTreeElements(aP, aRoot, a
       expandPathToThisElement(d);
     }
 
     // Recurse over children.
     if (aT._kids) {
       // The 'kids' class is just used for sanity checking in toggle().
       d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden");
 
-      let tlThisForMost, tlKidsForMost;
-      if (aT._kids.length > 1) {
-        tlThisForMost = aTlKids + "├──";
-        tlKidsForMost = aTlKids + "│  ";
-      }
-      let tlThisForLast = aTlKids + "└──";
-      let tlKidsForLast = aTlKids + "   ";
-
-      for (let [i, kid] of aT._kids.entries()) {
-        let isLast = i == aT._kids.length - 1;
-        aUnsafeNames.push(kid._unsafeName);
-        appendTreeElements2(d, aProcess, aUnsafeNames, aRoot, kid,
-                            !isLast ? tlThisForMost : tlThisForLast,
-                            !isLast ? tlKidsForMost : tlKidsForLast,
-                            valueText.length);
+      let kidTreelineText1 = aTreelineText1 + aTreelineText2b;
+      for (let i = 0; i < aT._kids.length; i++) {
+        let kidTreelineText2a, kidTreelineText2b;
+        if (i < aT._kids.length - 1) {
+          kidTreelineText2a = kVerticalAndRight_Right_Right;
+          kidTreelineText2b = kVertical_Space_Space;
+        } else {
+          kidTreelineText2a = kUpAndRight_Right_Right;
+          kidTreelineText2b = "   ";
+        }
+        aUnsafeNames.push(aT._kids[i]._unsafeName);
+        appendTreeElements2(d, aProcess, aUnsafeNames, aRoot, aT._kids[i],
+                            kidTreelineText1, kidTreelineText2a,
+                            kidTreelineText2b, valueText.length);
         aUnsafeNames.pop();
       }
     }
   }
 
   let rootStringLength = aRoot.toString().length;
   appendTreeElements2(aP, aProcess, [aRoot._unsafeName], aRoot, aRoot,
-                      aPadText, aPadText, rootStringLength);
+                      aPadText, "", "", rootStringLength);
 }
 
 // ---------------------------------------------------------------------------
 
 function appendSectionHeader(aP, aText) {
   appendElementWithText(aP, "h2", "", aText + "\n");
   return appendElement(aP, "pre", "entries");
 }
@@ -1883,41 +1941,41 @@ function appendSectionHeader(aP, aText) 
 function saveReportsToFile() {
   let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   fp.appendFilter("Zipped JSON files", "*.json.gz");
   fp.appendFilters(Ci.nsIFilePicker.filterAll);
   fp.filterIndex = 0;
   fp.addToRecentDocs = true;
   fp.defaultString = "memory-report.json.gz";
 
-  let fpFinish = function(aFile) {
+  let fpFinish = function(file) {
     let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
                    .getService(Ci.nsIMemoryInfoDumper);
     let finishDumping = () => {
-      updateMainAndFooter("Saved memory reports to " + aFile.path,
+      updateMainAndFooter("Saved memory reports to " + file.path,
                           SHOW_TIMESTAMP, HIDE_FOOTER);
     };
-    dumper.dumpMemoryReportsToNamedFile(aFile.path, finishDumping, null,
+    dumper.dumpMemoryReportsToNamedFile(file.path, finishDumping, null,
                                         gAnonymize.checked);
   };
 
   let fpCallback = function(aResult) {
     if (aResult == Ci.nsIFilePicker.returnOK ||
         aResult == Ci.nsIFilePicker.returnReplace) {
       fpFinish(fp.file);
     }
   };
 
   try {
     fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave);
   } catch (ex) {
     // This will fail on Android, since there is no Save as file picker there.
     // Just save to the default downloads dir if it does.
-    Downloads.getSystemDownloadsDirectory().then(function(aDirPath) {
-      let file = FileUtils.File(aDirPath);
+    Downloads.getSystemDownloadsDirectory().then(function(dirPath) {
+      let file = FileUtils.File(dirPath);
       file.append(fp.defaultString);
       fpFinish(file);
     });
 
     return;
   }
   fp.open(fpCallback);
 }
--- a/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
@@ -266,17 +266,17 @@ WARNING: the following values are negati
 \n\
 This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
 Explicit Allocations\n\
 \n\
 98.00 MB (100.0%) -- explicit\n\
 ├──150.00 MB (153.06%) ── js/compartment(http://too-big.com/)/stuff [?!]\n\
 ├───5.00 MB (05.10%) ── ok\n\
 └──-57.00 MB (-58.16%) -- (2 tiny) [?!]\n\
-   ├───-2.00 MB (-02.04%) ── neg1 [?!]\n\
+   ├───-2.00 MB (-2.04%) ── neg1 [?!]\n\
    └──-55.00 MB (-56.12%) ── heap-unclassified [?!]\n\
 \n\
 Other Measurements\n\
 \n\
  100.00 MB ── heap-allocated\n\
   -0.00 MB ── other1 [?!]\n\
 -222.00 MB ── other2 [?!]\n\
       -333 ── other3 [?!]\n\
@@ -333,24 +333,24 @@ This indicates a defect in one or more m
 Explicit Allocations\n\
 \n\
 99.95 MB (100.0%) -- explicit\n\
 ├──99.00 MB (99.05%) ── big\n\
 └───0.95 MB (00.95%) -- (3 tiny)\n\
     ├──0.96 MB (00.96%) ── heap-unclassified\n\
     ├──0.01 MB (00.01%) -- a\n\
     │  ├──0.04 MB (00.04%) ── pos\n\
-    │  ├──-0.01 MB (-00.01%) ── neg2 [?!]\n\
-    │  └──-0.02 MB (-00.02%) ── neg1 [?!]\n\
-    └──-0.02 MB (-00.02%) -- b/c [?!]\n\
+    │  ├──-0.01 MB (-0.01%) ── neg2 [?!]\n\
+    │  └──-0.02 MB (-0.02%) ── neg1 [?!]\n\
+    └──-0.02 MB (-0.02%) -- b/c [?!]\n\
        ├───0.01 MB (00.01%) ── g/h\n\
        ├───0.00 MB (00.00%) ── i/j\n\
-       └──-0.04 MB (-00.04%) -- d [?!]\n\
+       └──-0.04 MB (-0.04%) -- d [?!]\n\
           ├───0.02 MB (00.02%) ── e\n\
-          └──-0.06 MB (-00.06%) ── f [?!]\n\
+          └──-0.06 MB (-0.06%) ── f [?!]\n\
 \n\
 Other Measurements\n\
 \n\
 100.00 MB ── heap-allocated\n\
 \n\
 End of 5th\n\
 ";
 
@@ -436,17 +436,17 @@ WARNING: the following values are negati
     other5 \n\
 \n\
 This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
 Explicit Allocations\n\
 \n\
 102,760,448 B (100.0%) -- explicit\n\
 ├──157,286,400 B (153.06%) ── js/compartment(http://too-big.com/)/stuff [?!]\n\
 ├────5,242,880 B (05.10%) ── ok\n\
-├───-2,097,152 B (-02.04%) ── neg1 [?!]\n\
+├───-2,097,152 B (-2.04%) ── neg1 [?!]\n\
 └──-57,671,680 B (-56.12%) ── heap-unclassified [?!]\n\
 \n\
 Other Measurements\n\
 \n\
  104,857,600 B ── heap-allocated\n\
         -111 B ── other1 [?!]\n\
 -232,783,872 B ── other2 [?!]\n\
           -333 ── other3 [?!]\n\
@@ -502,24 +502,24 @@ WARNING: the following values are negati
 This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
 Explicit Allocations\n\
 \n\
 104,801,280 B (100.0%) -- explicit\n\
 ├──103,809,024 B (99.05%) ── big\n\
 ├────1,007,616 B (00.96%) ── heap-unclassified\n\
 ├───────10,240 B (00.01%) -- a\n\
 │       ├──40,960 B (00.04%) ── pos\n\
-│       ├──-10,240 B (-00.01%) ── neg2 [?!]\n\
-│       └──-20,480 B (-00.02%) ── neg1 [?!]\n\
-└──────-25,600 B (-00.02%) -- b/c [?!]\n\
+│       ├──-10,240 B (-0.01%) ── neg2 [?!]\n\
+│       └──-20,480 B (-0.02%) ── neg1 [?!]\n\
+└──────-25,600 B (-0.02%) -- b/c [?!]\n\
        ├───10,240 B (00.01%) ── g/h\n\
        ├────5,120 B (00.00%) ── i/j\n\
-       └──-40,960 B (-00.04%) -- d [?!]\n\
+       └──-40,960 B (-0.04%) -- d [?!]\n\
           ├───20,480 B (00.02%) ── e\n\
-          └──-61,440 B (-00.06%) ── f [?!]\n\
+          └──-61,440 B (-0.06%) ── f [?!]\n\
 \n\
 Other Measurements\n\
 \n\
 104,857,600 B ── heap-allocated\n\
 \n\
 End of 5th\n\
 ";
 
--- a/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
@@ -358,37 +358,37 @@ Other Measurements\n\
 \n\
 -0.00 MB (100.0%) -- p8\n\
 └──-0.00 MB (100.0%) -- a\n\
    ├──-0.00 MB (50.00%) -- b\n\
    │  ├──-0.00 MB (31.82%) -- c\n\
    │  │  ├──-0.00 MB (18.18%) ── e [-]\n\
    │  │  └──-0.00 MB (13.64%) ── d [-]\n\
    │  ├──-0.00 MB (22.73%) ── f [-]\n\
-   │  └───0.00 MB (-04.55%) ── (fake child) [!]\n\
+   │  └───0.00 MB (-4.55%) ── (fake child) [!]\n\
    └──-0.00 MB (50.00%) -- g\n\
       ├──-0.00 MB (31.82%) ── i [-]\n\
       ├──-0.00 MB (27.27%) ── h [-]\n\
-      └───0.00 MB (-09.09%) ── (fake child) [!]\n\
+      └───0.00 MB (-9.09%) ── (fake child) [!]\n\
 \n\
 End of P8\n\
 ";
 
   // This is the output for a verbose diff.
   let expectedDiffVerbose =
 "\
 P\n\
 \n\
 WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
 Explicit Allocations\n\
 \n\
 -10,005 B (100.0%) -- explicit\n\
 ├──-10,000 B (99.95%) ── storage/prefixset/goog-phish-shavar\n\
 ├───────-6 B (00.06%) ── spell-check [2]\n\
-└────────1 B (-00.01%) ── xpcom/category-manager\n\
+└────────1 B (-0.01%) ── xpcom/category-manager\n\
 \n\
 Other Measurements\n\
 \n\
 1,002,000 B (100.0%) -- a\n\
 ├──1,000,000 B (99.80%) ── b\n\
 ├──────1,000 B (00.10%) -- c\n\
 │      ├──-999,000 B (-99.70%) ── e\n\
 │      ├──998,000 B (99.60%) ── d\n\
@@ -456,21 +456,21 @@ Other Measurements\n\
 \n\
 -22 B (100.0%) -- p8\n\
 └──-22 B (100.0%) -- a\n\
    ├──-11 B (50.00%) -- b\n\
    │  ├───-7 B (31.82%) -- c\n\
    │  │   ├──-4 B (18.18%) ── e [-]\n\
    │  │   └──-3 B (13.64%) ── d [-]\n\
    │  ├───-5 B (22.73%) ── f [-]\n\
-   │  └────1 B (-04.55%) ── (fake child) [!]\n\
+   │  └────1 B (-4.55%) ── (fake child) [!]\n\
    └──-11 B (50.00%) -- g\n\
       ├───-7 B (31.82%) ── i [-]\n\
       ├───-6 B (27.27%) ── h [-]\n\
-      └────2 B (-09.09%) ── (fake child) [!]\n\
+      └────2 B (-9.09%) ── (fake child) [!]\n\
 \n\
 End of P8\n\
 ";
 
   // This is the output for the crash reports diff.
   let expectedDiff2 =
 "\
 Main Process (pid NNN)\n\